ES6手抄

这份手抄包含ES2015 [ES6]的使用提示, 技巧, 最佳实践 和以及一些常用代码片段. 欢迎贡献!本文尽量保留原作者的意思,标记为"ps:"都是个人的理解,当然也有一些翻译不到位的地方,望请见谅!

var 对比 let / const

除var之外, 我们现在可以通过两个新的标识符(letconst)来存储变量. 而它们与 var不同的是, letconst 语句并不会提升到作用域的前端,这是因为ES6之前不存在"块级作用域"!

一个使用var的例子:

  1. var snack = 'Meow Mix';
  2. function getFood(food) {
  3. // snack变量被提升到这里
  4. // var snack = undefined
  5. if (food) {
  6. var snack = 'Friskies';
  7. // snack = 'Friskies';
  8. return snack;
  9. }
  10. return snack;
  11. }
  12. getFood(false); // undefined

然而, 当我们使用let替换var,程序到底发生了什么:

  1. let snack = 'Meow Mix';
  2. function getFood(food) {
  3. if (food) {
  4. // 这里不会被提升到作用域的前端
  5. let snack = 'Friskies';
  6. return snack;
  7. }
  8. // 当food为false时,它只能从上一级作用域查找变量snack,这个过程一直延伸到"全局作用域"
  9. return snack;
  10. }
  11. getFood(false); // 'Meow Mix'

当重构遗留代码的时候,特别要小心那些使用var的代码!盲目地使用let代替var将会产生意想不到的行为!

注意: letconst都是块级作用域的. 因此,块级作用域的标识符在定义之前就被引用,将会产生ReferenceError(引用错误)

  1. console.log(x);
  2. let x = "hi"; // 引用错误: x还没有定义

最佳实践: 在遗留代码中保留var声明的语句,以表示这段代码重构需要慎重考虑!当我们在一个新的代码库中工作时,使用let声明的变量,将会随着时间的推移发生改变,以及const声明的变量,通常并不会改变!(ps: 实质上,使用const声明的变量只有"只读"状态)

使用语句块替换IIFE

自调用函数表达式的常见用途是将变量封装在其作用域!ES6提供了一种创建块级作用域的能力,因此,我们不在单纯局限于函数作用域!

  1. (function () {
  2. var food = 'Meow Mix';
  3. }());
  4. console.log(food); // 引用错误

使用ES6块:

  1. {
  2. let food = 'Meow Mix';
  3. }
  4. console.log(food); // 引用错误

箭头函数

很多时候,嵌套函数的目的是想"维护"当前词法作用域中this。看下面的例子:

  1. function Person(name) {
  2. this.name = name;
  3. }
  4. Person.prototype.prefixName = function (arr) {
  5. return arr.map(function (character) {
  6. // ps: this被绑定到全局对象,严格模式this被绑定到undefined
  7. return this.name + character; // 不能从undefined读取到name属性
  8. });
  9. };

一个比较普遍的解决方案是通过一个变量(ps:通常是that)来存储当前上下文的this

  1. function Person(name) {
  2. this.name = name;
  3. }
  4. Person.prototype.prefixName = function (arr) {
  5. var that = this; // 存储当前上下文的this
  6. return arr.map(function (character) {
  7. return that.name + character;
  8. });
  9. };

我们可以为数组的map方法传入一个适当的上下文this作为map方法的参数:

  1. function Person(name) {
  2. this.name = name;
  3. }
  4. Person.prototype.prefixName = function (arr) {
  5. return arr.map(function (character) {
  6. return this.name + character;
  7. }, /* 上下文 */this);
  8. }

甚至我们可以使用bind来为函数绑定适当的上下文:

  1. function Person(name) {
  2. this.name = name;
  3. }
  4. Person.prototype.prefixName = function (arr) {
  5. return arr.map(function (character) {
  6. return this.name + character;
  7. }.bind(/* 上下文 */this));
  8. }

使用箭头函数,词法作用域中this并没有变得很神秘,重写上面的代码(ps: this已经进行了正确的绑定):

  1. function Person(name) {
  2. this.name = name;
  3. }
  4. Person.prototype.prefixName = function (arr) {
  5. return arr.map(character => this.name + character);
  6. }

最佳实践: 当你想维护词法作用域中this,最好使用箭头函数.

当你使用函数表达式只是简单地返回一个值,箭头函数也显得更为简洁!

  1. var squares = arr.map(function (x) { return x * x }); // 函数表达式
  1. const arr = [1, 2, 3, 4, 5];
  2. const squares = arr.map(x => x * x); // Arrow Function for terser implementation

最佳实践: 尽可能使用箭头函数替代函数表达式.

字符串

随着ES6标准库功能的日臻完善,我们可以在字符串中使用新方法,诸如.includes().repeat()

.includes( )

  1. var string = 'food';
  2. var substring = 'foo';
  3. console.log(string.indexOf(substring) > -1);

当字符串之间是包含关系,将会返回一个> -1的值,我们可以简单地使用 .includes()取代这种操作,并且.includes()将会返回一个布尔(boolean)值:

  1. const string = 'food';
  2. const substring = 'foo';
  3. console.log(string.includes(substring)); // true

.repeat( )

  1. function repeat(string, count) {
  2. var strings = [];
  3. while(strings.length < count) {
  4. strings.push(string);
  5. }
  6. return strings.join('');
  7. }

在ES6,我们有一个更简洁的接口实现(.repeat())来实现字符串重复功能:

  1. // String.repeat(numberOfRepetitions)
  2. 'meow'.repeat(3); // 'meowmeowmeow'

模板字面量

使用模板字面量, 我们可以构造一段拥有特殊字符的字符串,并且我们无需显式地对它们进行转义!

  1. var text = "This string contains \"double quotes\" which are escaped."
  1. let text = `This string contains "double quotes" which are escaped.`

模板字面量 也支持(变量)插值, 它扮演的是连接字符串和值的角色:

  1. var name = 'Tiger';
  2. var age = 13;
  3. console.log('My cat is named ' + name + ' and is ' + age + ' years old.');

更为简单的是:

  1. const name = 'Tiger';
  2. const age = 13;
  3. console.log(`My cat is named ${name} and is ${age} years old.`);

在ES5,我们通常就想下面这样处理新行(new line):

  1. var text = (
  2. 'cat\n' +
  3. 'dog\n' +
  4. 'nickelodeon'
  5. )

或者(通过数组的join()方法来实现):

  1. var text = [
  2. 'cat',
  3. 'dog',
  4. 'nickelodeon'
  5. ].join('\n')

模板字面量 会为我们保留新行(new line)无需显式对它们进行上面的操作:

  1. let text = ( `cat
  2. dog
  3. nickelodeon`
  4. )

模板字面量甚至可以接收表达式:

  1. let today = new Date()
  2. let text = `The time and date is ${today.toLocaleString()}`

解构赋值

解构,意味着它允许我们以更为方便的语法从(多层嵌套的)数组和对象中"提取"出来,并将它们存储在相应的变量!

数组解构赋值

  1. var arr = [1, 2, 3, 4];
  2. var a = arr[0];
  3. var b = arr[1];
  4. var c = arr[2];
  5. var d = arr[3];
  1. let [a, b, c, d] = [1, 2, 3, 4];
  2. console.log(a); // 1
  3. console.log(b); // 2

对象解构赋值

  1. var luke = { occupation: 'jedi', father: 'anakin' }
  2. var occupation = luke.occupation; // 'jedi'
  3. var father = luke.father; // 'anakin'
  1. let luke = { occupation: 'jedi', father: 'anakin' }
  2. let {occupation, father} = luke;
  3. console.log(occupation); // 'jedi'
  4. console.log(father); // 'anakin'

模块

ES6之前,我们使用诸如Browserify库来创建在客户端模块,并且像Node.js一样使用require来加载模块.随着ES6的来临,我们可以直接使用任何类型(AMD和CommonJS)的模块。

CommonJS中暴露接口

  1. module.exports = 1
  2. module.exports = { foo: 'bar' }
  3. module.exports = ['foo', 'bar']
  4. module.exports = function bar () {}

ES6中暴露接口

在ES6, 我们暴露接口的方式变得多样化.我们甚至可以实现Named Exports(暴露名称):

  1. export let name = 'David';
  2. export let age = 25;​​

甚至暴露一个对象列表:

  1. function sumTwo(a, b) {
  2. return a + b;
  3. }
  4. function sumThree(a, b, c) {
  5. return a + b + c;
  6. }
  7. export { sumTwo, sumThree };

我们甚至通过简单地使用export关键字来暴露一个值:

  1. export function sumTwo(a, b) {
  2. return a + b;
  3. }
  4. export function sumThree(a, b, c) {
  5. return a + b + c;
  6. }

最后,我们可以暴露默认的模块接口(export default bindings):

  1. function sumTwo(a, b) {
  2. return a + b;
  3. }
  4. function sumThree(a, b, c) {
  5. return a + b + c;
  6. }
  7. let api = {
  8. sumTwo,
  9. sumThree
  10. }
  11. export default api

最佳实践: 你应当一直在模块的最底部使用export default方法,这样会使得模块(对模块的使用者)更为清晰, 并且有必要指出当前模块暴露了什么,这样可以节省它们大多时间(去理解代码的逻辑). 更何况,CommonJS模块通常会导出一个值或对象。我们坚持采用这种模式,将会使代码更容易阅读,并让CommonJS模块和ES6模块之间修改代码简直就是"easy job"!

ES6中的导入

ES6给我们带来各种导入(importing)风格. 我们可以导入整个文件:

  1. import `underscore`

有一点非常重要是,import指令只有在当前文件的顶部才能生效.

就像Python一样,我们可以以命名的方式导入模块相应的部分(比如sumTwo, sumThree):

  1. import { sumTwo, sumThree } from 'math/addition'

甚至可以为它们取”别名”:

  1. import {
  2. sumTwo as addTwoNumbers,
  3. sumThree as sumThreeNumbers
  4. } from 'math/addition'

此外,我们可以使用*字符导入所有的东西(也称为命名空间导入):

  1. import * as util from 'math/addition'

最后,我们可以从模块中导入值的列表:

  1. import * as additionUtil from 'math/addtion';
  2. const { sumTwo, sumThree } = additionUtil;

当我们导入默认的对象(这里指得是React),我们甚至可以选择性地导入这个对象的一些方法(React.Component):

  1. import React from 'react';
  2. const { Component, PropTypes } = React;

注意:模块暴露的值是绑定关系,而不是引用关系。因此,改变模块这种"绑定"关系,会影响所有依赖这些导出模块内的值。避免更改这些模块暴露出来的公共接口。(ps: 这里比较难理解)

参数

ES5中, 我们处理函数的默认参数,不定参数(PS:参数数量不固定),命名参数有各种不同的方式. 在ES6, 实现相同的功能变得更为简洁!

默认参数

  1. function addTwoNumbers(x, y) {
  2. x = x || 0;
  3. y = y || 0;
  4. return x + y;
  5. }

ES6简化了我们为函数参数提供"默认值":

  1. function addTwoNumbers(x=0, y=0) {
  2. return x + y;
  3. }
  1. addTwoNumbers(2, 4); // 6
  2. addTwoNumbers(2); // 2
  3. addTwoNumbers(); // 0

(Rest)其余参数

在ES5,我们经常像下面那样处理"函数参数数量不确定"的情况:

  1. function logArguments() {
  2. for (var i=0; i < arguments.length; i++) {
  3. console.log(arguments[i]);
  4. }
  5. }

使用rest运算符(PS:(3个点)`…`), 我们可以不定数量的参数:

  1. function logArguments(...args) {
  2. for (let arg of args) {
  3. console.log(arg);
  4. }
  5. }

命名参数(Named Parameters)

ES5中,一个处理”命名参数”的常用模式是可配置对象模式,这种模式在jQuery中广泛使用!

  1. function initializeCanvas(options) {
  2. var height = options.height || 600;
  3. var width = options.width || 400;
  4. var lineStroke = options.lineStroke || 'black';
  5. }

我们可以通过"解构赋值"来实现相同的功能:

  1. function initializeCanvas(
  2. { height=600, width=400, lineStroke='black'}) {
  3. ...
  4. }
  5. // Use variables height, width, lineStroke here

如果我们想整个值都是可配置(ps: 可选)的,我们可以”解构”一个空的对象:

  1. function initializeCanvas(
  2. { height=600, width=400, lineStroke='black'} = {}) {
  3. ...
  4. }

Spread操作符

我们可以使用spread操作符向函数传递一个数组,而这个数组的值作为函数参数来使用(ps: 比如获取数组的最大值):

  1. // ps: ES5
  2. Math.max(-1, 100, 9001, -32) // 9001
  3. Math.max.apply(null, [-1, 100, 9001, -32]) // 9001
  1. // ES6
  2. Math.max(...[-1, 100, 9001, -32]) // 9001

ES6之前,我们通过创建一个构造器函数和扩展构造函数的原型(prototype)的属性这种方式来实现类:

  1. function Person(name, age, gender) {
  2. this.name = name;
  3. this.age = age;
  4. this.gender = gender;
  5. }
  6. Person.prototype.incrementAge = function () {
  7. return this.age += 1;
  8. };

通常像下面这种方式来继承类:

  1. function Personal(name, age, gender, occupation, hobby) {
  2. Person.call(this, name, age, gender);
  3. this.occupation = occupation;
  4. this.hobby = hobby;
  5. }
  6. Personal.prototype = Object.create(Person.prototype);
  7. Personal.prototype.constructor = Personal;
  8. Personal.prototype.incrementAge = function () {
  9. return Person.prototype.incrementAge.call(this) += 1;
  10. }

ES6迫切之下为JavaScript引擎提供一种"语法糖"的方式来掩盖上面代码中丑陋的行为,那就是直接使用class关键字来创建类:

  1. class Person {
  2. constructor(name, age, gender) {
  3. this.name = name;
  4. this.age = age;
  5. this.gender = gender;
  6. }
  7. incrementAge() {
  8. this.age += 1;
  9. }
  10. }

使用extends关键字来继承父类(PS:这里值得是Person)

  1. // 使用extends继承Person父类
  2. class Personal extends Person {
  3. // ps: 构造函数
  4. constructor(name, age, gender, occupation, hobby) {
  5. super(name, age, gender);
  6. this.occupation = occupation;
  7. this.hobby = hobby;
  8. }
  9. incrementAge() {
  10. super.incrementAge();
  11. this.age += 20;
  12. console.log(this.age);
  13. }
  14. }

最佳实践: 虽然ES6提供这种创建类的语法以掩盖运行在JavaScript引擎背后对于类的实现原理,但是对于那些入门的家伙是相当好的特性,这样允许他们写出更为干净的代码.

Symbols(标记)

Symbols在ES6之间就已经存在了, 但是我们现在可以通过公共接口直接使用Symbols.其中一个例子是创建独特的属性键,这将永远不会(与其它代码)发生冲突:

  1. const key = Symbol();
  2. const keyTwo = Symbol();
  3. const object = {};
  4. object[key] = 'Such magic.';
  5. object[keyTwo] = 'Much Uniqueness'
  6. // 两个Symbols之间永不会有相同的值
  7. >> key === keyTwo
  8. >> false

Maps(映射)

Maps在JavaScript中是非常有用的数据结构!ES6之前, 我们通过对象来创建hash(哈希) maps来实现相同的目标:

  1. var map = new Object();
  2. map[key1] = 'value1';
  3. map[key2] = 'value2';

然而,这不能避免我们意外的"覆盖"某些特定属性名: (ps:例如下面代码将hasOwnProperty方法修改为一个字符串)

  1. // ps:代码不能直接运行
  2. > getOwnProperty({ hasOwnProperty: 'Hah, overwritten'}, 'Pwned');
  3. > TypeError: Property 'hasOwnProperty' is not a function

实际上,Maps允许我们set(设置), get(获取)search(查找)值,甚至为我们提供更多的功能.

  1. let map = new Map();
  2. > map.set('name', 'david');
  3. > map.get('name'); // david
  4. > map.has('name'); // true

Maps的最令人震惊的部分是,我们不再局限于使用字符串(作为对象的属性) . 现在,我们可以使用任何类型作为键,并且它不会被类型转换为字符串(ps:对象的所有键都是字符串类型!)

  1. // ps:
  2. var obj = {name: 'codemarker'};
  3. typeof Object.keys(obj)[0]; //=> "string"
  1. let map = new Map([
  2. ['name', 'david'],
  3. [true, 'false'],
  4. [1, 'one'],
  5. [{}, 'object'],
  6. [function () {}, 'function']
  7. ]);
  8. for (let key of map.keys()) {
  9. console.log(typeof key);
  10. //=> string, boolean, number, object, function
  11. };

注意:使用非基本数据类型(比如函数或对象)将会导致map.get()方法测试相等性问题时无法正常地工作。因此,坚持使用基本数据类型,如字符串,布尔和数字。(ps:Sorry,没看懂这段话的意思!)

我们可以通过.entries( )来遍历整个map:

  1. for (let [key, value] of map.entries()) {
  2. console.log(key, value);
  3. }

WeakMaps

为了在<ES5的版本中存储私有数据,我们不得不采用各种不同的方法以实现这样的意图。其中一种方法是使用命名约定(ps:下面代码中的_age, _incrementAge):

  1. class Person {
  2. constructor(age) {
  3. this._age = age;
  4. }
  5. _incrementAge() {
  6. this._age += 1;
  7. }
  8. }

但是,命名约定可能造成代码库混乱,并且这种方案并不总是可行的。取而代之,我们将使用WeakMaps来存储我们的私有数据:

  1. let _age = new WeakMap();
  2. class Person {
  3. constructor(age) {
  4. _age.set(this, age);
  5. }
  6. incrementAge() {
  7. let age = _age.get(this) + 1;
  8. _age.set(this, age);
  9. if(age > 50) {
  10. console.log('Midlife crisis');
  11. }
  12. }
  13. }

使用WeakMaps存储私有数据最cool的地方是:它们的键并不会泄露! 我们可以通过Reflect.ownKeys()进行验证:

  1. > const person = new Person(50);
  2. > person.incrementAge(); // 'Midlife crisis'
  3. > Reflect.ownKeys(person); // []

Promises(承诺)

Promises将会改写回调函数的风格(callback hell):

ps:(个人理解,callback hell是一种异步Javascript回调函数的代码书写风格,比如下面代码中func2必须等待func1有相应的处理结果(value1),回调才能继续处理下去!很明显的问题,代码将会横向"变态级"扩张)

  1. func1(function (value1) {
  2. func2(value1, function(value2) {
  3. func3(value2, function(value3) {
  4. func4(value3, function(value4) {
  5. func5(value4, function(value5) {
  6. // Do something with value 5
  7. });
  8. });
  9. });
  10. });
  11. });

将代码改为"竖直"风格看看(ps:哈哈,是不是优雅多了?):

  1. func1(value1)
  2. .then(func2)
  3. .then(func3)
  4. .then(func4)
  5. .then(func5, value5 => {
  6. // Do something with value 5
  7. });

ES6之前, 我们采用 bluebird 或者Q实现上面优雅的方案.现在ES6原生支持Promise:

  1. new Promise((resolve, reject) =>
  2. reject(new Error('Failed to fulfill Promise')))
  3. .catch(reason => console.log(reason));

Promise有两个事件处理程序, 分别是resolverejected.当 Promise的状态为fulfilled时,resolve函数将会被调用,而当 Promise的状态为rejected时,resolve函数将会被调用!

Promises的好处: 使用一连串的回调嵌套来处理错误将会使程序变得混乱.使用Promises的好处,我们的代码结构变得非常清晰,以冒泡的形式抛出错误并优雅地处理相应的错误。此外,被resolved/rejected Promise后的值是保持不变的 - 它永远不会改变。

下面是使用承诺的一个实际的例子:

  1. var fetchJSON = function(url) {
  2. return new Promise((resolve, reject) => {
  3. $.getJSON(url)
  4. .done((json) => resolve(json))
  5. .fail((xhr, status, err) => reject(status + err.message));
  6. });
  7. }

我们也使用通过Promise.all( )方法来使Promises并行处理异步操作的数组:

  1. var urls = [
  2. 'http://www.api.com/items/1234',
  3. 'http://www.api.com/items/4567'
  4. ];
  5. var urlPromises = urls.map(fetchJSON);
  6. Promise.all(urlPromises)
  7. .then(function(results) {
  8. results.forEach(function(data) {
  9. });
  10. })
  11. .catch(function(err) {
  12. console.log("Failed: ", err);
  13. });

THX FOR READING

:-(