es6-参考手册
该手册包括ES2015[ES6]的知识点、技巧、建议和每天工作用的代码段例子。欢迎补充和建议。
var 和 let / const
除了var
,我们现在有了两种新的标示符用来存储值——let
和const
。与var
不同的是,let
和const
声明不会提前到作用域的开头。(译注:即不发生声明提前)
一个使用var
的例子:
var snack = 'Meow Mix';function getFood(food) { if (food) { var snack = 'Friskies'; return snack; } return snack;}getFood(false); // undefined
然而, 我们把var
替换成let
,观察会发生什么:
let snack = 'Meow Mix';function getFood(food) { if (food) { let snack = 'Friskies'; return snack; } return snack;}getFood(false); // 'Meow Mix'
这种行为变化提示我们,在使用var
重构遗留代码的时候需要小心,盲目的把let
替换成var
会导致以外的行为
注意:let
和const
具有块作用域。因此在它们定义前调用会引发ReferenceError
console.log(x); // ReferenceError: x is not definedlet x = 'hi';
建议:在遗留代码中的保留var
声明表示需要小心的重构。在新建代码库时,使用let
声明以后会只会发生改变的变量,用const
声明那些以后不会被改变的变量。
译注:const
修饰的基本类型不能改变,而其修饰的对象、函数、数组等引用类型,可以改变内部的值,但不能改变其引用。
用块(Blocks)替换立即执行函数(IIFEs)
立即执行函数常被用作闭包,把变量控制在作用域内。在ES6中,我们可以创建一个块作用域而且不仅仅是基于函数的作用域。
(function () { var food = 'Meow Mix';}());console.log(food); // Reference ErrorUsing ES6 Blocks:{ let food = 'Meow Mix';};console.log(food); // Reference Error
箭头函数(Arrow Functions)
通常我们会建立嵌套函数,当我们想保留this
上下文作用域的时候(该怎么办?)。如下就是一个例子:
function Person(name) { this.name = name;}Person.prototype.prefixName = function (arr) { return arr.map(function (character) { return this.name + character; // Cannot read property 'name' of undefined });};
一个常见的解决方法是利用一个变量存储这个this
的上下文。
function Person(name) { this.name = name;}Person.prototype.prefixName = function (arr) { var that = this; // Store the context of this return arr.map(function (character) { return that.name + character; });};
我们也可以传入这个this
的上下文:
function Person(name) { this.name = name;}Person.prototype.prefixName = function (arr) { return arr.map(function (character) { return this.name + character; }, this);};
还可以用bind
绑定这个上下文:
function Person(name) { this.name = name;}Person.prototype.prefixName = function (arr) { return arr.map(function (character) { return this.name + character; }.bind(this));};
使用箭头函数,this
的词法作用域不会被影响,我们可以像以下这样重写之前的代码
function Person(name) { this.name = name;}Person.prototype.prefixName = function (arr) { return arr.map(character => this.name + character);};
建议: 只要当你想保留this
的词法作用域时,就使用箭头函数
对于一个简单返回一个值的函数来说,箭头函数显得更加简洁
var squares = arr.map(function (x) { return x * x }); // Function Expressionconst arr = [1, 2, 3, 4, 5];const squares = arr.map(x => x * x); // Arrow Function for terser implementation
建议: 尽可能用箭头函数替换你的函数定义
字符串(Strings)
在ES6中, 标准库也在不断的扩充。在这些变化中就有很多方法可以用于字符串,比如.includes()
和.repeat()
.includes()
var string = 'food';var substring = 'foo';console.log(string.indexOf(substring) > -1);
检测返回值是否大于-1表示字符串是否存在,我们可以用.includes()
替换,它返回一个boolean值。
const string = 'food';const substring = 'foo';console.log(string.includes(substring)); // true
.repeat()
function repeat(string, count) { var strings = []; while(strings.length < count) { strings.push(string); } return strings.join('');}
在ES6中,我们一种更简洁的实现方法:
// String.repeat(numberOfRepetitions)'meow'.repeat(3); // 'meowmeowmeow'
模板字面量(Template Literals)
(译注:原文是Template Literals,而非Template Strings)
利用模板字面量,我们可以直接在字符串使用特殊字符而不用转义它们。
var text = "This string contains \"double quotes\" which are escaped.";let text = `This string contains "double quotes" which don't need to be escaped anymore.`;
模板字面量还支持插值,可以轻松完成连接字符串和值的任务:
var name = 'Tiger';var age = 13;console.log('My cat is named ' + name + ' and is ' + age + ' years old.');
const name = 'Tiger';const age = 13;console.log(`My cat is named ${name} and is ${age} years old.`);
在ES5中,我们这样操作多行字符串:
var text = ( 'cat\n' + 'dog\n' + 'nickelodeon');
或者
var text = [ 'cat', 'dog', 'nickelodeon'].join('\n');
模板字面量可以保留多行字符串,我们无需显式的放置它们:
let text = ( `catdognickelodeon`);
模板字面量可以接受表达式, 比如:
let today = new Date();let text = `The time and date is ${today.toLocaleString()}`;
解构赋值(Destructuring)
解构赋值允许我们从数组或对象中提取出值(甚至深度嵌套的值),并把他们存入变量的简单语法
解构数组(Destructuring Arrays)
var arr = [1, 2, 3, 4];var a = arr[0];var b = arr[1];var c = arr[2];var d = arr[3];let [a, b, c, d] = [1, 2, 3, 4];console.log(a); // 1console.log(b); // 2
解构对象(Destructuring Objects)
var luke = { occupation: 'jedi', father: 'anakin' };var occupation = luke.occupation; // 'jedi'var father = luke.father; // 'anakin'let luke = { occupation: 'jedi', father: 'anakin' };let {occupation, father} = luke;console.log(occupation); // 'jedi'console.log(father); // 'anakin'
模块(Modules)
ES6之前,我们只用如Browserify的库在客户端创建模块,并且需要用到Node.js。利用ES6,我们现在可以直接使用任何类型的模块(AMD和CommonJS)
CommonJS中的exports
module.exports = 1;module.exports = { foo: 'bar' };module.exports = ['foo', 'bar'];module.exports = function bar () {};
ES6中的export
在ES6中,提供各种不同类型的exports,我们可以运行如下:
export let name = 'David';export let age = 25;
输出对象列表:
function sumTwo(a, b) { return a + b;}function sumThree(a, b, c) { return a + b + c;}export { sumTwo, sumThree };
我们也可以简单地通过export
关键字输出函数、对象和值(等等):
export function sumTwo(a, b) { return a + b;}export function sumThree(a, b, c) { return a + b + c;}And lastly, we can export default bindings:function sumTwo(a, b) { return a + b;}function sumThree(a, b, c) { return a + b + c;}let api = { sumTwo, sumThree};
输出默认api:
/* Which is the same as * export { api as default }; */
建议:在模块结束的地方,始终输出默认的方法。这样可以清晰地看到接口,并且通过弄清楚输出值的名称节省时间。所以在CommonJS模块中通常输出一个对象或值。坚持使用这种模式,会使我们的代码易读,并且可以轻松的在ES6和CommonJS中进行插补。
ES6
ES6 提供提供各种不同的imports,我们输入一整个文件:
import 'underscore';
这里值得注意的是,简单的输入一文件会在文件的最顶层执行代码。和Python类似,我们已经命名了imports:
import { sumTwo, sumThree } from 'math/addition';
我们还可以重命名这些已经有名的imports:
import { sumTwo as addTwoNumbers, sumThree as sumThreeNumbers} from 'math/addition';
此外,我们可以输入各种东西(也叫做 namespace import)
import * as util from 'math/addition';
最后,我们可以从模块输入一列值:
import * as additionUtil from 'math/addition';const { sumTwo, sumThree } = additionUtil;
如下这样从默认绑定进行输入
import api from 'math/addition';// 例如: import { default as api } from 'math/addition';
虽然最好要保持输出简单,但是如果我们需要,我们有时可以混用默认输入,当我们想如下输出的时候:
// foos.jsexport { foo as default, foo1, foo2 };
我们可以如下输入它们:
import foo, { foo1, foo2 } from 'foos';
当使用commonj语法(如React)输入一个模型的输出时,我们可以这样做:
import React from 'react';const { Component, PropTypes } = React;
这个也可以进一步简化,使用:
import React, { Component, PropTypes } from 'react';
注意:输出的值是绑定,不是引用。因此,绑定的值发生变化会影响输出的模型中的值。避免修改这些输出值的公共接口。
参数(Parameters)
在ES5中,我们可以很多方法操作函数参数的默认值、未定义的参数和有定义的参数。在ES6中,我们可以用更简单的语法实现这一切。
默认参数(Default Parameters)
function addTwoNumbers(x, y) { x = x || 0; y = y || 0; return x + y;}
在ES6中,我们可以简单的把默认值赋给参数:
function addTwoNumbers(x=0, y=0) { return x + y;}addTwoNumbers(2, 4); // 6addTwoNumbers(2); // 2addTwoNumbers(); // 0
剩余参数(Rest Parameters)
在ES5中,我们这样操作一个未定义的参数:
function logArguments() { for (var i=0; i < arguments.length; i++) { console.log(arguments[i]); }}
使用休止符(...)我们可以传入大量未定义的参数:
function logArguments(...args) { for (let arg of args) { console.log(arg); }}
已命名的参数(Named Parameters)
ES5中,一种处理已命名参数的方式是使用选项方式,这种方法来自jQuery。
function initializeCanvas(options) { var height = options.height || 600; var width = options.width || 400; var lineStroke = options.lineStroke || 'black';}
通过解构成正式参数的方式,我们可以实现同样的功能:
function initializeCanvas({ height=600, width=400, lineStroke='black'}) { // Use variables height, width, lineStroke here}
如果我们想使全部参数值可选,我们可以用一个空对象这样结构:
function initializeCanvas( { height=600, width=400, lineStroke='black'} = {}) { // ... }
展开运算符(Spread Operator)
在ES5中,查找一个array中的最大值需要使用Math.max的apply方法:
Math.max.apply(null, [-1, 100, 9001, -32]); // 9001
在es6中,我们使用展开运算符将array传递给函数作为参数:
Math.max(...[-1, 100, 9001, -32]); // 9001
我们可以用这样简洁的语法链接数组字面量:
let cities = ['San Francisco', 'Los Angeles'];let places = ['Miami', ...cities, 'Chicago']; // ['Miami', 'San Francisco', 'Los Angeles', 'Chicago']
类(classes)
在ES6之前,我们通过创建构造器函数,并且在其prototype上添加属性的方法创建一个类:
function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender;}Person.prototype.incrementAge = function () { return this.age += 1;};
并且用一下方法创建继承类:
function Personal(name, age, gender, occupation, hobby) { Person.call(this, name, age, gender); this.occupation = occupation; this.hobby = hobby;}Personal.prototype = Object.create(Person.prototype);Personal.prototype.constructor = Personal;Personal.prototype.incrementAge = function () { Person.prototype.incrementAge.call(this); this.age += 20; console.log(this.age);};
ES6 提供了十分有用的句法在后台实现这一切,我们可以这样直接创建一个类:
class Person { constructor(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } incrementAge() { this.age += 1; }}
并且用关键字extends
实现继承:
class Personal extends Person { constructor(name, age, gender, occupation, hobby) { super(name, age, gender); this.occupation = occupation; this.hobby = hobby; } incrementAge() { super.incrementAge(); this.age += 20; console.log(this.age); }}
建议:使用ES6的语法创建类模糊了后台的实现和原型如何工作,这个好特性可以使我们的代码更整洁。
Symbols
Symbol在ES6之前就已经出现了, 但是现在我们有了一个公共的接口可以直接使用。Symbol是唯一且不可改变的值,被用作哈希中的键。
Symbol()
调用Symbol()
或者Symbol(description)
会创建一个不能在全局查找的独一无二的符号。一种使用symbol()
的情况是,利用自己的逻辑修补第三方的对象或命名空间,但不确定会不会在库更新时产生冲突。例如,如果你想添加一个方法refreshCompontent
到React.Component
,并且确信这个方法他们不会在以后的更新中添加。
const refreshComponent = Symbol();React.Component.prototype[refreshComponent] = () => { // do something}###Symbol.for(key)
Symbol.for(key)
依然会创建一个唯一且不能修改的Symbol,但是它可以在全局被查找。两次调用相同的Symbol.for(key)
会创建一样的Symbol实例。注意,他和Symbol(description)
不是相同的:
Symbol('foo') === Symbol('foo') // falseSymbol.for('foo') === Symbol('foo') // falseSymbol.for('foo') === Symbol.for('foo') // true
一个常见的symbol方法Symbol.for(key)
是可互操作的。(使用这个方法)这个可以通过使用自己的代码在包括已知接口的第三方的对象参数中查找symbol成员实现,例如:
function reader(obj) { const specialRead = Symbol.for('specialRead'); if (obj[specialRead]) { const reader = obj[specialRead](); // do something with reader } else { throw new TypeError('object cannot be read'); }}
在另一个库中:
const specialRead = Symbol.for('specialRead');class SomeReadableType { [specialRead]() { const reader = createSomeReaderFrom(this); return reader; }}
ES6中,一个值得注意的是关于Symbol的互操作性的例子是Symbol.iterator
,它存在于Arrays、Strings、Generators等等的所有可迭代类型中。当作为一个方法调用的时候,它会返回一个具有迭代器接口的对象。
Maps
Maps是JavaScript中十分有用的结构。在ES6之前, 我们通过对象创建哈希maps:
var map = new Object();map[key1] = 'value1';map[key2] = 'value2';
但是,这样不能保护我们对已有属性以外的重写:
> getOwnProperty({ hasOwnProperty: 'Hah, overwritten'}, 'Pwned');> TypeError: Property 'hasOwnProperty' is not a function
Map允许我们使用set、get和search(等等)访问属性值。
let map = new Map();> map.set('name', 'david');> map.get('name'); // david> map.has('name'); // true
最意想不到的是Map不再限制我们只使用字符串作为键,我们现在可以使用任何类型作为键而不会发生类型转换。
let map = new Map([ ['name', 'david'], [true, 'false'], [1, 'one'], [{}, 'object'], [function () {}, 'function']]);for (let key of map.keys()) { console.log(typeof key); // > string, boolean, number, object, function}
注意:当使用如map.get()
等方法测试相等的时候,诸如function和object这样的非原始值不能正常工作。因此,依然应该使用原始值(作为键),比如String、Boolean和Number。 我们也可以使用.entries()
方法作为迭代器遍历Map
for (let [key, value] of map.entries()) { console.log(key, value);}
WeakMaps
ES6之前,为了保存私有数据,我们采取了很多方式。其中一个方法就是命名转换:
class Person { constructor(age) { this._age = age; } _incrementAge() { this._age += 1; }}
但是命名转换会引起代码库混乱,并且不能保证总是被支持。为此,我们使用WeakMaps存储数据:
let _age = new WeakMap();class Person { constructor(age) { _age.set(this, age); } incrementAge() { let age = _age.get(this) + 1; _age.set(this, age); if (age > 50) { console.log('Midlife crisis'); } }}
使用WeakMap存储数据时的一个很有趣的事情是,这个key不会暴露出属性名,需要使用Reflect.ownKeys()
实现:
> const person = new Person(50);> person.incrementAge(); // 'Midlife crisis'> Reflect.ownKeys(person); // []
使用WeakMap更实际的例子是在不污染DOM自身的情况下存储与DOM元素相关的数据:
let map = new WeakMap();let el = document.getElementById('someElement');// 给元素存一个弱引用map.set(el, 'reference');// 获得元素的值let value = map.get(el); // 'reference'// 移除引用el.parentNode.removeChild(el);el = null;// 元素被回收后,map是空的
如上所示,当一个对象被GC回收后,WeakMap会自动移除以其为标识符的键值对。
注意:为了进一步说明这个例子的实用性。当一个与DOM对应的对象的具有引用时,考虑jQuery如何存储它。使用WeakMaps,jQuery可以在DOM元素被删除时自动释放与之关联的内存。总而言之,对任何库而言,WeakMaps对操作DOM元素是非常实用的。
Promises
Promise允许我们把水平的代码(回调函数的地狱):
func1(function (value1) { func2(value1, function (value2) { func3(value2, function (value3) { func4(value3, function (value4) { func5(value4, function (value5) { // Do something with value 5 }); }); }); });});
转换为竖直的代码:
func1(value1) .then(func2) .then(func3) .then(func4) .then(func5, value5 => { // Do something with value 5 });
在ES6之前,我们使用bluebird或是Q,现在我们有了Promises:
new Promise((resolve, reject) => reject(new Error('Failed to fulfill Promise'))) .catch(reason => console.log(reason));
这里我们有2个handlers,resolve(Promise执行成功时调用的函数)和reject(Promise失败时调用的函数)。
使用Promise的好处:使用嵌套的回调函数处理错误会很混乱。使用Promise,我们可以很清晰的使错误冒泡,并且就近处理它们。更好的是,在它处理成功(或失败)之后Promise的值是不可修改的。
以下是个使用Promise的实例:
var request = require('request');return new Promise((resolve, reject) => { request.get(url, (error, response, body) => { if (body) { resolve(JSON.parse(body)); } else { resolve({}); } });});
我们可以使用Promise.all()
并行的处理一个异步操作数组:
let urls = [ '/api/commits', '/api/issues/opened', '/api/issues/assigned', '/api/issues/completed', '/api/issues/comments', '/api/pullrequests'];let promises = urls.map((url) => { return new Promise((resolve, reject) => { $.ajax({ url: url }) .done((data) => { resolve(data); }); });});Promise.all(promises) .then((results) => { // Do something with results of all our promises });
Generators
和Promise使我们避免回调函数的地狱相似,Generators可以扁平化我们的代码——给我们一种同步执行异步代码的感觉,Generators是个很重要的函数,它使我们可以暂停操作的执行,随后返回表达式的值。
下面是使用Generator的一个简单例子:
function* sillyGenerator() { yield 1; yield 2; yield 3; yield 4;}var generator = sillyGenerator();> console.log(generator.next()); // { value: 1, done: false }> console.log(generator.next()); // { value: 2, done: false }> console.log(generator.next()); // { value: 3, done: false }> console.log(generator.next()); // { value: 4, done: false }
这里,next()
使我们generator继续推进,并且得到新的表达式的值(译注:每次推进到下一个yield值)。当然,上面的例子很牵强,我们可以利用Generator以同步的方式写异步代码:
// 利用Generator屏蔽异步过程function request(url) { getJSON(url, function(response) { generator.next(response); });}
下面我们写一个generator函数用来返回我们自己的数据:
function* getData() { var entry1 = yield request('http://some_api/item1'); var data1 = JSON.parse(entry1); var entry2 = yield request('http://some_api/item2'); var data2 = JSON.parse(entry2);}
利用yield的功能,我们确保entry1可以获得返回的数据,用于解析并存储到data1中。
当我们利用generator以同步的方式写异步的代码时,其中的错误不会简单清晰的传递。因此,我们利用Promise加强generator:function request(url) { return new Promise((resolve, reject) => { getJSON(url, resolve); });}
我们写了一个函数,利用next
用来按序地一步步遍历generator。该函数利用上述的请求方式并yeild一个Promise(对象)。
function iterateGenerator(gen) { var generator = gen(); (function iterate(val) { var ret = generator.next(); if(!ret.done) { ret.value.then(iterate); } })();}
通过Promise加强generator后,我们可以利用Promise.catch和Promise.reject这样清晰的方式传播错误。只用这个加强版的Generator和以前一样简单:
iterateGenerator(function* getData() { var entry1 = yield request('http://some_api/item1'); var data1 = JSON.parse(entry1); var entry2 = yield request('http://some_api/item2'); var data2 = JSON.parse(entry2);});
我们可以重用写好的代码,像过去使用Generator一样,这一点很强大。当我们利用generator以同步的方式写异步的代码的同时,利用一个不错的方式保留了错误传播的能力,我们实际上可以利用一个更为简单的方式达到同样的效果:异步等待(Async-Await)。
Async Await
这是一个在ES2016(ES7)中即将有的特性,async await允许我们更简单地使用Generator和Promise执行和已完成工作相同的任务:
var request = require('request');function getJSON(url) { return new Promise(function(resolve, reject) { request(url, function(error, response, body) { resolve(body); }); });}async function main() { var data = await getJSON(); console.log(data); // NOT undefined!}main();
在后台,它的实现类似Generators。我(作者)强烈建议使用这个替代Generators + Promises。还会有很多的资源出现并使用ES7,同时,Babel也会用在这里。
Getter 和 Setter 函数
ES6 已经支持了Getter
和Setter
函数,例如:
class Employee { constructor(name) { this._name = name; } get name() { if(this._name) { return 'Mr. ' + this._name.toUpperCase(); } else { return undefined; } } set name(newName) { if (newName == this._name) { console.log('I already have this name.'); } else if (newName) { this._name = newName; } else { return false; } }}var emp = new Employee("James Bond");// 内部使用了get方法if (emp.name) { console.log(emp.name); // Mr. JAMES BOND}// 内部使用了setter(译注:原文中这一句和上一句注释的表述就这么不一样)emp.name = "Bond 007";console.log(emp.name); // Mr. BOND 007
最新的浏览器都支持对象中的getter/setter函数,我们可以使用他们计算属性、添加事件以及在setting和getting前的预处理
var person = { firstName: 'James', lastName: 'Bond', get fullName() { console.log('Getting FullName'); return this.firstName + ' ' + this.lastName; }, set fullName (name) { console.log('Setting FullName'); var words = name.toString().split(' '); this.firstName = words[0] || ''; this.lastName = words[1] || ''; }}person.fullName; // James Bondperson.fullName = 'Bond 007';person.fullName; // Bond 007