14.2 ES6 新特性介绍
箭头函数和词法this(Arrows and Lexical This)
JS中可以使用一些箭头符号语法:
<!-- | 单行注释 |
--> | “趋向于”操作符,语义为goes to |
<= | 小于等于 |
=> | ES6引入的箭头,用于函数简写 |
本节说明ES6中新引入的箭头(=>)语法的使用。箭头函数是使用=>
符号语法表示的函数简写,和C#,Java 8中的语法类似。箭头同时支持表达式和声明体。
下面分别使用ES5和ES6编写的一个功能等同的代码,可以看出箭头函数的基本语法,箭头左边是参数,右边是函数体:
// ES5
var selected = allJobs.filter(function (job) {
return job.isSelected();
});
// ES6
var selected = allJobs.
filter(job => job.isSelected());
函数有多个参数的情况,使用括号包含起来:
// ES5
var total = values.reduce(function (a, b) {
return a + b;
}, 0);
// ES6
var total = values.reduce((a, b) => a + b, 0);
和函数不同的是,箭头(arrows)和其外围代码分享相同的词法作用域this, 也就是, 箭头函数没有自己的this值,其this值继承自外围作用域。类似的,如果一个arrow在另外一个函数里面,它将分享其父函数的参数变量。
这带来一个明显的好处,我们知道以前在JS函数里面的匿名函数中,如果要使用外部函数调用者的this变量,我们得自己保存一个函数范围的局部变量,像下面这样:
{
...
addAll: function addAll(pieces) {
var self = this;
_.each(pieces, function (piece) {
self.add(piece);
});
},
...
}
那么现在使用箭头函数,我们就可以直接使用外部函数的this值。
// ES6
{
...
addAll: function addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
下面有几个箭头函数的实际使用例子:
// Expression bodies
var odds = evens.map(v => v + 1);
var nums = evens.map((v, i) => v + i);
// Statement bodies
nums.forEach(v => {
if (v % 5 === 0)
fives.push(v);
});
// Lexical this
var bob = {
_name: "Bob",
_friends: [],
printFriends() {
this._friends.forEach(f =>
console.log(this._name + " knows " + f));
}
};
// Lexical arguments
function square() {
let example = () => {
let numbers = [];
for (let number of arguments) {
numbers.push(number * number);
}
return numbers;
};
return example();
}
square(2, 4, 7.5, 8, 11.5, 21); // returns: [4, 16, 56.25, 64, 132.25, 441]
注意:以上的例子代码不需要Babel的支持。
类(Classes)
ES6终于在JS中引入了类(Class)这个面向对象编程的基本概念。相对于基于原型的面向对象(prototype-based OO)模式,这样单一简便的声明方式更容易被理解和使用。类支持基于原型的继承、父类调用、实例化、静态方法和构造函数。
下面的代码来自著名的Three.js 3D引擎:
class SkinnedMesh extends THREE.Mesh {
constructor(geometry, materials) {
super(geometry, materials);
this.idMatrix = SkinnedMesh.defaultMatrix();
this.bones = [];
this.boneMatrices = [];
//...
}
update(camera) {
//...
super.update();
}
static defaultMatrix() {
return new THREE.Matrix4();
}
}
继承
如上面的代码,当我们想从Mesh派生子类对象时,可以使用extends和super语法,super指向父类。使用内置的 extends 实现继承比ES5中的原型继承具有更好的可读性和维护性。
模板字符串(Template Strings)
模板字符串给构造字符串带来便利。我们可以使用可选的标签来定制字符串的构建,这样可以避免注入,以及更高级的内容。
// 基础用法
`This is a pretty little template string.`
// 多行字符串
`In ES5 this is
not legal.`
// 插入变量绑定
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
// 非转义模板字符串
String.raw`In ES5 "\n" is a line-feed.`
解构赋值(Destructuring)
解构赋值允许你使用模式匹配的语法将数组和对象的属性赋给各种变量。这种赋值语法简洁紧凑,同时还比传统的属性访问方法更为清晰。
通常来说,你很可能这样访问数组中的前三个元素:
var first = someArray[0];
var second = someArray[1];
var third = someArray[2];
如果使用解构赋值的特性,将会使等效的代码变得更加简洁并且可读性更高:
var [first, second, third] = someArray;
SpiderMonkey(Firefox的JavaScript引擎)已经支持解构的大部分功能,但是仍不健全。你可以通过bug 694100跟踪解构和其它ES6特性在SpiderMonkey中的支持情况。
数组与迭代器的解构
以上是数组解构赋值的一个简单示例,其语法的一般形式为:
[ variable1, variable2, ..., variableN ] = array;
这将为variable1到variableN的变量赋予数组中相应元素项的值。如果你想在赋值的同时声明变量,可在赋值语句前加入var
、let
或const
关键字,例如:
var [ variable1, variable2, ..., variableN ] = array;
let [ variable1, variable2, ..., variableN ] = array;
const [ variable1, variable2, ..., variableN ] = array;
事实上,用变量
来描述并不恰当,因为你可以对任意深度的嵌套数组进行解构:
var [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo);
// 1
console.log(bar);
// 2
console.log(baz);
// 3
此外,你可以在对应位留空来跳过被解构数组中的某些元素:
var [,,third] = ["foo", "bar", "baz"];
console.log(third);
// "baz"
而且你还可以通过“不定参数”模式捕获数组中的所有尾随元素:
var [head, ...tail] = [1, 2, 3, 4];
console.log(tail);
// [2, 3, 4]
当解构失败时,会以软错误(fail-soft)的形式处理,最终得到的结果都是:undefined
。
console.log([][0]);
// undefined
var [missing] = [];
console.log(missing);
// undefined
数组解构赋值的模式同样适用于迭代器:
function* fibs() {
var a = 0;
var b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
var [first, second, third, fourth, fifth, sixth] = fibs();
console.log(sixth);
// 5
对象的解构
通过解构对象,你可以把它的每个属性与不同的变量绑定,首先指定被绑定的属性,然后紧跟一个要解构的变量。
var robotA = { name: "Bender" };
var robotB = { name: "Flexo" };
var { name: nameA } = robotA;
var { name: nameB } = robotB;
console.log(nameA);
// "Bender"
console.log(nameB);
// "Flexo"
当属性名与变量名一致时,可以简写如下:
var { foo, bar } = { foo: "lorem", bar: "ipsum" };
console.log(foo);
// "lorem"
console.log(bar);
// "ipsum"
与数组解构一样,你可以随意嵌套并进一步组合对象解构:
var complicatedObj = {
arrayProp: [
"Zapp",
{ second: "Brannigan" }
]
};
var { arrayProp: [first, { second }] } = complicatedObj;
console.log(first);
// "Zapp"
console.log(second);
// "Brannigan"
类似的,当你解构一个未定义的属性时,得到的值为undefined
:
var { missing } = {};
console.log(missing);
// undefined
请注意,当你解构对象并赋值给变量时,如果你已经声明或不打算声明这些变量(亦即赋值语句前没有let
、const
或var
关键字),你应该注意这样一个潜在的语法错误:
{ blowUp } = { blowUp: 10 };
// Syntax error 语法错误
为什么会出错?这是因为JavaScript语法通知解析引擎将任何以{开始的语句解析为一个块语句(例如,{console}
是一个合法块语句)。解决方案是将整个表达式用一对小括号包裹:
({ safe } = {});
// No errors 没有语法错误
解构值不是对象、数组或迭代器
当你尝试解构null
或undefined
时,你会得到一个类型错误:
var {blowUp} = null;
// TypeError: null has no properties(null没有属性)
然而,你可以解构其它原始类型,例如:布尔值
、数值
、字符串
,但是你将得到undefined
:
var {wtf} = NaN;
console.log(wtf);
// undefined
原因是,当使用对象赋值模式时,被解构的值需要被强制转换为对象。大多数类型都可以被转换为对象,但null
和undefined
却无法进行转换。当使用数组赋值模式时,被解构的值一定要包含一个迭代器。
默认值
当你要解构的属性未定义时你可以提供一个默认值:
var [missing = true] = [];
console.log(missing);
// true
var { message: msg = "Something went wrong" } = {};
console.log(msg);
// "Something went wrong"
var { x = 3 } = {};
console.log(x);
// 3
解构的实际应用
函数参数定义
作为开发者,我们需要实现设计良好的API,通常的做法是为函数设计一个对象作为参数,然后将不同的实际参数作为对象属性,以避免让API使用者记住多个参数的使用顺序。我们可以使用解构特性来避免这种问题,当我们想要引用它的其中一个属性时,大可不必反复使用这种单一参数对象。
function removeBreakpoint({ url, line, column }) {
// ...
}
这是一段来自Firefox开发工具JavaScript调试器(同样使用JavaScript实现)的代码片段。
配置对象参数
延伸一下之前的示例,我们同样可以给需要解构的对象属性赋予默认值。当我们构造一个提供配置的对象,并且需要这个对象的属性携带默认值时,解构特性就派上用场了。举个例子,jQuery的ajax
函数使用一个配置对象作为它的第二参数,我们可以这样重写函数定义:
jQuery.ajax = function (url, {
async = true,
beforeSend = noop,
cache = true,
complete = noop,
crossDomain = false,
global = true,
// ... 更多配置
}) {
// ... do stuff
};
如此一来,我们可以避免对配置对象的每个属性都重复var foo = config.foo || theDefaultFoo;
这样的操作。
与ES6迭代器协议协同使用
ECMAScript 6中定义了一个迭代器协议,当你迭代Maps(ES6标准库中新加入的一种对象)后,你可以得到一系列形如[key, value]
的键值对,我们可通过键值对解构来轻松地访问键和值:
var map = new Map();
map.set(window, "the global");
map.set(document, "the document");
for (var [key, value] of map) {
console.log(key + " is " + value);
}
// "[object Window] is the global"
// "[object HTMLDocument] is the document"
只遍历键:
for (var [key] of map) {
// ...
}
或只遍历值:
for (var [,value] of map) {
// ...
}
多重返回值
JavaScript语言中尚未整合多重返回值的特性,但是无须多此一举,因为你自己就可以返回一个数组并将结果解构:
function returnMultipleValues() {
return [1, 2];
}
var [foo, bar] = returnMultipleValues();
或者,你可以用一个对象作为容器并为返回值命名:
function returnMultipleValues() {
return {
foo: 1,
bar: 2
};
}
var { foo, bar } = returnMultipleValues();
这两个模式都比额外保存一个临时变量要好得多。
function returnMultipleValues() {
return {
foo: 1,
bar: 2
};
}
var temp = returnMultipleValues();
var foo = temp.foo;
var bar = temp.bar;
Chrome中有关解构的支持正在开发中,其它浏览器也将适时增加支持。所以你可能需要使用Babel或Traceur将ES6代码转译为相应的ES5代码。
缺省值(Default)
function f(x, y=12) {
// y is 12 if not passed (or passed as undefined)
return x + y;
}
剩余(Rest) 和 展开(Spread)语法
剩余和展开的语法类似,都是使用三个点(...)的符号前缀。spread语法 允许表达式在出现多个参数(函数调用)或者多个元素(数组操作)或者多个变量(解构赋值)时进行扩展。
和spread展开元素不同的是,rest用多个值来组装元素。
Spread语法
函数调用:
myFunction(...iterableObj);
数组值:
[...iterableObj, 4, 5, 6]
我们经常在需要使用数组为参数调用函数时,使用apply,如下面所示:
function myFunction(x, y, z) { } var args = [0, 1, 2]; myFunction.apply(null, args);
使用ES6的spread语法,你可以简化为:
function myFunction(x, y, z) { } var args = [0, 1, 2]; myFunction(...args);
上面的spread语法把args数组展开成函数的x,y,z参数。这可以用在new一个对象中:
var dateFields = readDateFields(database); var d = new Date(...dateFields);
可以在任何参数上应用spread语法,并可以应用多次:
function myFunction(v, w, x, y, z) { } var args = [0, 1]; myFunction(-1, ...args, 2, ...[3]);
上面的代码把[0, 1]数组值展开给函数参数 w 和 x。把[3]展开给 z。
我们还可以使用spread来简化数组操作,以前我们要往已有数组中插入数据比较麻烦,要调用push,concat,slice等等,现在我们可以像下面这样操作,非常直观:
var parts = ['shoulders', 'knees']; var lyrics = ['head', ...parts, 'and', 'toes']; // ["head", "shoulders", "knees", "and", "toes"]
spread只能被用在可遍历的对象上,因此下面的代码会报错:
var obj = {"key1":"value1"}; var array = [...obj]; // TypeError: obj is not iterable
Rest语法
rest语法允许我们表示不确定数目的参数数组。
function(a, b, ...theArgs) { // ... }
上面的代码表示从第三个参数开始的所有其余参数都将被放到theArgs数组元素中。rest参数和函数默认的arguments
不同。
rest只代表一部分,而arguments代表全部,rest是一个数组,arguments不是。
使用let
ES6(ECMAScript 6)引入了新关键词来声明变量:let。和使用var声明不同的是,var是函数范围(function-scoped)而let是块范围(block-scoped):也就是这些变量只在定义块内部有效。 我们用例子来说明其中的差异。
var example = function(p1) { if (p1) { var demo = p1 + 10; } var ret = demo - 2; return ret; };
上面的代码不会报错,因为demo变量是函数范围的,在整个example函数范围内都可以使用。但是如果把var全部替换成let,则将报错demo变量未定义,因为demo变量将只在if语句块中有效。
使用let的好处是使得变量声明的有效范围更为严格,不会导致意外的冲突。
使用const
我们可以使用const来声明一个常量,该常量将不能被修改(赋值)。
迭代器(Iterators) 和 For..Of
迭代器和C++ STL中的概念类似,实现一些指定的接口,以使得可以使用统一的方式来遍历数据。for..of
就是依赖于迭代器来实现的。
let fibonacci = {
[Symbol.iterator]() {
let pre = 0, cur = 1;
return {
next() {
[pre, cur] = [cur, pre + cur];
return { done: false, value: cur }
}
}
}
}
上面的代码中fibonacci对象中包含一个[Symbol.iterator]() 方法,Symbol是ES6新引入的概念,为了避免函数名冲突,这里暂不讨论。一个拥有 [Symbol.iterator]() 方法的对象被认为是可遍历的(iterable),可使用for..of来遍历。 对 for-of 语句来说,它首先调用被遍历集合对象的 [Symbol.iterator]() 方法,该方法返回一个迭代器对象,迭代器对象可以是拥有 .next 方法的任何对象;然后,在 for-of 的每次循环中,都将调用该迭代器对象上的 .next 方法。所以下面的代码将把1000以下的斐波那契数字打印出来。
for (var n of fibonacci) {
// truncate the sequence at 1000
if (n > 1000)
break;
console.log(n);
}
Firefox所有发布版本和 Chrome 50+ 版本都已经支持 iterator 和 for..of 语法,对于不支持的浏览器,可以启用Babel来编译为ES5。
迭代基于如下动态类型(duck-typed)接口:
interface IteratorResult {
done: boolean;
value: any;
}
interface Iterator {
next(): IteratorResult;
}
interface Iterable {
[Symbol.iterator](): Iterator
}
ES6 的迭代器通过 .done 和 .value 这两个属性来标识每次的遍历结果,这就是迭代器的设计原理,这与其他语言中的迭代器有所不同。 在 Java 中,迭代器对象要分别使用 .hasNext()和 .next() 两个方法。在 Python 中,迭代器对象只有一个 .next() 方法,当没有可遍历的元素时将抛出一个 StopIteration 异常。这些设计都是为了控制遍历过程。
在没有for..of之前,我们使用for..in和ES5引入的forEach来完成遍历,相比而言,for..of具备如下特点:
- 这是遍历数组最简单直接的方法
- 避免了所有
for–in
语法存在的坑 - 与
forEach()
不同的是,它支持break
、continue
和return
语句。 - for–in 用于遍历对象的属性,而for-of 用于遍历数据如数组元素。
生成器(Generators)
生成器(Generators)使用function*
和 yield
语法:
function* talkcat(name) { yield "hello " + name + "!"; yield "i hope you are enjoying the blog posts"; if (name.startsWith("X")) { yield "it's cool how your name starts with X, " + name; } yield "see you later!"; }
这看上去很像一个函数,这被称为 Generator 函数,它与我们常见的函数有很多共同点,但还可以看到下面两个差异:
- 通常的函数以
function
开始,但 Generator 函数以function*
开始。 - 在 Generator 函数内部,
yield
是一个关键字,和return
有点像。不同点在于,所有函数(包括 Generator 函数)都只能返回一次,而在 Generator 函数中可以 yield 任意次。yield 表达式暂停了 Generator 函数的执行,然后可以从暂停的地方恢复执行。
常见的函数不能暂停执行,而 Generator 函数可以,这就是这两者最大的区别。
我们来看看调用talkcat时,会返回什么:
> var iter = talkcat("wow"); [object Generator] > iter.next() { value: "hello wow!", done: false } > iter.next() { value: "i hope you are enjoying the blog posts", done: false } > iter.next() { value: "see you later!", done: false } > iter.next() { value: undefined, done: true }
Generator 函数的调用方法与普通函数一样:talkcat("wow")
,但调用一个 Generator 函数时并没有立即执行,而是返回了一个 Generator 对象(上面代码中的 iter
),这时函数就立即暂停在函数代码的第一行。
每次调用 Generator 对象的 .next()
方法时,函数就开始执行,直到遇到下一个 yield 表达式为止。
当执行最后一个 iter.next()
时,就到达了 Generator 函数的末尾,所以返回结果的 .done
属性值为 true
,并且 .value
属性值为 undefined
。
从技术层面上讲,每当 Generator 函数执行遇到 yield 表达式时,函数的栈帧 — 本地变量,函数参数,临时值和当前执行的位置,就从堆栈移除,但是 Generator 对象保留了对该栈帧的引用,所以下次调用 .next()
方法时,就可以恢复并继续执行。
值得提醒的是 Generator 并不是多线程。在支持多线程的语言中,同一时间可以执行多段代码,并伴随着执行资源的竞争,执行结果的不确定性和较好的性能。而 Generator 函数并不是这样,当一个 Generator 函数执行时,它与其调用者都在同一线程中执行,每次执行顺序都是确定的,有序的,并且执行顺序不会发生改变。与线程不同,Generator 函数可以在内部的 yield 的标志点暂停执行。
通过介绍 Generator 函数的暂停、执行和恢复执行,我们知道了什么是 Generator 函数,那么现在抛出一个问题:Generator 函数到底有什么用呢?
生成器(Generators)可以简化迭代器的创建。Generators 实际上是 iterators 的子类型,内置实现了 next
和 throw
接口:
interface Generator extends Iterator {
next(value?: any): IteratorResult;
throw(exception: any);
}
这样开发人员可以不用重复去实现这些接口:
var fibonacci = {
[Symbol.iterator]: function*() {
var pre = 0, cur = 1;
for (;;) {
var temp = pre;
pre = cur;
cur += temp;
yield cur;
}
}
}
for (var n of fibonacci) {
// truncate the sequence at 1000
if (n > 1000)
break;
console.log(n);
}
Unicode
全面支持Unicode,包括新的字符串unicode语法格式和新的正则表达式(RegExp),使用 u
符号来处理编码节点,以及新的字符串处理接口。这些增强使得Javascript可以构建全球化的应用程序。
// same as ES5.1 console.log("