ECMAScript 6新特性印象之一:新语法


前记

按照规划,明年年中,ECMAScript 6(ES6)就要正式发布了。

最近抽空看了Dr. Axel Rauschmayer的几篇文章和演讲PPT,对新特性有了些了解。

趁没忘,抓紧记录下,夹杂自己的感受。

计划分三部分:

  1. 新语法
  2. 面对对象和模块化
  3. 标准库扩充

参考了以下文章/PPT:

其他文章:

总体印象

的确是「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主要做了两件事:

  1. 简化了代码形式,默认return表达式结果。
  2. 自动绑定语义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)
// 但胖箭头在执行效率上会更高

胖箭头函数与正常函数的区别:

  1. 胖箭头在创建时即绑定this(lexical this);正常函数的this是在执行时动态传入的(dynamic this)。
  2. 胖箭头没有内部方法[[Construct]]和属性原型,所以new (() => {})是会报错的。
  3. 胖箭头没有arguments变量。

这样,以后在定义方法/函数时,就有了清晰的选择:

  1. 定义子程序(subroutine),用胖箭头,自动获得语义this。
  2. 定义方法(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方法,调用会返回:

  1. 返回迭代对象的一个元素:{ done: false, value: elem }
  2. 如果已到迭代对象的末端:{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是一个SymbolSymbol是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的这部分实现有点繁琐,需要更多示例才能理解这个特性。


发表评论

电子邮件地址不会被公开。

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>