这份手抄包含ES2015 [ES6]的使用提示, 技巧, 最佳实践 和以及一些常用代码片段. 欢迎贡献!本文尽量保留原作者的意思,标记为"ps:"都是个人的理解,当然也有一些翻译不到位的地方,望请见谅!
- 译者:@devtip
- 翻译日期: 2016-01-27
- 原文地址: https://github.com/DrkSephy/es6-cheatsheet
- pdf版本请点击这里前往百度网盘
除var之外, 我们现在可以通过两个新的标识符(let,const)来存储变量. 而它们与 var不同的是, let 和 const 语句并不会提升到作用域的前端,这是因为ES6之前不存在"块级作用域"!
一个使用var的例子:
var snack = 'Meow Mix';
function getFood(food) {
// snack变量被提升到这里
// var snack = undefined
if (food) {
var snack = 'Friskies';
// snack = 'Friskies';
return snack;
}
return snack;
}
getFood(false); // undefined
然而, 当我们使用let替换var,程序到底发生了什么:
let snack = 'Meow Mix';
function getFood(food) {
if (food) {
// 这里不会被提升到作用域的前端
let snack = 'Friskies';
return snack;
}
// 当food为false时,它只能从上一级作用域查找变量snack,这个过程一直延伸到"全局作用域"
return snack;
}
getFood(false); // 'Meow Mix'
当重构遗留代码的时候,特别要小心那些使用var的代码!盲目地使用let代替var将会产生意想不到的行为!
注意: let 和const都是块级作用域的. 因此,块级作用域的标识符在定义之前就被引用,将会产生
ReferenceError
(引用错误)
console.log(x);
let x = "hi"; // 引用错误: x还没有定义
最佳实践: 在遗留代码中保留var声明的语句,以表示这段代码重构需要慎重考虑!当我们在一个新的代码库中工作时,使用let声明的变量,将会随着时间的推移发生改变,以及const声明的变量,通常并不会改变!(ps: 实质上,使用const声明的变量只有"只读"状态)
自调用函数表达式的常见用途是将变量封装在其作用域!ES6提供了一种创建块级作用域的能力,因此,我们不在单纯局限于函数作用域!
(function () {
var food = 'Meow Mix';
}());
console.log(food); // 引用错误
使用ES6块:
{
let food = 'Meow Mix';
}
console.log(food); // 引用错误
很多时候,嵌套函数的目的是想"维护"当前词法作用域中this。看下面的例子:
function Person(name) {
this.name = name;
}
Person.prototype.prefixName = function (arr) {
return arr.map(function (character) {
// ps: this被绑定到全局对象,严格模式this被绑定到undefined
return this.name + character; // 不能从undefined读取到name属性
});
};
一个比较普遍的解决方案是通过一个变量(ps:通常是that)来存储当前上下文的this
function Person(name) {
this.name = name;
}
Person.prototype.prefixName = function (arr) {
var that = this; // 存储当前上下文的this
return arr.map(function (character) {
return that.name + character;
});
};
我们可以为数组的map方法传入一个适当的上下文this
作为map方法的参数:
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并没有变得很神秘,重写上面的代码(ps: 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 }); // 函数表达式
const arr = [1, 2, 3, 4, 5];
const squares = arr.map(x => x * x); // Arrow Function for terser implementation
最佳实践: 尽可能使用箭头函数替代函数表达式.
随着ES6标准库功能的日臻完善,我们可以在字符串中使用新方法,诸如**.includes()和.repeat()**。
var string = 'food';
var substring = 'foo';
console.log(string.indexOf(substring) > -1);
当字符串之间是包含关系,将会返回一个> -1
的值,我们可以简单地使用 **.includes()取代这种操作,并且.includes()**将会返回一个布尔(boolean)值:
const string = 'food';
const substring = 'foo';
console.log(string.includes(substring)); // true
function repeat(string, count) {
var strings = [];
while(strings.length < count) {
strings.push(string);
}
return strings.join('');
}
在ES6,我们有一个更简洁的接口实现(.repeat())来实现字符串重复功能:
// String.repeat(numberOfRepetitions)
'meow'.repeat(3); // 'meowmeowmeow'
使用模板字面量, 我们可以构造一段拥有特殊字符的字符串,并且我们无需显式地对它们进行转义!
var text = "This string contains \"double quotes\" which are escaped."
let text = `This string contains "double quotes" which are escaped.`
模板字面量 也支持(变量)插值, 它扮演的是连接字符串和值的角色:
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,我们通常就想下面这样处理新行(new line):
var text = (
'cat\n' +
'dog\n' +
'nickelodeon'
)
或者(通过数组的join()方法来实现):
var text = [
'cat',
'dog',
'nickelodeon'
].join('\n')
模板字面量 会为我们保留新行(new line)无需显式对它们进行上面的操作:
let text = ( `cat
dog
nickelodeon`
)
模板字面量甚至可以接收表达式:
let today = new Date()
let text = `The time and date is ${today.toLocaleString()}`
解构,意味着它允许我们以更为方便的语法从(多层嵌套的)数组和对象中"提取"出来,并将它们存储在相应的变量!
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); // 1
console.log(b); // 2
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'
ES6之前,我们使用诸如Browserify库来创建在客户端模块,并且像Node.js一样使用require来加载模块.随着ES6的来临,我们可以直接使用任何类型(AMD和CommonJS)的模块。
module.exports = 1
module.exports = { foo: 'bar' }
module.exports = ['foo', 'bar']
module.exports = function bar () {}
在ES6, 我们暴露接口的方式变得多样化.我们甚至可以实现Named 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;
}
最后,我们可以暴露默认的模块接口(export default bindings):
function sumTwo(a, b) {
return a + b;
}
function sumThree(a, b, c) {
return a + b + c;
}
let api = {
sumTwo,
sumThree
}
export default api
最佳实践: 你应当一直在模块的最底部使用export default方法,这样会使得模块(对模块的使用者)更为清晰, 并且有必要指出当前模块暴露了什么,这样可以节省它们大多时间(去理解代码的逻辑). 更何况,CommonJS模块通常会导出一个值或对象。我们坚持采用这种模式,将会使代码更容易阅读,并让CommonJS模块和ES6模块之间修改代码简直就是"easy job"!
ES6给我们带来各种导入(importing)风格. 我们可以导入整个文件:
import `underscore`
有一点非常重要是,import指令只有在当前文件的顶部才能生效.
就像Python一样,我们可以以命名的方式导入模块相应的部分(比如sumTwo, sumThree):
import { sumTwo, sumThree } from 'math/addition'
甚至可以为它们取"别名":
import {
sumTwo as addTwoNumbers,
sumThree as sumThreeNumbers
} from 'math/addition'
此外,我们可以使用*
字符导入所有的东西(也称为命名空间导入):
import * as util from 'math/addition'
最后,我们可以从模块中导入值的列表:
import * as additionUtil from 'math/addtion';
const { sumTwo, sumThree } = additionUtil;
当我们导入默认的对象(这里指得是React),我们甚至可以选择性地导入这个对象的一些方法(React.Component):
import React from 'react';
const { Component, PropTypes } = React;
注意:模块暴露的值是绑定关系,而不是引用关系。因此,改变模块这种"绑定"关系,会影响所有依赖这些导出模块内的值。避免更改这些模块暴露出来的公共接口。(ps: 这里比较难理解)
ES5中, 我们处理函数的默认参数,不定参数(PS:参数数量不固定),命名参数有各种不同的方式. 在ES6, 实现相同的功能变得更为简洁!
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); // 6
addTwoNumbers(2); // 2
addTwoNumbers(); // 0
在ES5,我们经常像下面那样处理"函数参数数量不确定"的情况:
function logArguments() {
for (var i=0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
使用rest运算符(PS:(3个点)`...`), 我们可以不定数量的参数:
function logArguments(...args) {
for (let arg of args) {
console.log(arg);
}
}
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
如果我们想整个值都是可配置(ps: 可选)的,我们可以"解构"一个空的对象:
function initializeCanvas(
{ height=600, width=400, lineStroke='black'} = {}) {
...
}
我们可以使用spread操作符向函数传递一个数组,而这个数组的值作为函数参数来使用(ps: 比如获取数组的最大值):
// ps: ES5
Math.max(-1, 100, 9001, -32) // 9001
Math.max.apply(null, [-1, 100, 9001, -32]) // 9001
// ES6
Math.max(...[-1, 100, 9001, -32]) // 9001
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 () {
return Person.prototype.incrementAge.call(this) += 1;
}
ES6迫切之下为JavaScript引擎提供一种"语法糖"的方式来掩盖上面代码中丑陋的行为,那就是直接使用class关键字来创建类:
class Person {
constructor(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
incrementAge() {
this.age += 1;
}
}
使用extends关键字来继承父类(PS:这里值得是Person)
// 使用extends继承Person父类
class Personal extends Person {
// ps: 构造函数
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提供这种创建类的语法以掩盖运行在JavaScript引擎背后对于类的实现原理,但是对于那些入门的家伙是相当好的特性,这样允许他们写出更为干净的代码.
Symbols在ES6之间就已经存在了, 但是我们现在可以通过公共接口直接使用Symbols.其中一个例子是创建独特的属性键,这将永远不会(与其它代码)发生冲突:
const key = Symbol();
const keyTwo = Symbol();
const object = {};
object[key] = 'Such magic.';
object[keyTwo] = 'Much Uniqueness'
// 两个Symbols之间永不会有相同的值
>> key === keyTwo
>> false
Maps在JavaScript中是非常有用的数据结构!ES6之前, 我们通过对象来创建hash(哈希) maps来实现相同的目标:
var map = new Object();
map[key1] = 'value1';
map[key2] = 'value2';
然而,这不能避免我们意外的"覆盖"某些特定属性名: (ps:例如下面代码将hasOwnProperty方法修改为一个字符串)
// ps:代码不能直接运行
> getOwnProperty({ hasOwnProperty: 'Hah, overwritten'}, 'Pwned');
> TypeError: Property 'hasOwnProperty' is not a function
实际上,Maps允许我们set(设置), **get(获取)**和 **search(查找)**值,甚至为我们提供更多的功能.
let map = new Map();
> map.set('name', 'david');
> map.get('name'); // david
> map.has('name'); // true
Maps的最令人震惊的部分是,我们不再局限于使用字符串(作为对象的属性) . 现在,我们可以使用任何类型作为键,并且它不会被类型转换为字符串(ps:对象的所有键都是字符串类型!)
// ps:
var obj = {name: 'codemarker'};
typeof Object.keys(obj)[0]; //=> "string"
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()
方法测试相等性问题时无法正常地工作。因此,坚持使用基本数据类型,如字符串,布尔和数字。(ps:Sorry,没看懂这段话的意思!)
我们可以通过**.entries( )**来遍历整个map:
for (let [key, value] of map.entries()) {
console.log(key, value);
}
为了在<ES5的版本中存储私有数据,我们不得不采用各种不同的方法以实现这样的意图。其中一种方法是使用命名约定(ps:下面代码中的_age, _incrementAge):
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');
}
}
}
使用WeakMaps存储私有数据最cool的地方是:它们的键并不会泄露! 我们可以通过**Reflect.ownKeys()**进行验证:
> const person = new Person(50);
> person.incrementAge(); // 'Midlife crisis'
> Reflect.ownKeys(person); // []
Promises将会改写回调函数的风格(callback hell):
ps:(个人理解,callback hell是一种异步Javascript回调函数的代码书写风格,比如下面代码中func2必须等待func1有相应的处理结果(value1),回调才能继续处理下去!很明显的问题,代码将会横向"变态级"扩张)
func1(function (value1) {
func2(value1, function(value2) {
func3(value2, function(value3) {
func4(value3, function(value4) {
func5(value4, function(value5) {
// Do something with value 5
});
});
});
});
});
将代码改为"竖直"风格看看(ps:哈哈,是不是优雅多了?):
func1(value1)
.then(func2)
.then(func3)
.then(func4)
.then(func5, value5 => {
// Do something with value 5
});
ES6之前, 我们采用 bluebird 或者Q实现上面优雅的方案.现在ES6原生支持Promise:
new Promise((resolve, reject) =>
reject(new Error('Failed to fulfill Promise')))
.catch(reason => console.log(reason));
Promise有两个事件处理程序, 分别是resolve和rejected.当 Promise的状态为fulfilled时,resolve函数将会被调用,而当 Promise的状态为rejected时,resolve函数将会被调用!
Promises的好处: 使用一连串的回调嵌套来处理错误将会使程序变得混乱.使用Promises的好处,我们的代码结构变得非常清晰,以冒泡的形式抛出错误并优雅地处理相应的错误。此外,被resolved/rejected Promise后的值是保持不变的 - 它永远不会改变。
下面是使用承诺的一个实际的例子:
var fetchJSON = function(url) {
return new Promise((resolve, reject) => {
$.getJSON(url)
.done((json) => resolve(json))
.fail((xhr, status, err) => reject(status + err.message));
});
}
我们也使用通过**Promise.all( )**方法来使Promises并行处理异步操作的数组:
var urls = [
'http://www.api.com/items/1234',
'http://www.api.com/items/4567'
];
var urlPromises = urls.map(fetchJSON);
Promise.all(urlPromises)
.then(function(results) {
results.forEach(function(data) {
});
})
.catch(function(err) {
console.log("Failed: ", err);
});
:-(