1.前言
1.1 介绍
本篇博客的大部分内容和案例均来源于互联网,因为自己也算不上权威就暂时没有这个必要
1.2 参考资料
2.ES6
ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
ECMAScript 和 JavaScript 的关系
一个常见的问题是,ECMAScript 和 JavaScript 到底是什么关系?
要讲清楚这个问题,需要回顾历史。1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。
该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性和中立性。
因此,ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 Jscript 和 ActionScript)。日常场合,这两个词是可以互换的。
let、const
在 ES2015 的新语法中,影响速度最为直接,范围最大的,恐怕得数 let 和 const 了,它们是继 var 之后,新的变量定义方法。与 let 相比,const 更容易被理解:const 也就是 constant 的缩写,跟 C/C++ 等经典语言一样,用于定义常量,即不可变量。
但由于在 ES6 之前的 ECMAScript 标准中,并没有原生的实现,所以在降级编译中,会马上进行引用检查,然后使用 var 代替。
1 | // foo.js |
块级作用域
在 ES6 诞生之前,我们在给 JavaScript 新手解答困惑时,经常会提到一个观点:
JavaScript 没有块级作用域
在 ES6 诞生之前的时代中,JavaScript 确实是没有块级作用域的。这个问题之所以为人所熟知,是因为它引发了诸如历遍监听事件需要使用闭包解决等问题。
1 | <button>一</button> |
前端新手非常容易写出类似的代码,因为从直观的角度看这段代码并没有语义上的错误,但是当我们点击任意一个按钮时,就会报出这样的错误信息:
Uncaught TypeError: Cannot read property 'innerText' of undefined
出现这个错误的原因是因为 buttons[i] 不存在,即为 undefined。
为什么会出现按钮不存在结果呢?通过排查,我们可以发现,每次我们点击按钮时,事件监听回调函数中得到的变量 i 都会等于 buttons.length,也就是这里的 4。而 buttons[4] 恰恰不存在,所以导致了错误的发生。
再而导致 i 得到的值都是 buttons.length 的原因就是因为 JavaScript 中没有块级作用域,而使对 i 的变量引用(Reference)一直保持在上一层作用域(循环语句所在层)上,而当循环结束时 i 则正好是 buttons.length。
而在 ES6 中,我们只需做出一个小小的改动,便可以解决该问题(假设所使用的浏览器已经支持所需要的特性):
1 | // ... |
通过把 for 语句中对计数器 i 的定义语句从 var 换成 let,即可。因为 let 语句会使该变量处于一个块级作用域中,从而让事件监听回调函数中的变量引用得到保持。我们不妨看看改进后的代码经过 babel 的编译会变成什么样子:
1 | // ... |
实现方法一目了然,通过传值的方法防止了 i 的值错误。
箭头函数(Arrow Function)
继 let 和 const 之后,箭头函数就是使用率最高的新特性了。当然了,如果你了解过 Scala 或者曾经如日中天的 JavaScript 衍生语言 CoffeeScript,就会知道箭头函数并非 ES6 独创。
箭头函数,顾名思义便是使用箭头(=>)进行定义的函数,属于匿名函数(Lambda)一类。当然了,也可以作为定义式函数使用,但我们并不推荐这样做,随后会详细解释。
使用
箭头函数有好几种使用语法:
1 | foo => foo + ' world' // means return `foo + ' world'` |
以上都是被支持的箭头函数表达方式,其最大的好处便是简洁明了,省略了 function 关键字,而使用 => 代替。
箭头函数语言简洁的特点使其特别适合用於单行回调函数的定义:
1 | let names = [ 'Will', 'Jack', 'Peter', 'Steve', 'John', 'Hugo', 'Mike' ] |
箭头函数与上下文绑定
事实上,箭头函数在 ES2015 标准中,并不只是作为一种新的语法出现。就如同它在 CoffeeScript 中的定义一般,是用于对函数内部的上下文 (this)绑定为定义函数所在的作用域的上下文。
1 | let obj = { |
上面代码中的 obj.foo 等价于:
1 | // ... |
注意事项
另外,要注意的是,箭头函数对上下文的绑定是强制性的,无法通过 apply 或 call 方法改变其上下文。
1 | let a = { |
另外,因为箭头函数会绑定上下文的特性,故不能随意在顶层作用域使用箭头函数,以防出错:
1 | // 假设当前运行环境为浏览器,故顶层作上下文为 `window` |
模板字符串
模板字符串模板出现简直对 Node.js 应用的开发和 Node.js 自身的发展起到了相当大的推动作用!我的意思并不是说这个原生的模板字符串能代替现有的模板引擎,而是说它的出现可以让非常多的字符串使用变得尤为轻松。
模板字符串要求使用 ` 代替原本的单/双引号来包裹字符串内容。它有两大特点:
- 支持变量注入
- 支持换行
支持变量注入(useful)
模板字符串之所以称之为“模板”,就是因为它允许我们在字符串中引用外部变量,而不需要像以往需要不断地相加、相加、相加……
1 | let name = 'Will Wen Gunn' |
支持换行
无论是上面的哪一种,都会让我们感到很不爽。但若使用模板字符串,仿佛打开了新世界的大门~
1 | let sql = ` |
对象字面量扩展语法
(本质上还是语法糖,但是挺爽的)
方法属性省略 function (useful)
1 | let obj = { |
同名方法属性省略语法
也是看上去有点鸡肋的新特性,不过在做 JavaScript 模块化工程的时候则有了用武之地。
1 | // module.js |
可以动态计算的属性名称
这个特性相当有意思,也是可以用在一些特殊的场景中。
1 | let arr = [1, 2, 3] |
表达式解构(useful)
1 | // Matching with object |
函数参数表达、传参
这个特性有非常高的使用频率,一个简单的语法糖解决了从前需要一两行代码才能实现的功能。
默认参数值
这个特性在类库开发中相当有用,比如实现一些可选参数:
1 |
|
后续参数
我们知道,函数的 call 和 apply 在使用上的最大差异便是一个在首参数后传入各个参数,一个是在首参数后传入一个包含所有参数的数组。如果我们在实现某些函数或方法时,也希望实现像 call 一样的使用方法,在 ES2015 之前,我们可能需要这样做:
1 | function fetchSomethings() { |
而在 ES2015 中,我们可以很简单的使用 … 语法糖来实现:
1 | function fetchSomethings(...args) { |
要注意的是,…args 后不可再添加
虽然从语言角度看,arguments 和 …args 是可以同时使用 ,但有一个特殊情况则不可:arguments 在箭头函数中,会跟随上下文绑定到上层,所以在不确定上下文绑定结果的情况下,尽可能不要再箭头函数中再使用 arguments,而使用 …args。
虽然 ECMA 委员会和各类编译器都无强制性要求用 …args 代替 arguments,但从实践经验看来,…args 确实可以在绝大部份场景下可以代替 arguments 使用,除非你有很特殊的场景需要使用到 arguments.callee 和 arguments.caller。所以我推荐都使用 …args 而非 arguments。
PS:在严格模式(Strict Mode)中,arguments.callee 和 arguments.caller 是被禁止使用的。
解构传参
在 ES2015 中,… 语法还有另外一个功能:无上下文绑定的 apply。什么意思?看看代码你就知道了。
1 | function sum(...args) { |
有什么卵用?我也不知道(⊙o⊙)… Sorry…
注意事项
默认参数值
和后续参数
需要遵循顺序原则,否则会出错。function(...args, last = 1) {
// This will go wrong
}
新的数据结构
在介绍新的数据结构之前,我们先复习一下在 ES2015 之前,JavaScript 中有哪些基本的数据结构。
- String 字符串
- Number 数字(包含整型和浮点型)
- Boolean 布尔值
- Object 对象
- Array 数组
其中又分为值类型
和引用类型
,Array 其实是 Object 的一种子类。
Set 和 WeakSet
我们再来复习下高中数学吧,集不能包含相同的元素,我们可以根据元素画出多个集的韦恩图…………
好了跑题了。是的,在 ES2015 中,ECMA 委员会为 ECMAScript 增添了集(Set)和“弱”集(WeakSet)。它们都具有元素唯一性,若添加了已存在的元素,会被自动忽略。
1 | let s = new Set() |
在实际开发中,我们有很多需要用到集的场景,如搜索、索引建立等。
WeakSet 在 JavaScript 底层作出调整(在非降级兼容的情况下),检查元素的变量引用情况。如果元素的引用已被全部解除,则该元素就会被删除,以节省内存空间。这意味著无法直接加入数字或者字符串。另外 WeakSet 对元素有严格要求,必须是 Object,当然了,你也可以用 new String(‘…’) 等形式处理元素。
1 | let weaks = new WeakSet() |
Map 和 WeakMap
从数据结构的角度来说,映射(Map)跟原本的 Object 非常相似,都是 Key/Value 的键值对结构。但是 Object 有一个让人非常不爽的限制:key 必须是字符串或数字。在一般情况下,我们并不会遇上这一限制,但若我们需要建立一个对象映射表时,这一限制显得尤为棘手。
而 Map 则解决了这一问题,可以使用任何对象作为其 key,这可以实现从前不能实现或难以实现的功能,如在项目逻辑层实现数据索引等。
1 | let map = new Map() |
而 WeakMap 和 WeakSet 很类似,只不过 WeakMap 的键和值都会检查变量引用,只要其一的引用全被解除,该键值对就会被删除。
1 | let weakm = new WeakMap() |
类(Classes)
类,作为自 JavaScript 诞生以来最大的痛点之一,终于在 ES2015 中得到了官方的妥协,“实现”了 ECMAScript 中的标准类机制。为什么是带有双引号的呢?因为我们不难发现这样一个现象:
1 | $ node |
回想一下在 ES2015 以前的时代中,我们是怎么在 JavaScript 中实现类的?function Foo() {}
var foo = new Foo()
是的,ES6 中的类只是一种语法糖,用于定义原型(Prototype)的。当然,饿死的厨师三百斤,有总比没有强,我们还是很欣然地接受了这一设定。
遗憾与期望
就目前来说,ES2015 的类机制依然很鸡肋:
- 不支持私有属性(private)
- 不支持前置属性定义,但可用 get 语句和 set 语句实现
- 不支持多重继承
没有类似于协议(Protocl)或接口(Interface)等的概念
中肯地说,ES2015 的类机制依然有待加强。但总的来说,是值得尝试和讨论的,我们可以像从前一样,不断尝试新的方法,促进 ECMAScript 标准的发展。
Promise
Promise,作为一个老生常谈的话题,早已被聪明的工程师们“玩坏”了。
光是 Promise 自身,目前就有多种标准,而目前最为流行的是 Promises/A+。而 ES2015 中的 Promise 便是基于 Promises/A+ 制定的。
概念
Promise 是一种用于解决回调函数无限嵌套的工具(当然,这只是其中一种),其字面意义为“保证”。它的作用便是“免去”异步操作的回调函数,保证能通过后续监听而得到返回值,或对错误处理。它能使异步操作变得井然有序,也更好控制。我们以在浏览器中访问一个 API,解析返回的 JSON 数据。
1 | fetch('http://example.com/api/users/top') |
Promise 在设计上具有原子性,即只有两种状态:未开始和结束(无论成功与否都算是结束),这让我们在调用支持 Promise 的异步方法时,逻辑将变得非常简单,这在大规模的软件工程开发中具有良好的健壮性。
基本用法
创建 Promise 对象
要为一个函数赋予 Promise 的能力,先要创建一个 Promise 对象,并将其作为函数值返回。Promise 构造函数要求传入一个函数,并带有 resolve 和 reject 参数。这是两个用于结束 Promise 等待的函数,对应的成功和失败。而我们的逻辑代码就在这个函数中进行。
此处,因为必须要让这个函数包裹逻辑代码,所以如果需要用到 this 时,则需要使用箭头函数或者在前面做一个 this 的别名。
1 | function fetchData() { |
进行异步操作
事实上,在异步操作内,并不需要对 Promise 对象进行操作(除非有特殊需求)。
1 | function fetchData() { |
因为在 Promise 定义的过程中,也会出现数层回调嵌套的情况,如果需要使用 this 的话,便显现出了箭头函数的优势了。
使用 Promise
让异步操作函数支持 Promise 后,我们就可以享受 Promise 带来的优雅和便捷了~
1 | fetchData() |
弊端
虽说 Promise 确实很优雅,但是这是在所有需要用到的异步方法都支持 Promise 且遵循标准。而且链式 Promise 强制性要求逻辑必须是线性单向的,一旦出现如并行、回溯等情况,Promise 便显得十分累赘。
所以在目前的最佳实践中,Promise 会作为一种接口定义方法,而不是逻辑处理工具。后文将会详细阐述这种最佳实践。
Symbol
Symbol 是一种很有意思的概念,它跟 Swift 中的 Selector 有点相像,但也更特别。在 JavaScript 中,对象的属性名称可以是字符串或数字。而如今又多了一个 Symbol。那 Symbol 究竟有什么用?
首先,我们要了解的是,Symbol 对象是具有唯一性的,也就是说,每一个 Symbol 对象都是唯一的,即便我们看不到它的区别在哪里。这就意味著,我们可以用它来保证一些数据的安全性。console.log(Symbol('key') == Symbol('key')) //=> false
如果将一个 Symbol 隐藏于一个封闭的作用域内,并作为一个对象中某属性的键,则外层作用域中便无法取得该属性的值,有效保障了某些私有库的代码安全性。
1 | let privateDataStore = { |
Proxy(代理)
Proxy 是 ECMAScript 中的一种新概念,它有很多好玩的用途,从基本的作用说就是:Proxy 可以在不入侵目标对象的情况下,对逻辑行为进行拦截和处理。
比如说我想记录下我代码中某些接口的使用情况,以供数据分析所用,但是因为目标代码中是严格控制的,所以不能对其进行修改,而另外写一个对象来对目标对象做代理也很麻烦。那么 Proxy 便可以提供一种比较简单的方法来实现这一需求。
注意,要使得代理起作用,必须对proxy对象操作而不是原对象
假设我要对 api 这一对象进行拦截并记录下代码行为,我就可以这样做:
1 | let apiProxy = new Proxy(api, { |
3.ES7
async/await
上文中我们提及到 co 是一个利用 Generator 模拟 ES7 中 async/await 特性的工具,那么,这个 async/await 究竟又是什么呢?它跟 co 又有什么区别呢?
我们知道,Generator Function 与普通的 Function 在执行方式上有著本质的区别,在某种意义上是无法共同使用的。但是,对于 ES7 的 Async Function 来说,这一点并不存在!它可以以普通函数的执行方式使用,并且有著 Generator Function 的异步优越性,它甚至可以作为事件响应函数使用。
1 | async function fetchData() { |
Decorators
对于 JavaScript 开发者来说,Decorators 又是一种新的概念,不过它在 Python 等语言中早已被玩出各种花式。
Decorator 的定义如下:
- 是一个表达式
- Decorator 会调用一个对应的函数
- 调用的函数中可以包含 target(装饰的目标对象)、name(装饰目标的名称)和 descriptor(描述器)三个参数
- 调用的函数可以返回一个新的描述器以应用到装饰目标对象上
PS:如果你不记得 descriptor 是什么的话,请回顾一下 Object.defineProperty() 方法。
简单实例
我们在实现一个类的时候,有的属性并不想被 for..in 或 Object.keys() 等方法检索到,那么在 ES5 时代,我们会用到 Object.defineProperty() 方法来实现:
1 | var obj = { |
那么在 ES7 中,我们可以用 Decorator 来很简单地实现这个需求:
1 | class Obj { |
修饰器
修饰器是一个对类进行处理的函数
1 | function testable(isTestable) { |
黑科技
正如上面所说,Decorator 在编程中早已不是什么新东西,特别是在 Python 中早已被玩出各种花样。聪明的工程师们看到 ES7 的支持当然不会就此收手,就让我们看看我们还能用 Decorator 做点什么神奇的事情。
假如我们要实现一个类似于 Koa 和 PHP 中的 CI 的框架,且利用 Decorator 特性实现 URL 路由,我们可以这样做。
1 | // 框架内部 |
最重要的是,同一个修饰对象是可以同时使用多个修饰器的,所以说我们还可以用修饰器实现很多很多有意思的功能。
编程风格
let取代var
let和var作用相似,而且let没有副作用
全局常量和线程安全
在let和const之间,建议优先选择const,尤其是在全局环境,不应该设置变量,只应该设置常量
const优于let有几个原因。一个是const可以提醒阅读程序的人,这个变量不应该改变;另一个是const比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对const进行优化,所以多使用const,有利于提高程序的运行效率,也就是说let和const的本质区别,其实是编译器内部的处理不同。
const声明常量还有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。
所有的函数都应该设置为常量。
长远来看,JavaScript 可能会有多线程的实现(比如 Intel 公司的 River Trail 那一类的项目),这时let表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。
字符串
静态字符串一律使用单引号或者反引号,动态字符串使用双引号
1 | // bad |
解构赋值
使用数组成员对变量赋值时,优先使用解构赋值。
1 | const arr = [1, 2, 3, 4]; |
函数的参数如果是对象的成员,优先使用解构赋值。
1 | // bad |
如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。
1 | // bad |
对象
单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。
1 | // bad |
对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。
1 | // bad |
对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写
1 | var ref = 'some value'; |
数组
使用扩展运算符(…)拷贝数组。
1 | // bad |
使用 Array.from 方法,将类似数组的对象转为数组
1 | const foo = document.querySelectorAll('.foo'); |
函数
立即执行函数可以写成箭头函数的形式。
1 | (() => { |
那些需要使用函数表达式的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。
1 | // bad |
所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。
1 | // bad |
不要在函数体内使用 arguments 变量,使用 rest 运算符(…)代替。因为 rest 运算符显式表明你想要获取参数,而且 arguments 是一个类似数组的对象,而 rest 运算符可以提供一个真正的数组。
1 | // bad |
使用默认值语法设置函数参数的默认值
1 | // bad |
map结构
注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。
1 | let map = new Map(arr); |
class
总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解
1 | // bad |
使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。
1 | // bad |
模块
首先,Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用import取代require
1 | // bad |
如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export default,export default与普通的export不要同时使用
不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default
1 | // bad |
如果模块默认输出一个函数,函数名的首字母应该小写。
1 | function makeStyleGuide() { |
如果模块默认输出一个对象,对象名的首字母应该大写。
1 | const StyleGuide = { |
ES6的模块和commonJs的不同
- commonjs模块输出的是一个值的拷贝,es6输出的是值的引用
- commonjs是运行时加载,es6是编译时输出接口
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。
1 | // lib.js |
上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
1 | // lib.js |
上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。
1 | $ node main.js |
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
1 | // lib.js |
上面代码说明,ES6 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化
ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块
由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。
1 | // lib.js |
上面代码中,main.js从lib.js输入变量obj,可以对obj添加属性,但是重新赋值就会报错。因为变量obj指向的地址是只读的,不能重新赋值,这就好比main.js创造了一个名为obj的const变量
最后,export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例
1 | // mod.js |
上面的脚本mod.js,输出的是一个C的实例。不同的脚本加载这个模块,得到的都是同一个实例