前记
按照规划,明年年中,ECMAScript 6(ES6)就要正式发布了。
最近抽空看了Dr. Axel Rauschmayer的几篇文章和演讲PPT,对新特性有了些了解。
趁没忘,抓紧记录下,夹杂自己的感受。
计划分三部分:
- 新语法
- 面对对象和模块化
- 标准库扩充
参考了以下文章/PPT:
- Use ECMAScript 6 today
- Ecmascript 6 Whats next for Javascript
- es6 features
- ECMAScript 6: arrow functions and method definitions
- Callable entities in ECMAScript 6
- Iterators and generators in ECMAScript 6
其他文章:
- ECMAScript 6新特性印象之二:面对对象和模块化
总体印象
的确是「design by champions」。各种为了代码书写效率进行的优化,借鉴了近年各种「新」语言的优秀特性,灵活性大大提升,阅读难度也提升了……
不过,熟悉Ruby的看了这些会放心不少吧。
新语法
1.块级作用域 关键字let
, const
function order(x, y) {
if (x > y) {
let tmp = x;
x = y;
y = tmp;
}
console.log(tmp === x); // 引用错误:tmp此时未定义
return [x,y];
}
JS终于有了块级作用域变量。虽然在代码结构层面没有太大的作用(以前没有时也活得很好么,虽然不怎么舒服),但会让代码更加准确,更易于阅读。
今年夏天发布的Swift中也增加了let
关键字,虽然有些许区别,但目的应该是差不多——提升代码可读性。
2.对象字面量的属性赋值简写 property value shorthand
let first = 'Bob';
let last = 'Dylan';
let singer = { first, last };
console.log(singer.first + " " + singer.last); // Bob Dylan
这对于经常使用对象作为配置属性参数的苦主来说,算个小小的抚慰了。估计重复添加同一属性会报错吧,没有验证。
3.方法定义 Method definitions
let obj = {
myMethod(arg0, arg1) {
...
}
};
避免了在对象定义中出现function
关键字,更加清晰明确地分离出函数的三种用途。
4.赋值解构 Destructuring
let singer = { first: "Bob", last: "Dylan" };
let { first: f, last: l } = singer; // 相当于 f = "Bob", l = "Dylan"
依然是为了方便。以后代码头部的「变量定义区域」不会有太多行了。
数组也是可以的,下面这个例子特别棒:
let [all, year, month, day] =
/^(\d\d\d\d)-(\d\d)-(\d\d)$/.exec("2014-08-31");
let [x, y] = [1, 2, 3]; // x = 1, y = 2
当然也可以这样,但有些……:
function f([x]) {...} // 参数定义
f(['Blonde on Blonde']);
下面是几种错误用法(Refutable):
let { a: x, b: y } = {a: 3}; // TypeError
let [x, y] = ['a']; // TypeError
更重要的是,支持默认值,在形式不匹配或目标值undefined时有效:
let { a: x, b: y=5 } = {a: 3, b: undefined }; // x = 3, y = 5
let [x, y='b'] = ['a']; // x = 'a', y = 'b'
5.函数的多项返回值 Multiple return values
function findSong(songs, songTitle) {
for (let trackNumber = 0; trackNumber < songs.length; trackNumber++) {
let song = songs[trackNumber];
if(songTitle ===song.title) {
return {song, trackNumber};
}
}
return {song: undefined, trackNumber: -1}
}
let songList = ["Tombstone blues", "Don't think twice", "North country girl"];
let {song, trackNumber} = findSong(songList, "North country girl"); // song = "North country girl", trackNumber = 2;
因为赋值解构,所以也可以这样:
let {song} = findSong(...);
let {trackNumber} = findSong(...);
let {trackNumber, song} = findSong(...); // 变量顺序不重要
其实就是返回个对象。
但也有个问题,变量名一定要与函数返回对象的属性名相同,这可以会是一个别扭点。
6.函数参数 - 默认值
function findArtist(name='', genre='') {
...
}
没什么好说的,以后不用再写var option = option || {}
了。
7.函数参数 - 参数打包 Rest parameters
function createArtistProfile(name, ...details) {
.. // details是个数组
}
所以,以后也不需要arguments
了。不过,看例子只是「1,rest」,不知可不可以「1,2,3,rest」。
8.函数参数 - 数组展开 Spread parameters
Math.max(...[1,11,111]); // 111
算是参数打包的逆操作,以后不用写[1,2,3].apply(Math.max)
这类代码了。
9.函数参数 - 指名参数 Named parameters
function func(arg0, {opt1, opt2}) {
return [opt1, opt2];
}
func(0, {opt1: 'a', opt2: 'b'}) // ['a', 'b']
同样是通过对象带来的变化。有个复杂点的例子:
class Entries {
// ...
selectEntries({ from = 0, to = this.length } = {}) {
// Long: { from: from=0, to: to=this.length }
// Use `from` and `to`
}
}
let entries = new Entries();
entries.selectEntries({ from: 5, to: 15 });
entries.selectEntries({ from: 5 });
entries.selectEntries({ to: 15 });
指名参数+赋值解构+默认参数,看着反而有点混乱了……自由度大自然带来阅读难度的上升,这又是一个权衡点。
10.胖箭头函数 Arrow functions
let bob = {
name: "Bob Dylan",
holdConcert: function (songList) {
songList.forEach(song => {
console.log(this.name + " sang " + song)
});
}
}
这里形式上借鉴了CoffeeScript里「fat arrow」(ES6对执行和内存上有优化)。Arrow functions主要做了两件事:
- 简化了代码形式,默认
return
表达式结果。 - 自动绑定语义this,即定义函数时的this。如上面例子中,
forEach
的匿名函数参数中用到的this
。
来看几个例子:
let squares = [ 1, 2, 3 ].map(x => x * x);
x => x + this.y
// 相当于
function(x) { return x + this.y }.bind(this)
// 但胖箭头在执行效率上会更高
胖箭头函数与正常函数的区别:
- 胖箭头在创建时即绑定this(lexical this);正常函数的this是在执行时动态传入的(dynamic this)。
- 胖箭头没有内部方法
[[Construct]]
和属性原型,所以new (() => {})
是会报错的。 - 胖箭头没有
arguments
变量。
这样,以后在定义方法/函数时,就有了清晰的选择:
- 定义子程序(subroutine),用胖箭头,自动获得语义this。
- 定义方法(method),用正常函数,动态this。而且可以用方法定义特性简写代码,避免
function
关键字出现。
11.字符串模板 Template strings
templateHandler`Hello ${first} ${last}!`
${first}
这样的结构在Ruby的字符串处理很常见,first
是动态替换的部分。templateHandler
是替换后的处理函数。
当然也可以不要handler,那就仅仅是模板替换了:
if(x > MAX) {
throw new Error(`At most ${MAX} allowed: $(x)!`);
}
Template strings支持多行,其间的文本也不会被转码:
var str = String.raw`This is a text
with multiple lines.
Escapes are not interpreted,
\n is not a newline.`;
结合不同的handler,用法多样,比如正则:
let str = "Bob Dylan - 2009 - Together Through Life";
let albumInfo = str.match(XRegExp.rx`
^(?<artist>[^/]+ ) - (?<year>\d{4}) - (?<albumTitle>[^/]+)$
`);
console.log(albumInfo.year); // 2009
12.迭代器 Iterators
稍微熟悉函数式编程(Python,Ruby也可以)的朋友对着这个概念应该都不陌生。ES6参考了Python的设计,迭代器有个next
方法,调用会返回:
- 返回迭代对象的一个元素:
{ done: false, value: elem }
- 如果已到迭代对象的末端:
{done: true[, value: retVal] }
上面第二种情况中的条件返回部分是为了递归调用生成器而设计的(迭代器其实是生成器的应用之一),具体说明参见这篇文章的对应部分。
下例实现了一个数组的迭代器:
function createArrayIterator(arr) {
let index = 0;
return {
next() {
if (index < arr.length) {
return { done: false, value: arr[index++] };
else {
return { done: true }
}
}
}
}
let arr = [1,2,3];
let iter = createArrayIterator(arr);
console.log(iter.next()); // 1
console.log(iter.next()); // 2
在ES6中,可迭代数据结构(比如数组)都必须实现一个名为Symbol.iterator
的方法,该方法返回一个该结构元素的迭代器。注意,Symbol.iterator
是一个Symbol,Symbol是ES6新加入的原始值类型。
针对可迭代的数据结构,ES6还引入了一个新的遍历方法 for-of。再举个例子,改造下上例中的createArrayIterator
:
function createArrayIterator(arr) {
let index = 0;
return {
[Symbol.iterator]() {
return this; // 因为本身就是个迭代器
},
next() {
...
}
}
}
let arr = [1, 2, 3];
for(x of createArrayIterator(arr)) { // 注意看
console.log(x);
}
当然,ES6中的数组本身就是可迭代的,上例仅仅是为了展示而已。
13.生成器 Generators
ES6的生成器同样借鉴了Python,通过操作符yield
来挂起、继续。
生成器的写法比较怪异,使用了关键字function*
:
function* generatorFunction() {
yield 1;
yield 2;
}
生成器返回一个对象,用来控制生成器执行,这个对象是可迭代的:
let genObj = generatorFunction();
genObj.next(); // { done: false, value: 1 }
genObj.next(); // { done: false, value: 2 }
genObj.next(); // { done: true }
下面这个例子演示了可递归调用的生成器,用到了操作符yield*
:
function* iterTree(tree) {
if (Array.isArray(tree)) {
for (let i = 0; i < tree.length; i++) {
yield* iterTree(tree[i]); // (*)
}
} else {
yield tree;
}
}
yield*
会交出(yield)全部迭代对象,而不仅仅是一个元素值。原话是「yield* in line (*) yields everything that is yielded by the iterable that is its operand. 」
yield*
还可以传递返回值。如:
let result1 = yield* step(); // step也是个generator
这个例子不太好,或者说,ES6的这部分实现有点繁琐,需要更多示例才能理解这个特性。