概述

ES6(ECMAScript6)是新版本 JavaScript 语言的标准,代号 _harmony_。上一次标准的制定还是 2009 年出台的 ES5。

ECMAScript 和 JavaScript 的关系:前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 Jscript 和 ActionScript)。日常场合,这两个词是可以互换的

ES6 和 ECMAScript2015 的关系:ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”

箭头操作符

如果你会 C# 或者 Java,你肯定知道 lambda 表示,ES6 中新增的 箭头操作符(=>) 就有异曲同工之妙。它简化了函数的书写。操作符左边为输入的参数,而右边则是进行操作及返回的值 Inputs => Outputs。我们知道在 JS 中回调是经常的事,而一般回调又以匿名函数的形式出现,每次都需要写一个 function,甚是繁琐,当引入箭头操作符后就可以方面的进行回调。

箭头函数的 this 值:普通 function 函数和箭头函数的行为有一个微妙的区别,箭头函数没有它自己的 this 值,箭头函数内的 this 值继承自外围作用域。

箭头集合:

<!– 单行注释
–> “趋向于”操作符
<= 小于等于
=> 箭头函数

如下面的例子所示。

var array = [1, 2, 3];
// 传统写法
array.forEach(function(v, i, a) {
  console.log(v);
});
// ES6
array.forEach(v = > console.log(v));
类的支持

ES6 中添加了对类的支持,引入了 class 关键字(其实 class 在 JavaScript 中一直是保留字,目的就是考虑到可能在以后的新版本中会用到)。JS 本身就是面向对象的,ES6 中提供的类实际上只是 JS 原型模式的包装。现在提供原生的 class 支持后,对象的创建、继承更加直观,而且父类方法的调用、实例化、静态方法和构造函数等概念都更加形象化。

下面代码展示了类在 ES6 中的使用。

// 类的定义
class Animal {
  // ES6 中新型构造器
  constructor(name) {
    this.name = name;
  }
  // 实例方法
  sayName() {
    console.log('My name is '+this.name);
  }
}
// 类的继承
class Programmer extends Animal {
  constructor(name) {
    // 直接调用父类构造器进行初始化
    super(name);
  }
  program() {
    console.log("I'm coding...");
  }
}
// 测试我们的类
var animal = new Animal('dummy'),
wayou = new Programmer('wayou');
// 输出 ‘My name is dummy’
animal.sayName();
// 输出 ‘My name is wayou’
wayou.sayName();
// 输出 ‘I'm coding...’
wayou.program();
增强的对象字面量

对象字面量被增加了,写法更加简洁与灵活,同时在定义对象的时候能够做的事情更多了。具体体现在:

  1. 可以在对象字面量里定义原型
  2. 定义方法可以不用 function 关键字
  3. 直接调用父类方法

这样一来,对象字面量与前面提到的类概念更加吻合,在编写面向对象的 JavaScript 时更加轻松方便

// 通过对象字面量创建对象
var human = {
  breathe() {
    console.log('breathing...');
  }
};
var worker = {
  __proto__: human, // 设置此对象的原型为 human, 相当于继承human
  company: 'freelancer',
  work() {
    console.log('working...');
  }
};
// 输出 ‘breathing...’
human.breathe();
// 调用继承来的 breathe 方法
// 输出 ‘breathing...’
worker.breathe();
字符串模版

字符串模版相对简单易懂一些,ES6 中允许使用反引号’来创建字符串,此种方法创建的字符串里面可以包含由美元符号加花括号包裹的变量 ${varible} 。如果你使用过像 C# 等后端强类型语言的话,对此功能应该不会陌生,如下面的例子所示。

// 产生一个随机数
var num = Math.random();
// 将这个数字输出到 console
console.log(`your num is ${num}`);
解构

自动解析数组或对象中的值,比如若一个函数要返回多个值,常规的做法是返回一个对象,将每个值作为这个对象的属性返回。但在 ES6 中,利用解构这一特性,可以直接返回一个数组,然后数组中的值会自动被解析到对应接收该值的变量中,如下面的例子所示。

// 函数返回值的解构
var [x,y]=getVal(),
// 数组解构
    [name,,age]=['wayou', 'male', 'secrect'];

function getVal() {
  return [ 1, 2 ];
}

// 输出:x:1, y:2
console.log('x:' + x + ', y:' + y);
// 输出:name: wayou, age: secrect
console.log('name: ' + name + ', age: ' + age);
参数默认值、不定参数、拓展参数

参数默认值

现在可以在定义函数的时候指定参数的默认值了,而不用像以前那样通过逻辑或操作符来达到目的了,如下面的例子所示。

function sayHello(name) {
  // 传统的指定默认参数的方式
  var name = name || 'dude';
  console.log('Hello ' + name);
}
// 运用 ES6 的默认参数
function sayHello2(name = 'dude') {
  console.log(`Hello ${name}`);
}
// 输出:Hello dude
sayHello();
// 输出:Hello Wayou
sayHello('Wayou');
// 输出:Hello dude
sayHello2();
// 输出:Hello Wayou
sayHello2('Wayou');

不定参数

不定参数是在函数中使用命名参数同时接收 _不定数量的未命名参数_。这只是一种 _语法糖_,在以前的 JavaScript 代码中我们可以通过 arguments 变量来达到这一目的。_不定参数的格式是三个句点后跟代表所有不定参数的变量名,如下面的这个例子所示,…x_ 代表了所有传入 add 函数的参数。

// 将所有参数相加的函数
function add(...x){
  return x.reduce((m, n) => m + n);
}
// 传递任意个数的参数
// 输出:6
console.log(add(1, 2, 3));
// 输出:15
console.log(add(1, 2, 3, 4, 5));

拓展参数

拓展参数则是另一种形式的 _语法糖_,它允许传递数组或者类数组直接作为函数的参数而不用通过 apply,如下面这个例子所示。

var people = ['Wayou', 'John', 'Sherlock'];
// sayHello 函数本来接收三个单独的参数人妖,人二和人三
function sayHello(people1, people2, people3) {
  console.log(`Hello ${people1}, ${people2}, ${people3}`);
}
// 但是我们将一个数组以拓展参数的形式传递,它能很好地映射到每个单独的参数
// 输出:Hello Wayou,John,Sherlock
sayHello(...people);

// 而在以前,如果需要传递数组当参数,我们需要使用函数的 apply 方法
// 输出:Hello Wayou,John, Sherlock
sayHello.apply(null, people);
let 与 const 关键字

可以把 let 看成 _var_,只是它定义的变量被限定在了 特定范围内才能使用 ,而离开这个范围则无效。const 则很直观,用来定义 _常量_,即无法被更改值的变量,如下面的例子所示。

// 输出: 0,1
for (let i = 0; i < 2; i++) console.log(i);
// 输出:undefined, 严格模式下会报错
console.log(i);

let 具有的一些特性:

  1. _let声明的变量拥有块级作用域_,也就是说 let 声明的变量的作用域只是外层块,而不是整个外层函数,let 声明仍 保留了提升的特性,但不会盲目提升
  2. _let 声明的全局变量不是全局对象的属性_,也就是说,你不能通过“window.变量名”的方式访问这些变量
  3. _形如 for(let x …) 的循环在每次迭代时都会为 x 创建新的绑定_,形同“立即执行函数表达式”(IIFE)
  4. let 声明的变量直到控制流到达该变量被定义的代码行时才会被装载,所以在到达之前使用该变量会触发错误
  5. 用 let 重定义变量会抛出一个语法错误
for…of 值遍历

我们都知道 for…in 循环用于遍历数组,类数组或对象,ES6 中新引入的 for…of 循环功能相似,不同的是每次循环它提供的不是序号而是值,它具有以下的优点。

  1. 这是最简洁、最直接的遍历数组元素的语法
  2. 这个方法避开了 for…in 循环的所有缺陷
  3. 与 forEach() 不同的是,它可以正确响应 break、continue 和 return 语句
  4. for…of 循环也可以遍历其他的集合,不仅支持数组,还支持大多数类数组对象,例如 DOM NodeList 对象

如下面的例子所示。

var someArray = [ "a", "b", "c" ];

for (v of someArray) {
  // 输出 a,b,c
  console.log(v);
}
iterator、generator

这一部分的内容有点生涩,详情可以参见 传送门 1传送门 2,以下是一些基本概念。

  1. iterator:是一个对象,拥有一个 next 方法,这个方法返回一个对象 {done, value},这个对象包含两个属性,一个布尔类型的 done 和包含任意值的 value
  2. iterable:是一个对象,拥有一个 obj[@@iterator]方法,这个方法返回一个 iterator
  3. generator:是一个特殊的 iterator,它的 next 方法可以接收一个参数并且返回值取决于它的构造函数(generator function)。generator 同时拥有一个 throw 方法
  4. generator 函数:即 generator 的构造函数,此函数内可以使用 yield 关键字,在 yield 出现的地方可以通过 generator 的 nextthrow 方法向外界传递值,generator 函数是通过 function * 来声明的
  5. yield 关键字:它可以暂停函数的执行,随后可以再次进入函数继续执。普通的函数只能 return 一次,而生成器的函数可以 yield 多次(当然也可以只 yield 一次)
模块

在 ES6 标准中,JavaScript 原生支持 module。这种将 JS 代码分割成不同功能的小块进行模块化的概念是在一些三方规范中流行起来的,比如 CommonJS 和 AMD 模式。将不同功能的代码分别写在不同文件中,各模块只需导出公共接口部分,然后通过模块导入的方式可以在其他地方使用,如下面的例子所示。

// point.js
module "point" {
  export class Point {
    constructor (x, y) {
      public x = x;
      public y = y;
    }
  }
}

// myapp.js
// 声明引用的模块
module point from "/point.js";
// 这里可以看出,尽管声明了引用的模块,还是可以通过指定需要的部分进行导入
import Point from "point";

var origin = new Point(0, 0);
console.log(origin);
Map、Set 和 WeakMap、WeakSet

这些是新加的集合类型,提供了更加方便的获取属性值的方法,不用像以前一样用 hasOwnProperty 来检查某个属性是属于原型链上还是当前对象的。同时,在进行属性值添加与获取时有专门的 getset 方法。

Set 类似于数组,但是成员的值都是唯一的,没有重复的值。

Map 解构提供了“值-值”的对应,是一种更完善的 Hash 解构实现。

如下面的例子所示。

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

有时候我们会把对象作为一个对象的键用来存放属性值,普通集合类型比如简单对象会阻止垃圾回收器对这些作为属性键存放的对象的回收,有造成 内存泄漏 的危险,而 WeakMapWeakSet 则更加安全些,这些作为属性键的对象如果没有别的变量在引用它们,则会被回收释放掉,如下面的例子所示。

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

// Weak Sets
var ws = new WeakSet();
// 因为添加到 ws 的这个临时对象没有其他变量引用它,所以 ws 不会保存它的值,也就是说这次添加其实没有意思
ws.add({ data: 42 });
Proxies

Proxy 可以监听对象身上发生了什么事情,并在这些事情发生后执行一些相应的操作。一下子让我们对一个对象有了很强的追踪能力,同时在数据绑定方面也有很用处,如下面的例子所示。

// 定义被侦听的目标对象
var engineer = { name: 'Joe Sixpack', salary: 50 };
// 定义处理程序
var interceptor = {
  set: function (receiver, property, value) {
    console.log(property, 'is changed to', value);
    receiver[property] = value;
  }
};
// 创建代理以进行侦听
engineer = Proxy(engineer, interceptor);
// 做一些改动来触发代理
// 控制台输出:salary is changed to 60
engineer.salary = 60;

这里进行进一步的解释,对于处理程序,是在被侦听的对象身上发生了相应事件之后,处理程序里面的方法就会被调用,上面的例子中我们设置了 set 的处理函数,表明如果我们侦听的对象的属性被更改,也就是被 set 了,那这个处理程序就会被调用,同时通过参数能够得知是哪个属性被更改,更改为了什么值。

Symbols

我们知道对象其实是键值对的集合,而键值通常来说是字符串。而现在除了字符串外,我们还可以用 Symbol 这种值来作为对象的键。Symbol 是一种基本类型,像数字、字符串还有布尔一样,它不是一个对象。_Symbol 通过调用 Symbol 函数产生_,它接收一个可选的名字参数,_该函数返回的 Symbol 是唯一的_。之后就可以用这个返回值作为对象的键了。Symbol 还可以用来创建私有属性,外部无法直接由 Symbol 作为键的属性值,如下面的例子所示。

(function() {
  // 创建 symbol
  var key = Symbol("key");

  function MyClass(privateData) {
    this[key] = privateData;
  }

  MyClass.prototype = {
    doStuff: function() {
      ... this[key] ...
    }
  };

})();

var c = new MyClass("hello")
// 无法访问该属性,因为是私有的
c["key"] === undefined

获取 Symbol 的三种方法:

  1. 调用 Symbol(),这种方法每次调用都会返回一个新的唯一 Symbol
  2. 调用 Symbol.for(string)。这种方式会访问 Symbol 注册表,其中存储了已经存在的一系列 Symbol。这种方式与通过 Symbol() 定义的独立 Symbol 不同,_Symbol 注册表中的 Symbol 是共享的_,如果你连续调用 Symbol.for(“cat”),每次都会 返回相同的 Symbol ,注册表非常有用,在多个 web 页面或者同一个 web 页面的多个模块中经常需要共享一个 Symbol

  3. 使用标准定义的 Symbol,例如:Symbol.iterator,标准根据一些特殊用途定义了少许的几个 Symbol

Math、Number、String、Object 的新 API

对 Math、Number、String 还有 Object 等添加了许多新的 API,例如下面的例子所示。

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".contains("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) })
Promises

Promises 是处理异步操作的一种模式,之前在很多三方库中有实现,比如 jQuery 的 deferred 对象。当你发起一个异步请求,并绑定了 .when().done() 等事件处理程序时,其实就是在应用 promise 模式。

// 创建 promise
var promise = new Promise(function(resolve, reject) {
  // 进行一些异步或耗时操作
  if ( /*如果成功 */ ) {
    resolve("Stuff worked!");
  } else {
    reject(Error("It broke"));
  }
});
// 绑定处理程序
promise.then(function(result) {
  // promise 成功的话会执行这里
  console.log(result); // "Stuff worked!"
}, function(err) {
  // promise 失败会执行这里
  console.log(err); // Error: "It broke"
});

总结:前后端差异越来越小了