Skip to content

devtip/es6-cheatsheet-chinese

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 

Repository files navigation

ES6手抄

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

var 对比 let / const

除var之外, 我们现在可以通过两个新的标识符(letconst)来存储变量. 而它们与 var不同的是, letconst 语句并不会提升到作用域的前端,这是因为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将会产生意想不到的行为!

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

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

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

使用语句块替换IIFE

自调用函数表达式的常见用途是将变量封装在其作用域!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()**。

.includes( )

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

.repeat( )

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)的模块。

CommonJS中暴露接口

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

ES6中暴露接口

在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中的导入

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

(Rest)其余参数

在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);
    }
}

命名参数(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

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

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

Spread操作符

我们可以使用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(标记)

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

const key = Symbol();
const keyTwo = Symbol();
const object = {};

object[key] = 'Such magic.';
object[keyTwo] = 'Much Uniqueness'

// 两个Symbols之间永不会有相同的值
>> key === keyTwo
>> false

Maps(映射)

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

WeakMaps

为了在<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(承诺)

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有两个事件处理程序, 分别是resolverejected.当 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);
  });

THX FOR READING

:-(

About

es6手抄中文翻译

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published