ES6学习笔记(一)

ES6 学习笔记(一)

let 和 const

let 命令

  • 基本用法

    1
    2
    3
    4
    5
    6
    7
    // let 声明的变量只在 let 命令所在的代码块内有效
    {
    let a = 10;
    var b = 10;
    }
    console.log(a); // ReferenceError: a is not defined.
    console.log(b); // 10
    1
    2
    3
    4
    5
    // for 循环中同样如此
    for (let i = 0; i < 10; i++) {
    // ...
    }
    console.log(i); // ReferenceError: a is not defined.
    1
    2
    3
    4
    5
    6
    7
    8
    // 另外在 for 循环中,循环变量的那部分是一个父级作用域,循环体内部是一个单独的子作用域
    for (let i = 0; i < 3; i++) {
    let i = 'abc';
    console.log(i);
    }
    // abc
    // abc
    // abc
  • 不存在变量提升

    1
    2
    3
    4
    5
    6
    7
    // var 的情况
    console.log(foo); // undefined
    var foo = 'foo';

    // let 的情况
    console.log(bar); // ReferenceError
    let bar = 'bar';
  • 暂时性死区(temporal dead zone)

    本质上,已进入当前的块级作用域,所需变量就已经存在了,不过不可取,只有等到声明变量的哪一行代码出现才可以获取和使用该变量

    1
    2
    3
    4
    5
    6
    7
    // 在这个例子中,块级作用域已经有了 temp 变量,但没有等到声明语句出现就开始赋值,所以报错
    var temp = 123;

    if (true) {
    temp = 233; // ReferenceError
    let temp;
    }
    1
    2
    3
    4
    5
    6
    // bar 函数报错,因为 x 的默认值 y 没有声明
    function bar(x = y, y = 2) {
    return [x, y];
    }

    bar(); // ReferenceError: y is not defined.
    1
    2
    // 这个例子同样如此
    let x = x; // ReferenceError: x is not defined.
  • 不允许重复声明

    1
    2
    3
    var a = 1;
    var a = 2;
    let a = 3; // SyntaxError: Identifier 'a' has already been declared.
  • 块级作用域与函数声明

    ES6中规定,块级作用域中函数声明语句的行为类似于 let ,在块级作用域之外不可用。
    但在浏览器中的实现可以不遵守以上规定,有自己的行为方式
    - 允许在块级作用域内声明函数
    - 函数声明类似于 var,即会提升到全局作用域或函数作用域的头部
    - 同时函数声明还会提升到所在的块级作用域的头部
    上述的三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作 let 处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 浏览器的 ES6 环境
    (function () {
    if (false) {
    function f() {
    console.log('I am inside!');
    }
    }

    f(); // TypeError: f is not a function.
    }());

    // 在符合 ES6 的浏览器中等同于一下代码
    (function () {
    var f = undefined;
    if (false) {
    function f() {
    console.log('I am inside!');
    }
    }

    f(); // TypeError: f is not a function.
    }());

    所以应该避免在块级作用域内声明函数,确实需要的话也应该写成函数表达式,而非声明语句

    1
    2
    3
    4
    // 另外块级作用域需要大括号,否则也会报错
    'use strict'
    if (true)
    function f() {} // In strict mode code, functions can only be declared at top level or inside a block.

const命令

const 声明的通常是一个只读的常量,一旦声明,值不可被改变。本质上 const 保证的并不是变量的值而是变量指向的内存地址。对于简单类型的数据,值就是保存在变量指向的那个内存地址。而复合类型的数据,变量指向的内存地址保存的是一个指针,指向这个数据结构,指针是固定的,但数据结构可能会改变。

另外 constlet 一样,声明的常量不提升,存在暂时性死区以及不可重复声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// const 声明的简单类型的数据不可改变
const PI = 3.1415;
PI = 123; // TypeError: Assignment to constant variable.

// const 只声明不赋值也会报错
const foo; // SyntaxError: Missing initializer in const declaration

// const 声明基本类型与引用类型的区别
const a = [];
a.push("hello");
a = ["hello"]; // TypeError: Assignment to constant variable.

// 从 ES6 开始,let 命令,const 命令,class 命令声明的全局变量不属于顶层对象的属性
let a = 1;
window.a // undefined
var b = 2;
window.b // 2

解构赋值

  • 基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let [a, b, c] = [1, 2, 3];
a // 1
b // 2
c // 3

let [bar, foo] = [1];
bar // 1
foo // undefined

// 由于等式的右边都不是可遍历(不具备 Iterator接口),都会报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
  • 默认值
1
2
3
4
5
6
7
8
// 解构赋值允许指定默认值
let [x, y = 'b'] = ['a']; // x = 'a', y = 'b'

// 只有当一个数组成员 === undefined,默认值才会生效
let [x = 1] = null; // x = null;

// 如果默认值是一个表达式,则其是惰性求值的
let [x = f()] = [1]; // f() 不会执行
  • 对象的解构赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 基本用法
let {bar: bar, foo: foo} = {foo: 'aaa', bar: 'bbb'};
bar // 'bbb'
foo // 'aaa'

// 简写形式
let {bar, foo} = {foo: 'aaa', bar: 'bbb'};
bar // 'bbb'
foo // 'aaa'

// 嵌套赋值
let obj = {};
let arr = [];

({foo: obj.prop, bar: arr[0]} = {foo: 123, bar: true});
obj // {prop: 123}
arr // {true}

// 默认赋值
let {x = 3} = {x: undefined};
x // 3
let {z = 5} = {z: null};
z // null

// 不要将大括号写在首行,避免 JavaScript 将其解释为代码块
let x;
({x} = {x: 1});
  • 字符串的解构赋值
1
2
3
4
5
6
7
8
9
const [a, b, c, d, e] = 'hello';
a // 'h'
b // 'e'
c // 'l'
d // 'l'
e // 'o'

let {length: len} = 'hello';
len // 5
  • 数值和布尔值的解构赋值
1
2
3
4
5
6
7
8
9
// 进行解构赋值时会将右边的数值或布尔值转换成对象
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true

// 如果右边不能转换成对象的话,则会报错
let {prop: x} = undefined; // TypeError: Cannot destructure property `prop` of 'undefined' or 'null'.
let {prop: y} = null; // TypeError: Cannot destructure property `prop` of 'undefined' or 'null'.
  • 函数参数的解构赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 基本用法
[[1, 2], [3, 4]].map(([a, b]) => a + b); // [3, 7]

// undefined 会触发参数的默认值
[1. undefined, 3].map((x = 'yes') => x); // [1, 'yes', 3]

// 参数变量是默认声明的,不能使用 let 或 const 再次声明
function foo(x = 5) {
let x = 1; // SyntaxError: Identifier 'x' has already been declared
const x = 2; // SyntaxError: Identifier 'x' has already been declared
}

// 函数使用参数默认值时,不能有同名参数
function foo(x, x, y) {} // 不报错
function foo(x, x, y = 1) {} //报错

// 参数默认值是惰性求值的,即不是在定义时执行的,而是在运行时执行的。定义默认值的参数应该是函数的尾参数,这样比较容易辨别且便于计算函数的 length 值
  • 解构赋值用途

    - 交换变量的值
    - 从函数返回多个值
    - 函数参数的定义
    - 提取 JSON 数据
    - 函数参数的默认值
    - 遍历 Map 结构
    - 输入模块的指定方法

模板字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识

  • 当作普通字符串使用

    1
    2
    3
    4
    var a = `this is a \n string`;
    a;
    // 'this is a
    // string'
  • 用来定义多行字符串,所有的空格和缩进会被保留在输出之中

    1
    2
    3
    4
    5
    6
    7
    var b = `this is a temp   late strin

    g`;
    b;
    // 'this is a temp late strin
    //
    // g'
  • 在字符串中嵌入变量,需要将变量名写在 ${} 之中,并且大括号内还可以放入任意的 JavaScript 表达式

    1
    2
    3
    4
    // 大括号内可以放入变量名或表达式,未声明的变量会报错
    let x = 1;
    let y = 2;
    `${x} + ${y} = ${x + y}`; // '1 + 2 = 3'
    1
    2
    3
    4
    5
    6
    // 模板字符串还能调用函数
    function fn() {
    return 'Hello World';
    }

    `foo ${fn()} bar`; // foo Hello World bar
  • 标签模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // 将模板字符串紧跟在一个函数后面,该函数将被调来处理这个模板字符串
    alert`123`; // 等同于
    alert(123);

    // 如果模板字符串里面有变量,需要先将模板字符串处理成多个参数,再调用函数
    let a = 5;
    let b = 10;
    function tag(stringArr, value1, value2) {
    // ...
    }
    tag`Hello ${a + b} world ${a * b}`; // 等同于
    tag(['Hello ', ' world '], 15, 50);

    // 标签模板还有一个重要应用就是过滤 HTML 字符串,防止用户输入恶意内容
    let sender = `<script>alert("devil script");</script>`;
    function SaferHTML(templateData) {
    let s = templateData[0];
    for (let i = 1; i < arguments.length; i++) {
    let arg = String(arguments[i]);
    s += arg.replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
    s += templateData[i];
    }
    return s;
    }
    let message = SaferHTML`<p>${sender} has sent you a message.</p>`;
    message
    // "<p>&lt;script&gt;alert("devil script");&lt;/script&gt; has sent you a message.</p>"

箭头函数

箭头函数表达式的语法比 函数表达式 更短,并且不绑定自己的 argumentssuper 以及 new.target。其函数体内的 this 对象就是定义时所在的对象,而不是使用时所在的对象。另外箭头函数不能用作构造函数以及 Generator 函数。

  • 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Basic Syntax
    (p1, p2, ..., pN) => { statements };
    (p1, p2, ..., pN) => expression // 等同于 { expression }

    (singleParam) => { statements }
    singleParam => { statements } // 单个参数括号可以省略

    () => { statements } // 无参数

    // Advanced Syntax
    params => ({foo: bar}) // 返回值为对象时,需要加上括号

    (p1, p2, ...rest) => { statements } // 支持rest参数
    (p1, p2, p3 = default) => { statements } // 支持默认参数
    var f = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c; f(); // 支持解构赋值
  • 使用注意点

    • 函数体内的 this 对象其实就是外层代码块的 this,也就是说箭头函数根本没有自己的 this
    • 不可以当作构造函数,即不可以使用 new 命令,否则会抛出一个错误。
    • 不可以使用 arguments 对象,该对象在函数体内不存在,可以用 rest 代替。
    • 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
  • 箭头函数转换成 ES5

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // ES6
    function foo() {
    setTimeout(() => {
    console.log('id:', this.id);
    },100);
    }

    // ES5
    function foo() {
    var _this = this;

    setTimeout(function () {
    console.log('id:', _this.id);
    });
    }
  • 部署pipeline

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const pipeline = (...funcs) => val => funcs.reduce((a, b) =>
    b(a), val);

    const plus1 = a => a + 1;
    const mult2 = a => a * 2;

    const addThenMult = pipeline(plus1, mult2);

    addThenMult(5);

扩展运算符

扩展运算符(spread)是三个点(...)。类似于 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

扩展运算符调用的是遍历器接口 (Symbol.iterator),只有具有该接口的对象都可以使用该运算符将其转换成真正的数组

Array.from 方法可以将可遍历对象(iterabel)以及 类似数组的对象(array-like object)转换成真正的数组

Array.of 方法则是将一组值,转换为数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 基本用法
console.log(...[1, 2, 3]); // 1 2 3

// 替代 apply 方法
Math.max.apply(null, [8, 19, 2]); // ES5
Math.max(...[8192]); // ES6

// 将 arr2 添加到 arr1 的尾部
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];

arr1.push(...[arr2]); // push 方法的参数不能是数组

// 复制数组
const a1 = [1, 2];
const a2 = [...a1]; // 写法一
const [...a3] = a1; // 写法二

// 合并数组
[...a1, ...a2, ...a3];

// 扩展运算符用于数组赋值,正能放在参数的最后一位,否则会报错
const [first, ...rest] = [1, 2, 3, 4, 5];
const [first, ...second, rest] = [1, 2, 3, 4, 5]; // 报错

// 可以将字符串转换成真正的数组,以此求得字符串的长度,可以避免 js 将大于 \uFFFF 的 Unicode 字符算作两个字符的 bug
function length(str) {
return [...str].length;
}

function length(str) {
return Array.from(str).length; // Array.from 方法同样可以
}

length('x\uD83D\uDE80y'); // 3

// 可以将任何具有 Iterator 接口的对象转换成真正的数组
let nodeList = document.querySelecotrAll('div');
let array = [...nodeList];

let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
}

let arr = [...arrayLike]; // TypeError: arrayLike is not iterable, 此时可以使用 Array.from 方法将 arrayLike 转换为真正的数组

// Map 和 Set 结构上使用扩展运算符
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]

// Generator 函数运行后,返回一个遍历器对象,可以使用扩展运算符
const go = function *() {
yield 1;
yield 2;
yield 3;
};

[...go()]; // [1, 2, 3]

Set 和 Map 数据结构

Set

Set 数据结构类似于数组,但是成员的值都是唯一的,不存在重复的值。
Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化

  • 基本用法

    1
    2
    3
    4
    5
    const set = new Set([1, 2, 3, 4, 4]);
    [...set] // [1, 2, 3, 4]

    // 去除数组的重复成员,Set 内部判断两个值是否相等的算法类似于 '===' ,主要区别在于 NaN 等于自身
    [...new Set(array)]
  • 属性与方法

    属性:
    - Set.prototype.constructor: 构造函数,默认就是 Set 函数。
    - Set.prototypr.size: 返回 Set 实例的成员总数

    方法:
    - add(value):添加某个值,返回 Set 结构本身
    - delete(value):删除某个值,返回一个布尔值,表示是否删除成功
    - has(value):返回一个布尔值,表示该值是否为 Set 的成员
    - clear():清除所有成员,没有返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let s = new Set();
    s.add(1).add(2).add(3).add(2);
    s // Set(3) {1, 2, 3}
    s.size // 3
    s.has(1) // true
    s.delete(2);
    s.has(2) // false
    s.clear() // Set(0) {}

    // 应用,数组去重
    function dedupe(array) {
    return Array.from(new Set(array));
    }
  • 遍历操作

    - keys():返回键名的遍历器
    - values():返回键值的遍历器
    - entries():返回键值对的遍历器
    - forEach():返回回调函数遍历的每个成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    let set = new Set(['red', 'green', 'blue']);

    for (let item of set.keys()) {
    console.log(item); // red green blue
    }

    // 由于 Set 结构没有键名,只有键值,所以 keys 方法和 values 方法的行为完全一致
    for (let item of set.values()) {
    console.log(item); // red green blue
    }

    for (let item of set.entries()) {
    console.log(item); // ['red', 'red'] ['green', 'green'] ['blue', 'blue']
    }

    // Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的 values 方法
    Set.prototype[Symbol.iterator] === Set.prototype.values // true

    for (let item of set) {
    console.log(item); // red green blue
    }

    // forEach 方法,用于对每个成员执行某种操作,没有返回值
    let set = new Set([1, 4, 9]);
    set.forEach((value, key) => console.log(key + ': ' + value));

    // Set 具有遍历器接口 (`Symbol.iterator`),所以可以用扩展运算符将其变为真正的数组
    let set = new Set(['red', 'green', 'blue']);
    let arr = [...set]; // ['red', 'green', 'blue']

Map

Map 类似于对象,也是键值对的集合,但“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

  • 基本用法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // Map 可以接受一个数组作为参数,该数组的成员是一个个表示键值对的数组

    const map = new Map([
    ['name', '张三'],
    ['title', 'Author']
    ]);

    map.size // 2
    map.has('name') // true
    map.get('name') // "张三"
    map.has('title') // true
    map.get('title') // "Author"
    map.set(NaN, 123);
    map.get(NaN) // 123
  • 属性与方法

    - size:属性返回 Map 结构的成员总数
    - set(key, value):方法设置键名 key 对应的键值 value,然后返回整个 Map 结构,会覆盖已有的键值
    - get(key):方法读取 key 对应的键值,找不到 key ,返回 undefined
    - has(key):方法返回一个布尔值,表示某个键是否在当前 Map 对象之中
    - delete(key):方法删除某个 key ,返回 true,如果删除失败,返回 false
    - clear():方法清除所有成员,没有返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const m = new Map();
    m.set('year', 2018);
    m.set('target', 'done');
    m.size // 2
    m.get('year') // 2018
    m.has('year') // true
    m.delete('year') // true
    m.clear()
    m // Map(0) {}
  • 遍历方法

    - keys():返回键名的遍历器
    - values():返回键值的遍历器
    - entries():返回所有成员的遍历器
    - forEach():遍历 Map 的所有成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const map = new Map([
    ['F', 'no'],
    ['T', 'yes'],
    ]);

    for (let key of map.keys()) {
    console.log(key); // 'F' 'T'
    }

    for (let value of map.values()) {
    console.log(value); // 'no' 'yes'
    }

    for (let [key, value] of map.entries()) {
    console.log(key, value); // 'F' 'no' 'T' 'yes'
    }

    // 等同于使用 map.entries()
    // Map 结构的默认遍历接口(Symbol.iterator属性),就是 entries 方法
    for (let [key, value] of map) {
    console.log(key, value); // 'F' 'no' 'T' 'yes'
    }

Iterator 和 for…of循环

  • 基本概念

    遍历器(Iterator)是一种接口,为各种不同的数据结构(ArrayMapSetStringTypedArrayargumentsNodeList),提供一个统一的访问机制。使得数据结构的成员能够按某种次序排列。另外在 ES6 中创造了一种新的遍历命令 for...of 循环,Iterator 接口主要提供 for...of 消费。

    ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性上,或者说一个数据结构只要具有 Symbol.iterator 属性,就可以认为是”可遍历的“。

    执行这个属性会返回一个遍历器对象,该对象的根本特征就是具有 next 方法。每次调用 next 方法,都会返回一个代表当前成员的信息对象,具有 valuedone 两个属性

    凡是部署了 Symbol.iterator 属性的数据结构,就称为部署了遍历器接口,调用这个接口,就会返回一个遍历器对象

    原生具备 Iterator 接口的数据结构如下:

    - Array
    - Map
    - Set
    - String
    - TypedArray
    - arguments
    - NodeList

    对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。就对象而言,部署遍历器接口不是很必要,因为这使得对象实际上被当作 Map 结构使用,而 ES6 原生提供了

    对于类似数组的对象(存在数值键名和 length 属性),部署 Iterator 接口,可以将 Symbol.iterator 方法直接引用数组的 Iterator 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    let arr = ['a', 'b', 'c'];
    let iter = arr[Symbol.iterator]();
    iter.next(); // { vlaue: 'a', done: false }
    iter.next(); // { vlaue: 'b', done: false }
    iter.next(); // { vlaue: 'c', done: false }
    iter.next(); // { vlaue: 'undefined', done: true }

    // 一个为对象添加 Iterator 接口的例子
    let obj = {
    data: ['Hello', 'World'],
    [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
    next() {
    if (index < self.data.length) {
    return {
    value: self.data[index++],
    done: true
    };
    } else {
    return { value: undefined, done: true};
    }
    }
    };
    }
    };

    // 类数组对象部署 Iterator 方法
    let iterable = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator]
    };
    for (let item of iterable) {
    console.log(item); // 'a', 'b', 'c'
    }
  • 使用场合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // 1. 解构赋值
    // 对数组和 Set 结构进行结构赋值的时候,会默认调用 Symbol.iterator 方法
    let set = new Set().add('a').add('b').add('c');
    let [first, ...rest] = set; // first = 'a'; rest = ['b', 'c'];

    // 2. 扩展运算符
    // 扩展运算符(...)也会调用默认的 Iterator 接口
    var str = 'Hello';
    [...str] // ['H', 'e', 'l', 'l', 'o']

    // 3. yield*
    // yield* 后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口
    let generator = function* () {
    yield 1;
    yield* [2, 3, 4];
    yield 5;
    }

    let iterator = generator();

    iterator.next(); // { value: 1, done: false }
    iterator.next(); // { value: 2, done: false }
    iterator.next(); // { value: 3, done: false }
    iterator.next(); // { value: 4, done: false }
    iterator.next(); // { value: 5, done: false }
    iterator.next(); // { value: undefined, done: true }

    // 4. 其他场合
    // 由于数组的遍历会调用遍历器的接口,所以任何接受数组作为参数的场合,都调用了遍历器接口
    // - for...of
    // - Array.from()
    // - Map(), Set(), WeakMap(), WeakSet()
    // - Promise.all()
    // - Promise.race()
  • 字符串的 Iterator 接口

    1
    2
    3
    4
    5
    var str = 'hi';
    var iterator = str[Symbol.iterator]();
    iterator.next() // { value: 'h', done: false }
    iterator.next() // { value: 'i', done: false }
    iterator.next() // { value: undefined, done: true }
  • Iterator 接口与 Generator 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let obj = {
    * [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
    }
    }
    for (let x of obj) {
    console.log(x); // 'hello' 'world'
    }
  • 遍历器对象的 return(),throw()

    遍历器对象除了具有 next 方法,还可以具有 return 方法和 throw 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // return 方法的使用场合时,如果 for...of 循环提前退出(出错,break 语句或 continue),就会调用 return 方法
    function readLineSync(file) {
    return {
    [Symbol.iterator]() {
    return {
    next() {
    return { done: false };
    },
    return() {
    file.close();
    return { done: true };
    }
    }
    }
    }
    }

    // throw 方法主要配合 Generator 函数使用
  • for…in 与 for…of

    for...in 循环可以遍历数组的键名

    1
    2
    3
    for (let index in myArray) {
    console.log(myArray[index]);
    }

    缺点:
    - 数组的键名是数字,但是 for...in 循环是以字符串作为键名,如 ‘1’, ‘2’
    - for...in 循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键
    - 某些情况下,for...in 循环会以任意顺序遍历键名
    - for...in 主要是为遍历对象而设计的,不适用于遍历数组

    for...of 循环有着同 for...in 一样的简洁语法,没有 for...in 的诸多缺点。不同于 forEach 方法,它可以与 breakcontinuereturn 配合使用,另外它也提供了遍历所有数据结构的统一接口。

Promise 对象

所谓 Promise 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息

Promise 对象有以下三个特点

  1. 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态,pending(进行中),fulfiled(已成功)和 rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,其他任何操作都无法改变这个状态

  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。对象的状态改变只有两种可能:从 pending 变为 fulfiled 和从 pending 变为 rejected。只要这两种情况发生,状态就凝固了。不会再改变,也称为 resolved

  3. 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。另外如果不设置回调函数,Promise 内部抛出的错误不会反应到外部。还有,当处于 pending 状态时,无法得知目前进展到哪一个阶段

  • 基本用法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    // 一个 Promise 实例
    const promise = new Promise(function (resolve, reject) { // 接受一个函数作为参数,函数的参数分别是 resolve 和 reject
    // ... some code
    if (/* 异步操作成功 */) {
    resolve(value); // 将 Promise 对象的状态从”未完成“,变为”成功“,并将异步操作的结果作为参数传递出去
    } else {
    reject(error); // 将 Promise 对象的状态从”未完成“,变为”失败“,并将异步操作报出的错误作为参数传递出去
    }
    });
    // 实例生成后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数
    promise.then(function(value) {
    // success
    }, function(error) {
    // failure
    });

    // Promise 新建后就会立即执行
    let promise = new Promise(function (resolve, reject) {
    console.log('Promise');
    resolve();
    });
    promise.then(function () {
    console.log('resolved');
    });
    console.log('end');
    // Promise
    // end
    // resolved

    // 如果调用 `resolve` 函数和 `reject` 函数时带有参数,那么它们的参数会被传递给回调函数,resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例
    const p1 = new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('fail')), 3000);
    });

    const p2 = new Promise(function (resolve, reject) {
    setTimeout(() => resolve(p1), 1000);
    });

    p2.then(result => console.log(result)).catch(error => console.log(error));
  • Promise.prototype.then()

    为 Promise 实例添加状态改变时的回调函数,该方法的第一个参数是 resolved 状态的回调函数,第二个参数(可选)是 rejected 状态的回调函数

    then 方法返回的是一个新的 Promise 实例,因此可以采用链式写法

    1
    2
    3
    4
    5
    6
    getJSON('/post/1.json').then(
    post => getJSON(post.commentURL);
    ).then(
    comments => console.log('resolved: ', comments),
    err => console.log('rejected: ', err)
    );
  • Promise.prototype.catch()

    Promise.prototype.catch 方法是 .then(null, rejection) 的别名,用于指定发生错误时的回调函数
    跟传统的 try/catch 代码块不同的是,如果没有使用 catch 方法指定错误处理的回掉函数,Promise 对象抛出的错误不会传递到外层代码,只会在内部打印出错误信息,但是不会退出进程,终止脚本执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 这个例子中三个 Promise 对象中任何一个抛出的错误都会被最后一个 catch 捕获
    getJSON('/post/1.json').then(
    post => getJSON(post.commentURL);
    ).then(
    // ...some code
    ).catch(
    // ...处理前面三个 Promise 产生的错误
    );
    // 建议 Promise 对象后面要紧跟 catch 方法,这样可以处理 Promise 内部发生的错误
  • Promise.prototype.finally()

    finally 方法用于指定不管 Promise 对象最后的状态如何,都会执行的操作。而且该方法的回调函数不接受任何参数,所以没法知道前面的 Promise 状态到底是 fulfilled 还是 rejected。所以 finally 方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // finally 本质上是 then 方法的特例
    promise.finally(() => {
    // ...some code
    });
    //等同于
    promise.then(
    result => {
    // ...
    return result;
    },
    error => {
    // ...
    throw error;
    }
    );

    // finally 方法总是会返回原来的值
    Promise.resolve(2).then(() => {}, () => {}); // undefined
    Promise.resolve(2).finally(() => {}); // 2

    Promise.reject(3).then(() => {}, () => {}); // undefined
    Promise.reject(3).then(() => {}, () => {}); // 3
  • Promise.all()

    Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 下面是一个例子,在参数数组中,p1,p2,p3 都是 Promise 对象的实例
    const p = new Promise([p1, p2, p3]);
    // 参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例
    // 只要 p1,p2,p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1,p2,p3 的返回值组成一个数组,传递给 p 的 回调函数
    // 只要 p1,p2,p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1,p2,p3 的返回值组成一个数组,传递给 p 的 回调函数

    // 另外,如果作为参数的 Promise 实例,自己定义了 catch 方法,那么一旦其被 rejected 并不会触发 Promise.all() 的 catch 方法
    const p1 = new Promise((resolve, reject) => {
    resolve('Hello');
    })
    .then(result => result)
    .catch(e => e);

    const p2 = new Promise((resolve, reject) => {
    throw new Error('报错了');
    })
    .then(result => result)
    .catch(e => e);

    Promise.all([p1, p2])
    .then(result => console.log(result))
    .catch(e => console.log(e));
    // ['Hello', Error: 报错了]
    // 反之,上述例子中如果 p2 没有自己的 catch 方法,则会调用 Promise.all() 的 catch 方法
  • Promise.race()

    Promise.race 同样用于将多个 Promise 实例,包装成一个新的 Promise 实例。

    1
    2
    // 和 Promise.all 方法很类似,不过不同的是,p1,p2,p3 之中只要有一个实例率先改变状态,p的状态就会跟着改变,其实例的返回值就会传递给 p 的回调函数
    const p = Promise.race([p1, p2, p3]);
  • Promise.resolve()

    该方法可以将现有的对象转换成 Promise 对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 基本用法
    Promise.resolve('foo'); // 等价于
    new Promise(resolve => resolve('foo'))

    // 参数是一个 Promise 实例,那么 Promise.resolve 将不做任何修改,原封不动地返回这个实例

    // 参数是一个 thenable 对象,Promise.resolve 方法将这个对象转换成 Promise 对象,然后立即执行 thenable 对象的 then 方法
    let thenable = {
    then: function(resolve, reject) {
    resolve(42);
    }
    };
    let p1 = Promise.resolve(thenable);
    p1.then(value => console.log(value)); // 42

    // 参数不具有 then 方法的对象,或根本就不是对象
    const p = Promise.resolve('Hello');
    p.then(value => console.log(value)); // Hello

    // 不带有任何参数,这个例子中立即执行的 Promise 对象,是在本轮“事件循环”的结束之前,而不是下一轮的开始时
    setTimeout(() => {console.log('three');}, 0);
    Promise.resolve().then(() => {console.log('two');});
    console.log('one');
    // one
    // two
    // three
  • Promise.reject()

    Promise.reject(reason) 该方法也会返回一个新的 Promise 实例,该实例的状态为 reject

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const thenable = {
    then(resolve, reject) {
    reject('出错了');
    }
    };
    Promise.reject(thenable)
    .catch(e => {
    console.log(e === thenable);
    })
    // true

Generator 函数

  • 基本概念

    该函数是 ES6 提供的一种异步编程解决方案。从语法上理解,可以把 Generator 函数理解成一个状态机,封装了多个内部状态

    执行 Generator 函数 会返回一个遍历器对象,即该函数不仅是状态机,还是一个遍历器对象生成函数。从返回的遍历器对象中,可以依次遍历 Generator 函数内部的每一个状态

    相对于普通函数,Generator 函数有两个基本特征。一是,function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式,定义不同的内部状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 例子
    function* helloWorldGenerator() {
    yield 'Hello';
    yield 'world';
    return 'ending';
    }
    var hw = helloWorldGenerator();
    hw.next(); // { value: 'Hello', done: false }
    hw.next(); // { value: 'world', done: false }
    hw.next(); // { value: 'ending', done: false }
    hw.next(); // { value: undefined, done: true }
    • yield 表达式

    由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以需要提供一种可以暂停执行的函数。yield 表达式就是暂停标志。且只有调用 next 方法并且内部指针指向该语句时才会执行,即近似于手动的“惰性求值”。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // yield 表达式只能用在 Generator 函数里面,用在其他地方都会报错
    (function () {
    yield 1;
    })() // SyntaxError: Unexpected number

    // yield 表达式如果用在另一个表达式中,必须放在圆括号里面
    function* demo() {
    console.log('Hello' + yield); // SyntaxError: Unexpected identifier
    console.log('Hello' + yield 123); // SyntaxError: Unexpected identifier
    console.log('Hello' + (yield)); // OK
    console.log('Hello' + (yield + 123)); // OK
    }

    // yield 表达式用作函数参数或放在赋值表达式的右边,可以不加括号
    function* demo() {
    foo(yield 'a', yield 'b'); // OK
    let input = yield; // OK
    }
  • 与 Iterator 接口的关系

    任意一个对象的 Symbol.iterator 方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var myIterable = {};
    myIterable[Symbol.iterator] = function* {
    yield 1;
    yield 2;
    yield 3;
    }

    [...myIterable] // [1, 2, 3]

    // 另外 Generator 函数执行后,返回一个遍历器对象,该对象本身也具有 `Symbol.iterator` 属性,执行后返回其自身
    function* gen() {
    // ...
    }

    var g = gen();
    g[Symbol.iterator]() === g // true
  • next 方法的参数

    yield 表达式本身没有返回值,或者说总是返回 undefinednext 方法可以带一个参数,该参数会被当作上一个 yield 表达式的返回值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 一个实例
    function* foo(x) {
    var y = 2 * (yield (x + 1));
    var z = yield (y / 3);
    return (x + y + z);
    }

    var b = foo(5);
    b.next(); // { value: 6, done: false }
    b.next(12) // { value: 8, done: false }
    b.next(13) // { value: 42. done: true}

    // 想要第一次调用 next 方法时,就能够输入值,可以在 Generator 函数外面再包一层
  • for…of 循环

    for...of 循环可以自动遍历 Generator 函数执行时生成的 Iterator 对象,且不需要调用 next 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // 一个实例
    // 这里面一旦 next 方法返回对象的属性为 true,for...of循环就会中止,并不包含该返回对象,所以 return 中的 5 没有被打印出来
    function* foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    return 5;
    }
    for (let v of foo()) {
    console.log(v);
    } // 1, 2, 3, 4
    [...foo()]; // [1, 2, 3, 4]
    Array.from(foo()); // [1, 2, 3, 4]

    // 可以将 Generator 函数加到对象的 Symbol.iterator 属性上面,可以用 for...of 循环遍历该对象
    function* objectEntries() {
    let propKeys = Object.keys(this);

    for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
    }
    }

    let jane = { first: 'jane', last: 'Doe'};

    jane[Symbol.iterator] = objectEntries;

    for (let [key, value] of jane) {
    console.log(`${key}: ${value}`);
    }
    // first: jane
    // last: Doe
  • Generator.prototype.throw()

    Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后再 Generator 函数体内捕获

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    // 第一个实例
    var g = function* () {
    try {
    yield;
    } catch (e) {
    console.log('内部捕获', e);
    }
    }
    var i = g();
    i.next();
    try {
    i.throw('a');
    i.throw('b');
    } catch (e) {
    console.log('外部捕获', e);
    }
    // 内部捕获 a
    // 外部捕获 b
    // 1. 遍历器对象 i 连续抛出两个错误。第一个被 Generator 函数内的 catch 语句捕获,在第二次抛出错误的时候,由于 Generator 函数内部的 catch 语句已经执行过了,不会再捕捉这个错误,所以就被外部的 catch 捕获了
    // 2. throw 方法可以接受一个参数,该参数会被 catch 语句接受
    // 3. 如果 Generator 函数内部没有部署 try...catch 代码块,那么通过 throw 方法抛出的错误,将被外部 try...cathc 代码块捕获
    // 4. 如果 Generator 函数内部和外部,都没有部署 try...catch 代码块,那么程序将报错,直接中断执行
    // Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 体内抛出的错误,也可以被函数体外 catch 捕获

    // 第二个实例
    var gen = function* gen() {
    try {
    yield console.log('a');
    } catch(e) {
    console.log('error');
    }
    yield console.log('b');
    yield console.log('c');
    }
    var g = gen();
    g.next(); // a
    g.throw(); // error b
    g.next(); // c
    // 可以看到,g.throw 方法被捕获后,自动执行了一次 next 方法

    // 第三个实例
    function* g() {
    yield 1;
    console.log('throwing an exception');
    throw new Error('generator broke!');
    yield 2;
    yield 3;
    }
    function log(generator) {
    var v;
    console.log('starting generator');
    try {
    v = generator.next();
    console.log('第一次运行next方法', v);
    } catch (err) {
    console.log('捕捉错误', err);
    }
    try {
    v = generator.next();
    console.log('第二次运行next方法', v);
    } catch (err) {
    console.log('捕捉错误', err);
    }
    try {
    v = generator.next();
    console.log('第三次运行next方法', v);
    } catch (err) {
    console.log('捕捉错误', err);
    }
    console.log('caller done');
    }
    log(g());
    // starting generator
    // 第一次运行next方法 { value: 1, done: false }
    // throwing an exception
    // 捕捉错误 Error: generator broke!
    // 第三次运行next方法 { value: undefined, done: true }
    // caller done
  • Generator.prototype.return()

    Generator 函数返回的遍历器对象,还有一个 return 方法,可以返回给定的值,并且终结遍历 Generator 函数

    如果 Generator 函数内部有 try...catch 代码块,那么 return 方法会推迟到 finally 代码执行完再执行,而且应该被执行的 yield 表达式会被忽略,直接之后后面的那次

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 实例
    function* gen() {
    yield 1;
    yield 2;
    yield 3;
    }
    var g = gen();
    g.next(); // { value: 1, done: false }
    g.return('foo'); // { value: 'foo', done: true } return方法不带参数的话,value 为undefined
    g.next(); // { value: undefined, done: true }
  • next(), throw(), return() 的共同点

    1. next(),实际上是将 yield 表达式换成一个值
    2. throw(),实际上是将 yield 表达式换成一个 throw 语句
    3. return(),实际上是将 yield 表达式替换成一个 return 语句
  • yield* 表达式

    用来在一个 Generator 函数里面执行另一个 Generator 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // 一个实例
    function* foo() {
    yield 'a';
    yield 'b';
    }
    function* bar() {
    yield 'x';
    yield* foo();
    yield 'y';
    }
    // 等同于
    function* bar() {
    yield 'x';
    yield 'a';
    yield 'b';
    yield 'y';
    }
    // 等同于
    function* bar() {
    yield 'x';
    for (let v of foo()) {
    yield v;
    }
    yield 'y';
    }

    // foo 中有 return 语句的情况
    function* foo() {
    yield 'a';
    yield 'b';
    return c;
    }
    function* bar() {
    yield 'x';
    var v = yield* foo();
    console.log(v);
    yield 'y';
    }
    var a = bar();
    a.next() // {value: "x", done: false}
    a.next() // {value: "a", done: false}
    a.next() // {value: "b", done: false}
    a.next() // c {value: "y", done: false}
    a.next() // {value: undefined, done: true}
  • 作为对象属性的 Generator 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let obj = {
    * myGeneratorMethod() {
    // ...
    }
    }
    // 等同于
    let obj = {
    myGeneratorMethod: function* () {
    // ...
    }
    }
  • Generator 函数的 this

    Generator 函数总是返回一个遍历器,这个遍历器是 Generator 函数的实例同时也继承了 Generator 函数的 prototype 对象上的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 一个实例
    function* g() {}
    g.prototype.hello = function () {
    return 'hi';
    };
    let obj = g();
    obj instanceof g // true
    obj.hello() // hi!

    // Generator 函数也不能和 `new` 命令一起用
    function* F() {
    yield this.x = 2;
    yield this.y = 3;
    }
    new F(); // TypeError: F is not a constructor