14.2 ES6 新特性介绍

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));
      },
      ...
    }

下面有几个箭头函数的实际使用例子,可以在wow平台上运行看看结果:

// 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]

或者直接点这儿:ES6基础知识学习之箭头函数示例,注意:以上的例子代码不需要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的变量赋予数组中相应元素项的值。如果你想在赋值的同时声明变量,可在赋值语句前加入varletconst关键字,例如:


    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

请注意,当你解构对象并赋值给变量时,如果你已经声明或不打算声明这些变量(亦即赋值语句前没有letconstvar关键字),你应该注意这样一个潜在的语法错误:

    { blowUp } = { blowUp: 10 };
    // Syntax error 语法错误

为什么会出错?这是因为JavaScript语法通知解析引擎将任何以{开始的语句解析为一个块语句(例如,{console}是一个合法块语句)。解决方案是将整个表达式用一对小括号包裹:

    ({ safe } = {});
    // No errors 没有语法错误

解构值不是对象、数组或迭代器

当你尝试解构nullundefined时,你会得到一个类型错误:

    var {blowUp} = null;
    // TypeError: null has no properties(null没有属性)

然而,你可以解构其它原始类型,例如:布尔值数值字符串,但是你将得到undefined

    var {wtf} = NaN;
    console.log(wtf);
    // undefined

原因是,当使用对象赋值模式时,被解构的值需要被强制转换为对象。大多数类型都可以被转换为对象,但nullundefined却无法进行转换。当使用数组赋值模式时,被解构的值一定要包含一个迭代器

默认值

当你要解构的属性未定义时你可以提供一个默认值:

    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中有关解构的支持正在开发中,其它浏览器也将适时增加支持。所以你可能需要使用BabelTraceur将ES6代码转译为相应的ES5代码。

如前所述,当你使用踏得网开发时,在JS面板设置中已经内置支持了Babel。

缺省值(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() 不同的是,它支持 breakcontinue 和 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 的子类型,内置实现了 nextthrow 接口:

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("𠮷".length);// == 2

// new RegExp behaviour, opt-in ‘u’
console.log("𠮷".match(/./u)[0].length)// == 2

// new form
console.log("\u{20BB7}");
"\u{20BB7}" == "𠮷" == "\uD842\uDFB7"

// new String ops
"𠮷".codePointAt(0) == 0x20BB7

// for-of iterates code points
for(var c of "𠮷i") {
  console.log(c);
}

模块(Modules)

Modules是ES6中的一个重要特性,用来支持语言级别的组件化定义,是从流行的JS模块加载器(AMD, CommonJS)中整理而来。运行时行为由宿主定义(host-defined)的默认加载程序所定义。隐式异步模型–只有当被实际请求并且可用时才执行代码。

modules规范分两部分,一部分是如何导出(export 关键字),一部分是如何导入(import关键字),用法如下所示:

// lib/math.js
export function sum(x, y) {
  return x + y;
}
export var pi = 3.141593;
// app.js
import * as math from "lib/math";
console.log("2π = " + math.sum(math.pi, math.pi));
// otherApp.js
import {sum, pi} from "lib/math";
console.log("2π = " + sum(pi, pi));

上面的{sum, pi}看起来像前面提到的展开(spread)语法,但其实不是,这只是import的特有语法而已。

模块还支持默认导出export default和通配符导出export *:

// lib/mathplusplus.js
export * from "lib/math";
export var e = 2.71828182846;
export default function(x) {
    return Math.exp(x);
}
// app.js
import exp, {pi, e} from "lib/mathplusplus";
console.log("e^π = " + exp(pi));

集合(Map + Set + WeakMap + WeakSet)

ES6 新增了几种集合类型:Map,Set,WeakMap和WeakSet。这些是通用算法的高效数据结构。

Set 是 ES6 新增的有序列表集合,它不会包含重复项。数组(Array)可以存放任何类型的数据,不过数据除重需要自己实现。 Set 支持 add(item) 方法,用来向 Set 添加任意类型的元素,如果已经添加过则自动忽略;has(item) 方法用来检测 Set 中是否存在指定元素;delete(item) 方法用来从 Set 中删除指定元素;clear() 用来清空 Set;获取 Set 集合长度用 size 属性。如下:

var set = new Set();
set.add(window);
set.has(window); // true
set.size; // 1
set.add(window);
set.add(1);
set.size; // 2
set.delete(window);
set.has(window); // false
set.clear();
set.size; // 0

Map 是 ES6 新增的有序键值对集合。键值对的 key 和 value 都可以是任何类型的元素。通过 set(key, value) 方法为 Map 设置新的键值对,如果设置的 key 已经存在则用新的 value 覆盖,Map 在比较 key 时也不做类型转换,跟 Set 类似;Map 的 get(key) 方法用来获取指定 key 的值;Map 的 has(key) 、 delete(key) 、clear() 这些方法和 size 属性,与 Set 类似,直接看代码:

var map = new Map();
var key1 = {toString : function() { return 2}};
var key2 = 2;
map.set(key1, 1);
map.set(key2, 2);

map.has(key1); // true
map.has('2'); // false,类型不同
map.delete(2);
map.size; // 1
map.get(key2); // undefined

我们没办法像数组一样用 for 循环来迭代 Set,也没办法像对象一样用 for...in 来迭代 Map。但是可以用 ES6 提供的新方法 for...of 来遍历它们。

var set = new Set();
set.add('this is a demo.');
set.add(window);
set.add(top);

for(let item of set) {
    console.log(item);
}

WeakMap 相对于普通的 Map,也是键值对集合,只不过 WeakMap 的 key 只能是非空对象(non-null object)。WeakMap 对它的 key 仅保持弱引用,也就是说它不阻止垃圾回收器回收它所引用的 key。 WeakMap 最大的好处是可以避免内存泄漏。一个仅被 WeakMap 作为 key 而引用的对象,会被垃圾回收器回收掉。 WeakMap 拥有和 Map 类似的 set(key, value) 、get(key)、has(key)、delete(key) 和 clear() 方法,但没有 size 属性,也没有任何与迭代有关的方法。

// Sets
var s = new Set();
s.add("hello").add("goodbye").add("hello");
s.size === 2;
s.has("hello") === true;

// Maps
var m = new Map();
m.set("hello", 42);
m.set(s, 34);
m.get(s) == 34;

// Weak Maps
var wm = new WeakMap();
wm.set(s, { extra: 42 });
wm.size === undefined

// Weak Sets
var ws = new WeakSet();
ws.add({ data: 42 });
// Because the added object has no other references, it will not be held in the set

Support via polyfill

要跨浏览器支持Maps, Sets, WeakMaps, 和 WeakSets,你必须包含Babel polyfill。

代理(Proxies)

代理(Proxies)使得新建对象拥有宿主对象的可用全部行为。可用于截取、对象虚拟化、日志记录/分析等。

// Proxying a normal object
var target = {};
var handler = {
  get: function (receiver, name) {
    return `Hello, ${name}!`;
  }
};

var p = new Proxy(target, handler);
p.world === "Hello, world!";
// Proxying a function object
var target = function () { return "I am the target"; };
var handler = {
  apply: function (receiver, ...args) {
    return "I am the proxy";
  }
};

var p = new Proxy(target, handler);
p() === "I am the proxy";

标识符(Symbols)

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。

ES5中基本数据类型有6种:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

这里新添加了一种:Symbol

注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。

为什么要Symbol

Symbol的目的就是为了实现一个唯一不可变的值,任何一个Symbol都是唯一的,不会和其他任何Symbol相等。

很多时候其实我们都需要用到唯一的值。比如我们给一个DOM节点做动画,那么我们需要判断动画是否正在执行。

这个时候一般的做法是给DOM节点加上一个属性(或者classname之类的)做一个标记,比如下面这样:

d.isMoving = true;  //正在执行动画

但是这样做很容易产生命名冲突,万一你用了一个第三方方动画库,人家也是用 isMoving 来进行标记的怎么办。

解决方法是让这个名字变得很特殊:

d.my_is_moving_flag_prevent_conflict = true;

这样虽然基本能防止冲突,但是可读性/可维护性很差。

这个时候我们就需要一个唯一的值来做标记:

var isMoving = Symbol("is moving");
d[isMoving] = true;

因为每次创建的 Symbol 都是唯一的,所以别人即使再次创建一个Symbol 也不会和你的Symbol冲突。

基本用法

Symbol 有两种用法,一种是创建局部的Symbol:

var s = Symbol("foo");

另一种是创建全局的 Symbol:

var s = Symbol.for("foo");

当然我们一般用的都是局部的。

一定要区分 Symbol 和 String,Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台调试显示,但并非唯一标识的一部分。

虽然可以不用传,但是为了调试方便以及代码的可读性,建议还是带上这个参数.

直接使用 Symbol("xx"),每一次都会创建一个不同的 Symbol,不管传入的描述符是否一样:

var a=Symbol("a"), b=Symbol("a");
a === b;    //false

但是如果用 Symbol.for 创建全局Symbol,那么当描述符和以前的一个全局Symbol相同的时候,会返回之前的Symbol,所以他们是相等的,如果没有则创建一个新的:

var a = Symbol.for("a"), b = Symbol.for("a");
a === b;    //true

Symbol.iterator

iterator 是一种设计模式,很多语言通过iterator 实现遍历功能,如果一个对象支持遍历,那么它只需要实现 iterator 接口即可。

在JS中其实也是,只是在Symbol出现之前这是一个隐藏的特性,我们只能想下面这样通过 for 来实现数组遍历:

var a = [1,2,3];
for(var i=0;i<a.length;i++) { //xxxx}

其实for语句就是通过隐藏的 iterator 来实现遍历的,现在我们可以通过 Symbol.iterator 来取到这个 iterator 对象,并且自己调用 iterator.next() 来实现遍历:

var a = [1,2,3];
var i = a[Symbol.iterator]();
i.next(); //1
i.next();   //2
i.next();   //3

并且 Symbol.iterator 是一个全局唯一的值,可以保证不会和 a 上面的任何属性方法有冲突。

结合闭包和Symbol实现私有属性

Symbol可以帮助实现对象状态访问控制,比如私有属性,不过由于 Object.getOwnPropertySymbols() 接口的出现,导致即使是用 Symbol 作为key也可以被遍历出来,因此不能做到私有。

不过,排除这一点,我们可以基本上可以认为使用 Symbol 的属性是私有属性。

var People = (function() {
  var name = Symbol("name");
  class People {
    constructor(n) { //构造函数
    this[name] = n;
    }
    sayName() {
        console.log(this[name]);
    }
  }
  return People;
})();

参见上面的例子,我们使用闭包主要是保护这个 Symbol ,它无法直接在闭包外面访问。这样除了使用 Object.getOwnPropertySymbols() 之外我们无法直接访问 this[name] 属性。

至少这种做法比使用 this._name 要好很多,因为Symbol不会污染keys, for ... in之类的用法不会受到影响。

原生类型的子类化(Subclassable Built-ins)

在ES2015中, 内置类型如数组Array, 日期Date 和DOM元素 Element 可以被子类化。

// User code of Array subclass
class MyArray extends Array {
    constructor(...args) { super(...args); }
}

var arr = new MyArray();
arr[1] = 12;
arr.length == 2

Math + Number + String + Object APIs

扩展了一些开发库,包括核心数学库、数组转换,和对象复制。

Number.EPSILON
Number.isInteger(Infinity) // false
Number.isNaN("NaN") // false

Math.acosh(3) // 1.762747174039086
Math.hypot(3, 4) // 5
Math.imul(Math.pow(2, 32) - 1, Math.pow(2, 32) - 2) // 2

"abcde".includes("cd") // true
"abc".repeat(3) // "abcabcabc"

Array.from(document.querySelectorAll("*")) // Returns a real Array
Array.of(1, 2, 3) // Similar to new Array(...), but without special one-arg behavior
[0, 0, 0].fill(7, 1) // [0,7,7]
[1,2,3].findIndex(x => x == 2) // 1
["a", "b", "c"].entries() // iterator [0, "a"], [1,"b"], [2,"c"]
["a", "b", "c"].keys() // iterator 0, 1, 2
["a", "b", "c"].values() // iterator "a", "b", "c"

Object.assign(Point, { origin: new Point(0,0) })

Limited support from polyfill

Most of these APIs are supported by the Babel polyfill. However, certain features are omitted for various reasons (e.g. String.prototype.normalize needs a lot of additional code to support). You can find more polyfills here.

承诺(Promises)

承诺(Promises)是一个异步编程的实现模式。Promise对象用来表示一个未来的数值,好比一个承诺的结果是在未来(即异步):完成或者失败,但承诺本身是立即给到的(形式上的同步)。所以承诺的语义就是如果...就(then),比如说如果我成为总统,我就(then)换联储主席,如果换了联储主席,我就(then)加息。

Promise对象正是使用then方法来添加回调函数。then方法可以接受两个回调函数,第一个是异步操作成功时(变为resolved状态)时的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(可以省略)。一旦状态改变,就调用相应的回调函数。 异步任务返回一个Promise对象,我们可以把原先多重嵌套的回调函数编写方式改成简洁的链式调用,如下所示:

// ES5, 多重回调函数嵌套
 
function printAfterTimeout(string, timeout, done){
  setTimeout(function(){
    done(string);
  }, timeout);
}
printAfterTimeout('Hello ', 2e3, function(result){
  console.log(result);
  // nested callback
  printAfterTimeout(result + 'Reader', 2e3, function(result){
    console.log(result);
  });
});

// ES6, 链式调用

function printAfterTimeout(string, timeout){
  return new Promise((resolve, reject) => {
    setTimeout(function(){
      resolve(string);
    }, timeout);
  });
}
printAfterTimeout('Hello ', 2e3).then((result) => {
  console.log(result);
  return printAfterTimeout(result + 'Reader', 2e3);
}).then((result) => {
  console.log(result);
});

反射接口(Reflect API)

反射接口用来暴露对象运行时的元操作(meta-operations)。这相当于是反过来的代理接口(Proxy API),并允许调用和代理路径相同的元操作。这对于实现代理非常有用。

var O = {a: 1};
Object.defineProperty(O, 'b', {value: 2});
O[Symbol('c')] = 3;

Reflect.ownKeys(O); // ['a', 'b', Symbol(c)]

function C(a, b){
  this.c = a + b;
}
var instance = Reflect.construct(C, [20, 22]);
instance.c; // 42

你可以在线试试