0%

ES6总结

let和const

为了加强对变量生命周期的控制,ES6引入了块级作用域

块级作用域存在于:

  • 函数内部
  • 块中(字符{和}之间的区域)

特点:

  1. 不会被提升
  2. 重复声明报错
  3. 不绑定全局作用域

letconst的区别:const用于声明常量,其值一旦被设定不能再被修改,否则会报错。const不允许修改绑定,但是允许修改值。

1
2
3
4
5
6
7
8
9
10
const data = {
value: 1
}

// 没有问题
data.value = 2;
data.num = 3;

// 报错
data = {}; // Uncaught TypeError: Assignment to constant variable.

即常量data储存的是一个地址,这个地址指向一个对象。不可变的是这个地址,但对象是可变的,所以依然可以为其添加新属性。

如果真的想让对象冻结,应该使用Object.freeze方法。

为了保持兼容性,var命令和function命令声明的全局变量,依旧是全局对象的属性;另一方面规定,let命令、 const命令、class 命令声明的全局变量,不属于全局对象的属性。也就是说,从ES6开始,全局变量将逐步与全局对象的属性脱钩。

1
2
3
4
5
6
var a = 1;
// 如果在Node的REPL环境,可以写成global.a
// 或者采用通用方法,写成this.a
window.a // 1
let b = 1;
window.b // undefined

临时死区(TDZ):

JS引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到var声明),要么将声明放在TDZ中(遇到let和const声明)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移出,然后方可访问。

循环中的块级作用域:

1
2
3
4
5
6
7
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 3

解决方案:

立即调用函数表达式(IIFE)也能传入参数。因为函数内定义的任何函数可以访问外部函数的传入参数和变量(闭包),所以立即调用的函数表达式(IIFE)可以用于“锁定”值并有效地保存状态。

1
2
3
4
5
6
7
8
9
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = (function(i){
return function() {
console.log(i);
}
}(i))
}
funcs[0](); // 0

ES6

块级作用域的出现,实际上使得获得广泛应用的立即执行匿名函数(IIFE)不再必要了。

1
2
3
4
5
6
7
8
9
10
// IIFE写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
1
2
3
4
5
6
7
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0

那么当使用let的时候底层到底是怎么做的呢?

简单的来说,就是在 for (let i = 0; i < 3; i++) 中,即圆括号之内建立一个隐藏的作用域,这就可以解释为什么:

1
2
3
4
5
6
7
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc

然后每次迭代循环时都创建一个新变量,并以之前迭代中同名变量的值将其初始化。这样对于下面这样一段代码

1
2
3
4
5
6
7
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0

就相当于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 伪代码
(let i = 0) {
funcs[0] = function() {
console.log(i)
};
}

(let i = 1) {
funcs[1] = function() {
console.log(i)
};
}

(let i = 2) {
funcs[2] = function() {
console.log(i)
};
};

当执行函数的时候,根据词法作用域就可以找到正确的值,其实你也可以理解为let声明模仿了闭包的做法来简化循环过程。

1
2
3
4
5
6
7
var funcs = [];
for (const i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // Uncaught TypeError: Assignment to constant variable.

尝试对const声明的i修改,所以会报错。

1
2
3
4
5
6
7
8
var funcs = [], object = {a: 1, b: 1, c: 1};
for (var key in object) {
funcs.push(function(){
console.log(key)
});
}

funcs[0]()

‘c’

改成let或const -> ‘a’ 每次迭代不会修改已有的绑定,而是会创建一个新的绑定。

babel编译:

1
2
3
4
if (false) {
let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined
1
2
3
4
if (false) {
var _value = 1;
}
console.log(value);
1
2
3
4
5
let value = 1;
{
let value = 2;
}
value = 3;
1
2
3
4
5
var value = 1;
{
var _value = 2;
}
value = 3;
1
2
3
4
5
6
7
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0
1
2
3
4
5
6
7
8
9
10
11
12
var funcs = [];

var _loop = function _loop(i) {
funcs[i] = function () {
console.log(i);
};
};

for (var i = 0; i < 10; i++) {
_loop(i);
}
funcs[0](); // 0

解构赋值

ES6内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于 undefined,默认值是不会生效的。

1
2
3
4
var [x = 1] = [undefined];
x // 1
var [x = 1] = [null];
x // null

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

1
2
3
4
function f() {
console.log('aaa');
}
let [x = f()] = [1];

等价于

1
2
3
4
5
6
let x;
if ([1][0] === undefined) {
x = f();
} else {
x = [1][0];
}

对象的解构赋值,由于对象没有顺序,所以变量必须与属性同名才能取到值。

如果变量名与属性名不一致,必须写成下面这样。

1
2
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

变量声明与赋值是一体的,上面这种写法,其实只声明赋值了baz,所以调用foo的话,会报错foo is not defined

如果要将一个已经声明的变量用于解构赋值,必须非常小心。

1
2
3
4
// 错误的写法
var x;
{x} = {x: 1};
// SyntaxError: syntax error

上面代码的写法会报错,因为JavaScript引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免JavaScript将其解释为代码块,才能解决这个问题。

1
2
// 正确的写法
({x} = {x: 1});

解构赋值允许,等号左边的模式之中,不放置任何变量名。

不能使用圆括号的情况

  1. 变量声明语句中,不能带有圆括号。
  2. 函数参数中,模式不能带有圆括号。
  3. 赋值语句中,不能将整个模式,或嵌套模式中的一层,放在圆括号之中。

可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。

1
2
3
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确

箭头函数

和普通函数区别:

  1. 没有this

    箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值。

    这就意味着如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this。

  2. 没有arguments

    如果想要得到箭头函数的参数可以通过以下的方式:

    1
    let nums = (...nums) => nums;
  3. 不能通过 new 关键字调用

  4. 没有 new.target

    new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

  5. 没有原型

  6. 没有 super

最后,关于箭头函数,引用 MDN 的介绍就是:

An arrow function expression has a shorter syntax than a function expression and does not have its own this, arguments, super, or new.target. These function expressions are best suited for non-method functions, and they cannot be used as constructors.

method的定义:

A method is a function which is a property of an object.

对象属性中的函数就被称之为 method,那么 non-method 就是指不被用作对象属性中的函数了,可是为什么说箭头函数更适合 non-method 呢?

1
2
3
4
5
6
7
8
9
10
11
var obj = {
i: 10,
b: () => console.log(this.i, this),
c: function() {
console.log( this.i, this)
}
}
obj.b();
// undefined Window
obj.c();
// 10, Object {...}

因为它内部this的指向原因,当使用obj.b()的时候,很明显我们希望b方法里面的this指向obj,但是它却指向了obj所在上下文中的this(即window),违背了我们的意愿,所以箭头函数不适合作为对象的方法。这也是为什么vue组件里面方法不允许使用箭头函数的原因。

Symbol

  1. Symbol 值通过 Symbol 函数生成,使用 typeof,结果为 “symbol”

  2. Symbol 函数前不能使用 new 命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。

  3. instanceof 的结果为 false

    1
    2
    var s = Symbol('foo');
    console.log(s instanceof Symbol); // false
  4. Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

  5. 如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值。

    1
    2
    3
    4
    5
    6
    7
    const obj = {
    toString() {
    return 'abc';
    }
    };
    const sym = Symbol(obj);
    console.log(sym); // Symbol(abc)
  6. Symbol 函数的参数只是表示对当前 Symbol 值的描述,相同参数的 Symbol 函数的返回值是不相等的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 没有参数的情况
    var s1 = Symbol();
    var s2 = Symbol();

    console.log(s1 === s2); // false

    // 有参数的情况
    var s1 = Symbol('foo');
    var s2 = Symbol('foo');

    console.log(s1 === s2); // false
  7. Symbol 值不能与其他类型的值进行运算,会报错。

  8. Symbol 值可以显式转为字符串。

    1
    2
    3
    4
    var sym = Symbol('My symbol');

    console.log(String(sym)); // 'Symbol(My symbol)'
    console.log(sym.toString()); // 'Symbol(My symbol)'
  9. Symbol 值可以作为标识符,用于对象的属性名,可以保证不会出现同名的属性。

  10. Symbol 作为属性名,该属性不会出现在 for…in、for…of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var obj = {};
    var a = Symbol('a');
    var b = Symbol('b');

    obj[a] = 'Hello';
    obj[b] = 'World';

    var objectSymbols = Object.getOwnPropertySymbols(obj);

    console.log(objectSymbols);
    // [Symbol(a), Symbol(b)]
  11. 如果我们希望使用同一个 Symbol 值,可以使用 Symbol.for。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。

    1
    2
    3
    var s1 = Symbol.for('foo');
    var s2 = Symbol.for('foo');
    s1 === s2 // true
  12. Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。

    1
    2
    3
    4
    5
    var s1 = Symbol.for("foo");
    console.log(Symbol.keyFor(s1)); // "foo"

    var s2 = Symbol("foo");
    console.log(Symbol.keyFor(s2) ); // undefined

Symbol.for与Symbol

Symbol.for()与Symbol()这两种写法,都会生成新的Symbol。它们的区别是,前者会被登记全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的Symbol类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用 Symbol.for(“cat”)30次,每次都会返回同一个Symbol值,但是调用 Symbol(“cat”) 30次,会返回30个不同的Symbol值。

内置Symbol值

  1. Symbol.hasInstance

    当调用instanceof这个方法时,实际调用对象的Symbol.hasInstance属性,这个属性指向一个内部方法。

    1
    2
    3
    4
    5
    6
    class MyClass {
    [Symbol.hasInstance](foo) {
    return foo instanceof Array;
    }
    }
    console.log([1, 2, 3] instanceof new MyClass()) // true
  2. Symbol.isConcatSpreadable

    对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象使用Array.prototype.concat() 时,是否可以展开。

    Symbol.isConcatSpreadable属性等于trueundefined时,表示可以展开。

    类数组对象也可以展开,但它的Symbol.isConcatSpreadable属性默认为false,必须手动打开。

    1
    2
    3
    4
    5
    6
    7
    8
    let obj = {
    length: 2,
    0: 'c',
    1: 'd'
    };
    ['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']
    obj[Symbol.isConcatSpreadable] = true;
    ['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
  3. Symbol.iterator

    对象的Symbol.iterator属性,指向该对象的默认遍历器方法。

    1
    2
    3
    4
    5
    6
    7
    var myIterable = {};
    myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
    };
    [...myIterable] // [1, 2, 3]

    对象进行for…of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var myIterable = {};
    myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
    };
    for (let key of myIterable) {
    console.log(key);
    }
    //1
    //2
    //3
  4. Symbol.toPrimitive

    对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。

    Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。

    • Number:该场合需要转成数值

    • String:该场合需要转成字符串

    • Default:该场合可以转成数值,也可以转成字符串

      一般情况下,+连接运算符传入的参数是default,而对于乘法等算数运算符传入的是number。对于String(str),${str}等情况,传入的参数是string

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      let obj = {
      [Symbol.toPrimitive](hint) {
      switch (hint) {
      case 'number':
      return 123;
      case 'string':
      return 'str';
      case 'default':
      return 'default';
      default:
      throw new Error();
      }
      }
      };
      2 * obj // 246
      3 + obj // '3default'
      obj == 'default' // true
      String(obj) // 'str'

    Symbol.toPrimitive在类型转换方面,优先级是最高的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let ab = {
    valueOf() {
    return 0;
    },
    toString() {
    return '1';
    },
    [Symbol.toPrimitive]() {
    return 2;
    }
    }
    console.log(1+ab); // 3
    console.log('1'+ab); // 12

defineProperty 与 proxy

defineProperty

Object.defineProperty(obj, prop, descriptor)

descriptor:

  1. 数据描述符:configurableenumerablevaluewritable
  2. 存取描述符:configurableenumerablegetset

configurable:

当且仅当该属性的configurable为true时,该属性描述符才能够被改变,也能够被删除。默认为false。

enumerable:

当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。

proxy

Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。

1
let p = new Proxy(target, handler)

target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。

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
let onWatch = (obj, setBind, getLogger) => {
let handler = {
set(target, property, value, receiver) {
setBind(value, property)
return Reflect.set(target, property, value)
},
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
}
}
return new Proxy(obj, handler)
}

let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 控制台输出:监听到属性a改变
p.a // 'a' = 2

自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。

当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了。

1
var proxy = new Proxy(target, handler);

1.get()

get方法用于拦截某个属性的读取操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = {
name: "zj"
};

var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
throw ReferenceError("Property \"" + property + "\" does not exist.");
}
}
});

console.log(proxy.name) //"张三"

//console.log(proxy.age) //抛出一个错误
//如果没有这个拦截函数,访问不存在的属性,只会返回undefined

get方法可以继承

1
2
3
4
5
6
7
8
9
var proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET' + propertyKey);
return target[propertyKey];
}
});

var obj = Object.create(proto);
console.log(obj.xxx);

2.set()

set方法用来拦截某个属性的赋值操作

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
var validator = {
set: function(obj, prop, value) {
if(prop === 'age') {
if(!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if(value > 200) {
throw new RangeError('The age seems invalid');
}
}

//对于age以外的属性,直接保存
obj[prop] = value;
}
};

var person = new Proxy({}, validator);

person.age = 100;

console.log(person.age); //100

// person.age = 'young'; //报错

// person.age = 300; //报错

//利用set方法还是可以数据绑定,即每当对象发生变化时,会自动更新DOM

3.apply()

apply方法可以拦截函数的调用、call和apply的操作

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
var target = function () { return 'I am the target'; };

var handler = {
apply: function () {
return 'I am the proxy';
}
};

var p = new Proxy(target, handler);

console.log(p()); //I am the proxy
//上述代码p是Proxy的实例,当它作为函数调用时p(),就会被apply方法拦截,返回一个字符串


var twice = {
apply (target, ctx, args) {
return Reflect.apply(...arguments) * 2;
}
};
function sum (left, right) {
return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30

//上面代码中,每当执行proxy函数,就会被apply方法拦截

//另外 直接调用Reflect.apply方法,也会被拦截
Reflect.apply(proxy, null, [9, 10]) // 38

4.has()

用来拦截HasProperty操作,即判断对象是否有某个属性时,这个方法会生效。

典型的操作就是in运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};

var target = { _prop: 'foo', prop: 'foo'};

var proxy = new Proxy(target, handler);

console.log('_prop' in proxy);

上面代码中,如果原对象的属性名的第一个字符是下划线,proxy.has就会返回 false,从而不会被 in 运算符发现。

如果原对象不可配置或者禁止扩展,这时 has 拦截会报错。

1
2
3
4
5
6
7
8
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has: function(target, prop) {
return false;
}
});
'a' in p // TypeError is thrown

has拦截的是HasProperty操作,而不是HasOwnProperty操作,即 has 方法不判断一个属性是对象自身的属性,还是继承的属性。由于for…in操作内部也会用到 HasProperty 操作,所以has方法在for…in循环时也会生效。

5.construct()

construct方法用于拦截new命令

1
2
3
4
5
6
7
8
9
10
var p = new Proxy(function() {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return {value: args[0] * 10};
}
});

new p(1).value
//"called: 1"
//10

construct方法返回的必须是一个对象,否则会报错。

6.deleteProperty()

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property

deleteProperty方法拦截了delete操作符,删除第一个字符为下划线的属性会报错。

7.defineProperty()

defineProperty 方法拦截了 Object.defineProperty 操作

1
2
3
4
5
6
7
8
9
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar'
// TypeError: proxy defineProperty handler returned false for property '"foo"'

上面代码中,defineProperty方法返回false,导致添加新属性会抛出错误。

8.getOwnPropertyDescriptor()

getOwnPropertyDescriptor 方法拦截 Object.getOwnPropertyDescriptor ,返回一个属性描述对象或者 undefined 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var handler = {
getOwnPropertyDescriptor(target, key) {
if (key[0] === '_') {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
}
};
var target = {
_foo: 'bar',
baz: 'tar'
};
var proxy = new Proxy(target, handler);
console.log(Object.getOwnPropertyDescriptor(proxy, 'wat'))
// undefined
console.log(Object.getOwnPropertyDescriptor(proxy, '_foo'))
// undefined
console.log(Object.getOwnPropertyDescriptor(proxy, 'baz'))
// { value: 'tar', writable: true, enumerable: true, configurable: true }

上面代码中,handler.getOwnPropertyDescriptor 方法对于第一个字符为下划线的属性名会返回 undefined 。

9.getPrototypeOf()

getPrototypeOf 方法主要用来拦截 Object.getPrototypeOf() 运算符,以及其他一些操作:

  • Object.prototype.proto
  • Object.prototype.isPrototypeOf()
  • Object.getPrototypeOf()
  • Reflect.getPrototypeOf()
  • instanceof 运算符
1
2
3
4
5
6
7
var proto = {};
var p = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
Object.getPrototypeOf(p) === proto // true

上面代码中, getPrototypeOf 方法拦截 Object.getPrototypeOf() ,返回 proto 对象。

10.isExtensible()

isExtensible方法拦截Object.isExtensible 操作。

1
2
3
4
5
6
7
8
9
var p = new Proxy({}, {
isExtensible: function(target) {
console.log("called");
return true;
}
});
Object.isExtensible(p)
// "called"
// true

上面代码设置了 isExtensible 方法,在调用 Object.isExtensible 时会输出 called

这个方法有一个强限制,如果不能满足下面的条件,就会抛出错误。

1
Object.isExtensible(proxy) === Object.isExtensible(target)

下面是一个例子。

1
2
3
4
5
6
7
var p = new Proxy({}, {
isExtensible: function (target) {
return false;
}
});

Object.isExtensible(p) // 报错

11.ownKeys()

ownKeys 方法用来拦截 Object.keys() 操作。

1
2
3
4
5
6
7
8
9
let target = {};
let handler = {
ownKeys(target) {
return ['hello', 'world'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// [ 'hello', 'world' ]

上面代码拦截了对于target对象的Object.keys() 操作,返回预先设定的数组。

12.preventExtensions()

preventExtensions 方法拦截 Object.preventExtensions() 。该方法必须返回一个布尔值。

这个方法有一个限制,只有当 Object.isExtensible(proxy) 为 false (即不可扩展)时, proxy.preventExtensions 才能返回 true ,否则会报错。

13.setPrototypeOf()

setPrototypeOf 方法主要用来拦截 Object.setPrototypeOf 方法

1
2
3
4
5
6
7
8
9
10
var handler = {
setPrototypeOf (target, proto) {
throw new Error('Changing the prototype is forbidden');
}
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
proxy.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden

上面代码中,只要修改 target 的原型对象,就会报错

14.Proxy.revocable()

Proxy.revocable方法返回一个可取消的Proxy实例

1
2
3
4
5
6
7
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked

Proxy.revocable方法返回一个对象,该对象的 proxy 属性是 Proxy 实例, revoke 属性是一个函数,可以取消 Proxy 实例。上面代码中,当执行 revoke 函数之后,再访问 Proxy 实例,就会抛出一个错误。

Reflect

设计的目的

  1. 将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty ),放到Reflect对象上。

  2. 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false 。

  3. 让Object操作都变成函数行为。

    1
    2
    3
    4
    // 老写法
    'assign' in Object // true
    // 新写法
    Reflect.has(Object, 'assign') // true
  4. Reflect对象的方法和Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var loggedObj = new Proxy(obj, {
    get(target, name) {
    console.log('get', target, name);
    return Reflect.get(target, name);
    },
    deleteProperty(target, name) {
    console.log('delete' + name);
    return Reflect.deleteProperty(target, name);
    },
    has(target, name) {
    console.log('has' + name);
    return Reflect.has(target, name);
    }
    });

    上面代码中,每一个 Proxy 对象的拦截操作( get 、 delete 、 has ),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。

1.Reflect.set(target,name,value,receiver)

查找并返回 target 对象的 name 属性,如果没有该属性,则返回 undefined 。如果 name 属性部署了读取函数,则读取函数的this绑定 receiver 。

2.Reflect.get(target,name,receiver)

设置 target 对象的 name 属性等于 value 。如果 name 属性设置了赋值函数,则赋值函数的 this 绑定 receiver 。

3.Reflect.apply(target,thisArg,args)

等同于 Function.prototype.apply.call(fun,thisArg,args) 。一般来说,如果要绑定一个函数的this对象,可以这样写 fn.apply(obj, args) ,但是如果函数定义了自己的 apply 方法,就只能写成 Function.prototype.apply.call(fn, obj, args) ,采用Reflect对象可以简化这种操作。

4.Reflect.has(target,name)

等同于 name in obj

5.Reflect.construct(target,args)

等同于 new target(…args) ,这提供了一种不使用 new ,来调用构造函数的方法。

6.Reflect.deleteProperty(obj, name)

等同于 delete obj[name]

7.Reflect.getPrototypeOf(obj)

读取对象的 proto 属性,对应 Object.getPrototypeOf(obj)

8.Reflect.setPrototypeOf(obj, newProto)

设置对象的 proto 属性,对应 Object.setPrototypeOf(obj,newProto)

9.Reflect.defineProperty(target,name,desc)

10.Reflect.ownKeys(target)

11.Reflect.isExtensible(target)

12.Reflect.preventExtensions(target)

13.Reflect.getOwnPropertyDescriptor(target, name)

Reflect.set()、Reflect.defineProperty()、Reflect.freeze()、Reflect.seal()和 Reflect.preventExtensions() 返回一个布尔值,表示操作是否成功。它们对应的Object方法,失败时都会抛出错误。

1
2
3
4
// 失败时抛出错误
Object.defineProperty(obj, name, desc);
// 失败时返回false
Reflect.defineProperty(obj, name, desc);

上面代码中, Reflect.defineProperty方法的作用与Object.defineProperty是一样的,都是为对象定义一个属性。但是,Reflect.defineProperty方法失败时,不会抛出错误,只会返回false 。

set和map

Set

ES6 新增的一种新的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。

Set 本身是一种构造函数,用来生成 Set 数据结构。

1
new Set([iterable])

举个例子:

1
2
3
4
5
6
7
8
9
10
const s = new Set()
[1, 2, 3, 4, 3, 2, 1].forEach(x => s.add(x))

for (let i of s) {
console.log(i) // 1 2 3 4
}

// 去重数组的重复对象
let arr = [1, 2, 3, 2, 1, 1]
[... new Set(arr)] // [1, 2, 3]

Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。

向 Set 加入值的时候,不会发生类型转换,所以5"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是NaN等于自身,而精确相等运算符认为NaN不等于自身。

1
2
3
4
5
6
7
8
9
10
11
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}

let set1 = new Set()
set1.add(5)
set1.add('5')
console.log([...set1]) // [5, "5"]
  • Set 实例属性

    • constructor: Set构造函数

    • size:元素数量

      1
      2
      3
      4
      let set = new Set([1, 2, 3, 2, 1])

      console.log(set.length) // undefined
      console.log(set.size) // 3
  • Set 实例方法

    • 操作方法

      • add(value):新增,相当于 array里的push

      • delete(value):存在即删除集合中value

      • has(value):判断集合中是否存在 value

      • clear():清空集合


        1
        2
        3
        4
        5
        6
        7
        let set = new Set()
        set.add(1).add(2).add(1)

        set.has(1) // true
        set.has(3) // false
        set.delete(1)
        set.has(1) // false

        Array.from 方法可以将 Set 结构转为数组

        1
        2
        3
        4
        5
        6
        const items = new Set([1, 2, 3, 2])
        const array = Array.from(items)
        console.log(array) // [1, 2, 3]
        // 或
        const arr = [...items]
        console.log(arr) // [1, 2, 3]
    • 遍历方法(遍历顺序为插入顺序)

      • keys():返回一个包含集合中所有键的迭代器

      • values():返回一个包含集合中所有值得迭代器

      • entries():返回一个包含Set对象中所有元素得键值对迭代器

      • forEach(callbackFn, thisArg):用于对集合成员执行callbackFn操作,如果提供了 thisArg 参数,回调中的this会是这个参数,没有返回值

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        let set = new Set([1, 2, 3])
        console.log(set.keys()) // SetIterator {1, 2, 3}
        console.log(set.values()) // SetIterator {1, 2, 3}
        console.log(set.entries()) // SetIterator {1, 2, 3}

        for (let item of set.keys()) {
        console.log(item);
        } // 1 2 3
        for (let item of set.entries()) {
        console.log(item);
        } // [1, 1] [2, 2] [3, 3]

        set.forEach((value, key) => {
        console.log(key + ' : ' + value)
        }) // 1 : 1 2 : 2 3 : 3
        console.log([...set]) // [1, 2, 3]

        Set 可默认遍历,默认迭代器生成函数是 values() 方法

        1
        Set.prototype[Symbol.iterator] === Set.prototype.values	// true

        所以, Set可以使用 map、filter 方法

        1
        2
        3
        4
        5
        6
        let set = new Set([1, 2, 3])
        set = new Set([...set].map(item => item * 2))
        console.log([...set]) // [2, 4, 6]

        set = new Set([...set].filter(item => (item >= 4)))
        console.log([...set]) //[4, 6]

        因此,Set 很容易实现交集(Intersect)、并集(Union)、差集(Difference)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        let set1 = new Set([1, 2, 3])
        let set2 = new Set([4, 3, 2])

        let intersect = new Set([...set1].filter(value => set2.has(value)))
        let union = new Set([...set1, ...set2])
        let difference = new Set([...set1].filter(value => !set2.has(value)))

        console.log(intersect) // Set {2, 3}
        console.log(union) // Set {1, 2, 3, 4}
        console.log(difference) // Set {1}

        由于Set结构没有键名,只有键值(或者说键名和键值是同一个值),所以 key 方法和 value 方法的行为完全一致。

WeakSet

WeakSet 对象允许你将弱引用对象储存在一个集合中

WeakSet 与 Set 的区别:

  • WeakSet 只能储存对象引用,不能存放值,而 Set 对象都可以
  • WeakSet 对象中储存的对象值都是被弱引用的,即==垃圾回收机制不考虑 WeakSet 对该对象的应用==,如果没有其他的变量或属性引用这个对象值,则这个对象将会被垃圾回收掉(不考虑该对象还存在于 WeakSet 中),所以,WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了),==WeakSet 对象是无法被遍历的==(ES6 规定 WeakSet 不可遍历),也没有办法拿到它包含的所有元素

属性:

  • constructor:构造函数,任何一个具有 Iterable 接口的对象,都可以作参数

    1
    2
    3
    const arr = [[1, 2], [3, 4]]
    const weakset = new WeakSet(arr)
    console.log(weakset)

    将数组作为WeakSet构造函数的参数,数组的成员会自动成为WeakSet的成员,所以数组的成员也必须要是对象。

屏幕快照 2019-09-29 上午10.41.43

方法:

  • add(value):在WeakSet 对象中添加一个元素value
  • has(value):判断 WeakSet 对象中是否包含value
  • delete(value):删除元素 value
  • clear():清空所有元素,注意该方法已废弃
1
2
3
4
5
6
7
8
9
10
11
12
var ws = new WeakSet()
var obj = {}
var foo = {}

ws.add(window)
ws.add(obj)

ws.has(window) // true
ws.has(foo) // false

ws.delete(window) // true
ws.has(window) // false

用处:

加入其中的元素不会算入引用计数,所以当其他地方没有对对象的引用之后,就可以删除了,不会造成内存泄漏。

储存DOM节点,而不用担心这些节点从文档移除时,会引发内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
const foos = new WeakSet()
class Foo {
constructor() {
foos.add(this)
}
method () {
if (!foos.has(this)) {
throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
}
}
}

上面代码保证了 Foo 的实例方法,只能在 Foo 的实例上调用。这里使用WeakSet的好处是,foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑 foos ,也不会出现内存泄漏。

Map

集合 与 字典 的区别:

  • 共同点:集合、字典 可以储存不重复的值
  • 不同点:集合 是以 [value, value]的形式储存元素,字典 是以 [key, value] 的形式储存
1
2
3
4
5
6
7
8
const m = new Map()
const o = {p: 'haha'}
m.set(o, 'content')
m.get(o) // content

m.has(o) // true
m.delete(o) // true
m.has(o) // false

JavaScript的对象(Object),本质上是键值对的集合(Hash结构),但是传统上只能用字符串当作键

它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。如果你需要“键值对”的数据结构,Map比Object更合适。

任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数,例如:

1
2
3
4
5
6
7
8
9
10
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3

如果读取一个未知的键,则返回undefined

1
2
new Map().get('asfddfsasadf')
// undefined

注意,==只有对同一个对象的引用,Map 结构才将其视为同一个键==。这一点要非常小心。

1
2
3
4
const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined

上面代码的setget方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get方法无法读取该键,返回undefined

由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefinednull也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let map = new Map();

map.set(-0, 123);
map.get(+0) // 123

map.set(true, 1);
map.set('true', 2);
map.get(true) // 1

map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3

map.set(NaN, 123);
map.get(NaN) // 123

Map 的属性及方法

属性:

  • constructor:Map构造函数

  • size:返回字典中所包含的元素个数

    1
    2
    3
    4
    5
    6
    const map = new Map([
    ['name', 'An'],
    ['des', 'JS']
    ]);

    map.size // 2

操作方法:

  • set(key, value):向字典中添加新元素,set 方法返回的是Map本身,因此可以采用链式写法
  • get(key):通过键查找特定的数值并返回,找不到的话返回undefined
  • has(key):判断字典中是否存在键key
  • delete(key):通过键 key 从字典中移除对应的数据
  • clear():将这个字典中的所有元素删除

遍历方法

  • Keys():将字典中包含的所有键名以迭代器形式返回
  • values():将字典中包含的所有数值以迭代器形式返回
  • entries():返回所有成员的迭代器
  • forEach():遍历字典的所有成员
1
2
3
4
5
6
const map = new Map([
['name', 'An'],
['des', 'JS']
]);
console.log(map.entries()) // MapIterator {"name" => "An", "des" => "JS"}
console.log(map.keys()) // MapIterator {"name", "des"}

Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

1
2
map[Symbol.iterator] === map.entries
// true

Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。

对于 forEach ,看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const reporter = {
report: function(key, value) {
console.log("Key: %s, Value: %s", key, value);
}
};

let map = new Map([
['name', 'An'],
['des', 'JS']
])
map.forEach(function(value, key, map) {
this.report(key, value);
}, reporter);
// Key: name, Value: An
// Key: des, Value: JS

在这个例子中, forEach 方法的回调函数的 this,就指向 reporter

与其他数据结构的相互转换

  1. Map 转 Array

    1
    2
    const map = new Map([[1, 1], [2, 2], [3, 3]])
    console.log([...map]) // [[1, 1], [2, 2], [3, 3]]
  2. Array 转 Map

    1
    2
    const map = new Map([[1, 1], [2, 2], [3, 3]])
    console.log(map) // Map {1 => 1, 2 => 2, 3 => 3}
  3. Map 转 Object

    因为 Object 的键名都为字符串,而Map 的键名为对象,所以转换的时候会把非字符串键名转换为字符串键名。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function mapToObj(map) {
    let obj = Object.create(null)
    for (let [key, value] of map) {
    obj[key] = value
    }
    return obj
    }
    const map = new Map().set('name', 'An').set('des', 'JS')
    mapToObj(map) // {name: "An", des: "JS"}
  4. Object 转 Map

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function objToMap(obj) {
    let map = new Map()
    for (let key of Object.keys(obj)) {
    map.set(key, obj[key])
    }
    return map
    }

    objToMap({'name': 'An', 'des': 'JS'}) // Map {"name" => "An", "des" => "JS"}
  5. Map 转 JSON

    1
    2
    3
    4
    5
    6
    function mapToJson(map) {
    return JSON.stringify([...map])
    }

    let map = new Map().set('name', 'An').set('des', 'JS')
    mapToJson(map) // [["name","An"],["des","JS"]]
  6. JSON 转 Map

    1
    2
    3
    4
    5
    function jsonToStrMap(jsonStr) {
    return objToMap(JSON.parse(jsonStr));
    }

    jsonToStrMap('{"name": "An", "des": "JS"}') // Map {"name" => "An", "des" => "JS"}

WeakMap

WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的。

WeakMap 的设计目的在于,键名是对象的弱引用(垃圾回收机制不将该引用考虑在内),所以其所对应的对象可能会被自动回收。当对象被回收后, WeakMap 自动移除对应的键值对。典型应用是,一个对应DOM元素的 WeakMap 结构,当某个DOM元素被清除,其所对应的 WeakMap 记录就会自动被移除。基本上, WeakMap 的专用场合就是,它的键所对应的对象,可能会在将来消失。 WeakMap 结构有助于防止内存泄漏。

属性:

  • constructor:构造函数

方法:

  • has(key):判断是否有 key 关联对象
  • get(key):返回key关联对象(没有则则返回 undefined)
  • set(key):设置一组key关联对象
  • delete(key):移除 key 的关联对象
1
2
3
4
5
6
7
8
9
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();

myWeakmap.set(myElement, {timesClicked: 0});

myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);

上面代码中, myElement 是一个DOM节点,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在WeakMap里,对应的键名就是 myElement 。一旦这个DOM节点删除,该状态就会自动消失,不存在内存泄漏风险。

WeakMap的另一个用处是部署私有属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let _counter = new WeakMap();
let _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
if (counter < 1) return;
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
let c = new Countdown(2, () => console.log('DONE'));
c.dec()
c.dec()
// DONE

上面代码中,Countdown类的两个内部属性 _counter 和 _action ,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。

总结

  • Set
    • 成员唯一、无序且不重复
    • [value, value],键值与键名是一致的(或者说只有键值,没有键名)
    • 可以遍历,方法有:add、delete、has
  • WeakSet
    • 成员都是对象
    • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏
    • 不能遍历,方法有add、delete、has
  • Map
    • 本质上是键值对的集合,类似集合
    • 可以遍历,方法很多可以跟各种数据格式转换
  • WeakMap
    • 只接受对象作为键名(null除外),不接受其他类型的值作为键名
    • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
    • 不能遍历,方法有get、set、has、delete

扩展:Object与Set、Map

  1. Object 与 Set

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // Object
    const properties1 = {
    'width': 1,
    'height': 1
    }
    console.log(properties1['width']? true: false) // true

    // Set
    const properties2 = new Set()
    properties2.add('width')
    properties2.add('height')
    console.log(properties2.has('width')) // true
  2. Object 与 Map

JS 中的对象(Object),本质上是键值对的集合(hash 结构)

1
2
3
4
5
const data = {};
const element = document.getElementsByClassName('App');

data[element] = 'metadata';
console.log(data['[object HTMLCollection]']) // "metadata"

但当以一个DOM节点作为对象 data 的键,对象会被自动转化为字符串[Object HTMLCollection],所以说,Object 结构提供了 字符串-值 对应,Map则提供了 值-值 的对应。

Iterator

Iterator接口的目的就是为所有数据结构提供了一种统一的访问机制,即for…of循环。当使用这个for…of循环遍历某种数据结构时,该循环会自动去寻找Iterator接口。

  • 定义:为各种不同的数据结构提供统一的访问机制。

  • 原理:创建一个指针指向首个成员,按照次序使用next()指向下一个成员,直接到结束位置(数据结构只要部署Iterator接口就可完成遍历操作)

  • Iterator的作用有三个:

    1. 为各种数据结构,提供一个统一的、简便的访问接口;
    2. 使得数据结构的成员能够按某种次序排列;
    3. ES6创造了一种新的遍历命令for…of循环,Iterator接口主要供for…of消费。
  • 部署:默认部署在Symbol.iterator(具备此属性被认为可遍历的iterable)

  • 遍历器对象

    • next():下一步操作,返回{ done, value }(必须部署)
    • return()for-of提前退出调用,返回{ done: true }
    • throw():不使用,配合Generator函数使用

在ES6中有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构

1
2
3
4
5
6
7
8
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

console.log(iter); // Object [Array Iterator] {}
console.log(iter.next());// { value: 'a', done: false }
console.log(iter.next());// { value: 'b', done: false }
console.log(iter.next());// { value: 'c', done: false }
console.log(iter.next());// { value: undefined, done: true }

对象(Object)之所以没有默认部署Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作Map结构使用,ES5没有Map结构,而ES6原生提供了。一个对象如果要有可被 for…of 循环调用的Iterator接口,就必须在 Symbol.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
class rangeIterator {
constructor(start, end) {
this.value = start;
this.end = end;
}
[Symbol.iterator]() {
return this;
}

next() {
let value = this.value;
if (value < this.end) {
this.value++;
return {
done: false,
value: value
};
} else return {
done: true,
value: undefined
};
}
}

function range(start, end) {
return new rangeIterator(start, end);
}

for (let value of range(0, 3)) console.log(value);

上述是一个类部署的Iterator接口的写法,Symbol.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
let obj = {
data: ['hello', 'world'],
[Symbol.iterator]() {
let self = this,
index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
} else return {
value: undefined,
done: true
};
}
}
}
}

for (let value of obj) {
console.log(value)
}

对于类似数组的对象(==存在数值键名和length属性==),部署Iterator接口,有一个简便方法,就是 Symbol.iterator 方法直接引用数组的Iterator接口。

1
2
3
4
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
[...document.querySelectorAll('div')] // 可以执行了
1
2
3
4
5
6
7
8
9
10
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'
}

注意,普通对象部署数组的 Symbol.iterator 方法,并无效果

任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组,对于那些没有部署 Iterator 接口的类似数组的对象,扩展运算符就无法转为真正的数组。

1
2
3
4
5
6
7
8
let arrayLike = {  
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr = [...arrayLike];
// Uncaught TypeError: arrayLike is not iterable

上面代码中,arrayLike是一个类似数组的对象,但是没有部署Iterator接口,扩展运算符就会报错。这时,可以改为使用Array.from方法将arrayLike转为真正的数组。

1
2
3
4
5
6
7
8
9
let iterable = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3,
};
for (let item of Array.from(iterable)) {
console.log(item); // a b c
}

for of

  • 定义:调用Iterator接口产生遍历器对象(for-of内部调用数据结构的Symbol.iterator())
  • 遍历字符串:for-in获取索引for-of获取(可识别32位UTF-16字符)
  • 遍历数组:for-in获取索引for-of获取
  • 遍历对象:for-in获取for-of需自行部署
  • 遍历Set:for-of获取 => for (const v of set)
  • 遍历Map:for-of获取键值对 => for (const [k, v] of map)
  • 遍历类数组:包含length的对象Arguments对象NodeList对象(无Iterator接口的类数组可用Array.from()转换)
  • 计算生成数据结构:ArraySetMap
    • keys():返回遍历器对象,遍历所有的键
    • values():返回遍历器对象,遍历所有的值
    • entries():返回遍历器对象,遍历所有的键值对
  • for-in区别
    • 有着同for-in一样的简洁语法,但没有for-in那些缺点、
    • 不同于forEach(),它可与breakcontinuereturn配合使用
    • 提供遍历所有数据结构的统一操作接口

调用Iterator的场合

1.解构赋值

对数组和Set结构进行解构赋值时,会默认调用 Symbol.iterator 方法。

1
2
3
4
5
6
7
8
9
let set = new Set().add('a').add('b').add('c');

let [x,y] = set;

// x='a'; y='b'

let [first, ...rest] = set;

// first='a'; rest=['b','c'];

2.扩展运算符

扩展运算符(…)也会调用默认的iterator接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 例一

var str = 'hello';

[...str] // ['h','e','l','l','o']

// 例二

let arr = ['b', 'c'];

['a', ...arr, 'd']

// ['a', 'b', 'c', 'd']

上面代码的扩展运算符内部就调用Iterator接口。实际上,这提供了一种简便机制,可以将任何部署了Iterator接口的数据结构,转为数组。也就是说,只要某个数据结构部署了Iterator接口,就可以对它使用扩展运算符,将其转为数组。

1
let arr = [...iterable];

3.yield*

yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()(比如 new Map([[‘a’,1],[‘b’,2]]))
  • Promise.all()
  • Promise.race()

字符串的Iterator接口

字符串是一个类似数组的对象,也原生具有Iterator接口。

1
2
3
4
5
6
7
var someString = "hi";
typeof someString[Symbol.iterator]
// "function"
var iterator = someString[Symbol.iterator]();
iterator.next() // { value: "h", done: false }
iterator.next() // { value: "i", done: false }
iterator.next() // { value: undefined, done: true }

可以覆盖原生的 Symbol.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
//字符串的Iterator接口
let str = new String("hi");

console.log([...str]); // [ 'h', 'i' ]

str[Symbol.iterator] = function () {
return {
next: function () {
if (this._first) {
this._first = false;
return {
value: "bye",
done: false
};
} else {
return {
done: true
};
}
},
_first: true
};
};

console.log([...str]); //[ 'bye' ]
//字符串str的 Symbol.iterator 方法被修改了,所以扩展运算符
//( ... )返回的值变成了 bye ,而字符串本身还是 hi

Iterator接口与Generator函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
// 或者采用下面的简洁写法
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};
for (let x of obj) {
console.log(x);
}
// hello
// world

for…of循环

1.数组

数组原生具备iterator接口, for…of 循环本质上就是调用这个接口产生的遍历器,可以用下面的代码证明。

1
2
3
4
5
6
7
8
9
const arr = ['red', 'green', 'blue'];
let iterator = arr[Symbol.iterator]();
for(let v of arr) {
console.log(v); // red green blue
}
for(let v of iterator) {
console.log(v); // red green blue
}
//以上这俩个写法是等价的

JavaScript原有的 for…in 循环,只能获得对象的键名,不能直接获取键值。ES6提供 for…of 循环,允许遍历获得键值。

1
2
3
4
5
6
7
let arr = ['a', 'b', 'c', 'd'];
for (let a in arr) {
console.log(a); // 0 1 2 3
}
for (let a of arr) {
console.log(a); // a b c d
}

上面代码表明, for…in 循环读取键名, for…of 循环读取键值。如果要通过 for…of 循环,获取数组的索引,可以借助数组实例的 entries 方法和 keys 方法,参见《数组的扩展》章节。

for…of 循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟 for…in 循环也不一样。

1
2
3
4
5
6
7
8
let arr = [3, 5, 7];
arr.foo = 'hello';
for (let i in arr) {
console.log(i); // "0", "1", "2", "foo"
}
for (let i of arr) {
console.log(i); // "3", "5", "7"
}

上面代码中, for…of 循环不会返回数组 arr 的 foo 属性。

2.Set和Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (let e of engines) {
console.log(e);
}
// Gecko
// Trident
// Webkit
let es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (let [name, value] of es6) {
console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262

Set结构遍历时,返回的是一个值,而Map结构遍历时,返回的是一个数组,该数组的两个成员分别为当前Map成员的键名和键值。

3.计算生成的数据结构

有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6的数组、Set、Map都部署了以下三个方法,调用后都返回遍历器对象。

entries() 返回一个遍历器对象,用来遍历 [键名, 键值] 组成的数组。对于数组,键名就是索引值;对于Set,键名与键值相同。Map结构的iterator接口,默认就是调用entries方法。

keys() 返回一个遍历器对象,用来遍历所有的键名。

values() 返回一个遍历器对象,用来遍历所有的键值。

这三个方法调用后生成的遍历器对象,所遍历的都是计算生成的数据结构。

1
2
3
4
5
6
7
8
9
let arr = ['a', 'b', 'c'];

for (let pair of arr.entries()) {
console.log(pair);
}
console.log(arr.entries()); //Object [Array Iterator] {}
// [0, 'a']
// [1, 'b']
// [2, 'c']

4.类似数组的对象

类似数组的对象包括好几类。下面是 for…of 循环用于字符串、DOM NodeList对象、arguments对象的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 字符串
let str = "hello";
for (let s of str) {
console.log(s); // h e l l o
}
// DOM NodeList对象
let paras = document.querySelectorAll("p");
for (let p of paras) {
p.classList.add("test");
}
// arguments对象
function printArgs() {
for (let x of arguments) {
console.log(x);
}
}
printArgs('a', 'b');
// 'a'
// 'b'

并不是所有类似数组的对象都具有iterator接口,一个简便的解决方法,就是使用Array.from方法将其转为数组

1
2
3
4
5
6
7
8
9
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
// 报错
for (let x of arrayLike) {
console.log(x);
}
// 正确
for (let x of Array.from(arrayLike)) {
console.log(x);
}

5.对象

对于普通的对象, for…of 结构不能直接使用,会报错,必须部署了iterator接口后才能使用。但是,这样情况下, for…in 循环依然可以用来遍历键名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let es6 = {
edition: 6,
committee: "TC39",
standard: "ECMA-262"
};
for (e in es6) {
console.log(e);
}
// edition
// committee
// standard
for (e of es6) {
console.log(e);
}
// TypeError: es6 is not iterable

上面代码表示,对于普通的对象, for…in 循环可以遍历键名, for…of 循环会报错

一种解决方法是,使用 Object.keys 方法将对象的键名生成一个数组,然后遍历这个数组。

1
2
3
4
5
for (let key of Object.keys(someObject)) {

console.log(key + ": " + someObject[key]);

}

另一个方法是使用Generator函数将对象重新包装一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let obj = {
a: 1,
b: 2,
c: 3
}

function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
for (let [key, value] of entries(obj)) {
console.log(key, "->", value);
}
// a -> 1
// b -> 2
// c -> 3

与其他遍历方法的比较

最原始的就是for循环,但是太麻烦,因此数组提供内置的forEach方法,但是这种方法的弊端在于无法跳出forEach 循环,break命令或return命令都不能奏效。

for…in

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

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

for…in循环有几个缺点:

  • 数组的键名是数字,但是for…in循环是以字符串作为键名“0”、“1”、“2”等等。
  • for…in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键
  • 某些情况下,for…in循环会以任意顺序遍历键名。

总之, for…in 循环主要是为遍历对象而设计的,不适用于遍历数组。

for…of 循环相比上面几种做法,有一些显著的优点:

有着同for...in一样的简洁语法,但是没有for...in那些缺点。不同用于forEach方法,它可以与break、continue和return配合使用。提供了遍历所有数据结构的统一操作接口

下面是一个使用break语句,跳出 for…of 循环的例子。

1
2
3
4
5
for (var n of fibonacci) {
if (n > 1000)
break;
console.log(n);
}

上面的例子,会输出斐波纳契数列小于等于1000的项。如果当前项大于1000,就会使用break语句跳出 for…of 循环。

Generator

  • 每次调用next(),指针就从函数头部上次停下的位置开始执行,直到遇到下一个yield命令return语句为止
  • 函数内部可不用yield命令,但会变成单纯的暂缓执行函数(还是需要next()触发)
  • yield命令是暂停执行的标记,next()是恢复执行的操作
  • yield命令用在另一个表达式中必须放在圆括号
  • yield命令用作函数参数或放在赋值表达式的右边,可不加圆括号
  • yield命令本身没有返回值,可认为是返回undefined
  • yield命令表达式为惰性求值,等next()执行到此才求值
  • 函数调用后生成遍历器对象,此对象的Symbol.iterator是此对象本身
  • 在函数运行的不同阶段,通过next()从外部向内部注入不同的值,从而调整函数行为
  • 首个next()用来启动遍历器对象,后续才可传递参数
  • 想首次调用next()时就能输入值,可在函数外面再包一层
  • 一旦next()返回对象的donetruefor-of遍历会中止且不包含该返回对象
  • 函数内部部署try-finally且正在执行try,那么return()会导致立刻进入finally,执行完finally以后整个函数才会结束
  • 函数内部没有部署try-catchthrow()抛错将被外部try-catch捕获
  • throw()抛错要被内部捕获,前提是必须至少执行过一次next()
  • throw()被捕获以后,会附带执行下一条yield命令
  • 函数还未开始执行,这时throw()抛错只可能抛出在函数外部

概要:

Generator函数是一个状态机,封装了多个内部状态

执行Generator函数会返回一个遍历器对象,也就是说Generator除了是一个状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。

特征:

  • function关键字和函数名之间有个*号
  • 函数体内部使用 yield 语句,定义不同的内部状态
1
2
3
4
5
6
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
let hw = helloWorldGenerator();

Generator函数和普通函数不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)。

1
2
3
4
5
6
7
8
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
  • 第一次调用,Generator函数开始执行,直到遇到第一个 yield 语句为止。 next 方法返回一个对象,它的 value 属性就是当前 yield 语句的值hello, done 属性的值false,表示遍历还没有结束。
  • 第二次调用,Generator函数从上次 yield 语句停下的地方,一直执行到下一个 yield 语句。next 方法返回的对象的 value 属性就是当前 yield 语句的值world, done属性的值false,表示遍历还没有结束。
  • 第三次调用,Generator函数从上次 yield 语句停下的地方,一直执行到 return 语句(如果没有return语句,就执行到函数结束)。 next 方法返回的对象的 value 属性,就是紧跟在 return 语句后面的表达式的值(如果没有 return 语句,则 value 属性的值为undefined), done 属性的值true,表示遍历已经结束。
  • 第四次调用,此时Generator函数已经运行完毕, next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。

yield语句

由于Generator函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield语句就是暂停标志。

遍历器对象的 next 方法的运行逻辑如下。

(1)遇到 yield 语句,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值。

(2)下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 语句。

(3)如果没有再遇到新的 yield 语句,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值。

(4)如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined 。

需要注意的是, yield 语句后面的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的“惰性求值”(LazyEvaluation)的语法功能。

1
2
3
function* gen() {
yield 123 + 456;
}

上面的函数在调用时不会立即求值,只会在next方法将指针移到这一句时,才会求值。

yield和return语句的区别?

  • 相同点:
    • 都能返回紧跟在语句后面那个表达式的值
  • 不同点:
    • 每次遇到yield时,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备这个功能
    • 一个函数最多只能执行一次return语句,但是可以执行多次yield语句

Generator函数可以不用yield,这样就变成了一个单纯的暂缓执行函数

1
2
3
4
5
6
7
function* f() {
console.log('执行了!')
}
let generator = f();
setTimeout(function () {
generator.next()
}, 2000);

yield不能用在普通函数中,否则会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let arr = [1, [[2, 3], 4], [5, 6]];

let flat = function* (a) {
for (let i = 0; i < a.length; i++) {
let item = a[i];
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
}
};

for (let f of flat(arr)) {
console.log(f);
}
// 1, 2, 3, 4, 5, 6

yield语句如果用在一个表达式之中,必须放在圆括号里面。用作函数参数或者赋值表达式右边可以不加括号

与Iterator接口的关系

由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口。

1
2
3
4
5
6
7
8
let myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};

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

上面代码中,Generator函数赋值给 Symbol.iterator 属性,从而使得 myIterable 对象具有了Iterator接口,可以被 … 运算符遍历了。Generator函数执行后,返回一个遍历器对象。该对象本身也具有 Symbol.iterator 属性,执行后返回自身。

Generator函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。

1
2
3
4
5
function* gen() {
// some code
}
let g = gen();
console.log(g[Symbol.iterator]() === g); // true

next方法的参数

yield句本身没有返回值,或者说返回值为undefined。next方法可以带一个参数,这个参数就会被当作上一个yield语句的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
function* foo(x) {
let y = 2 * (yield(x + 1));
let z = yield(y / 3);
return (x + y + z);
}
let a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
let b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

第二次运行 next 方法的时候不带参数,导致y的值等于 2 * undefined (即 NaN ),除以3以后还是 NaN ,因此返回对象的 value 属性也等于 NaN 。第三次运行 Next 方法的时候不带参数,所以 z 等于 undefined ,返回对象的 value 属性等于 5 + NaN + undefined ,即 NaN 。

如果向 next 方法提供参数,返回结果就完全不一样了。上面代码第一次调用 b 的 next 方法时,返回 x+1 的值6;第二次调用 next 方法,将上一次 yield 语句的值设为12,因此 y 等于24,返回 y / 3 的值8;第三次调用 next 方法,将上一次 yield 语句的值设为13,因此 z 等于13,这时 x 等于

5, y 等于24,所以 return 语句的值等于42。

注意,由于 next 方法的参数表示上一个 yield 语句的返回值,所以第一次使用 next 方法时,不能带有参数。V8引擎直接忽略第一次使用 next 方法时的参数,只有从第二次使用 next 方法开始,参数才是有效的。从语义上讲,第一个 next 方法用来启动遍历器对象,所以不用带有参数。

for…of

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

1
2
3
4
5
6
7
8
9
10
11
12
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5

一旦next方法的返回对象的done属性为true,for…of循环就会终止。

for…of 循环、扩展运算符( … )、解构赋值和 Array.from 方法内部调用的,都是遍历器接口。这意味着,它们可以将Generator函数返回的Iterator对象,作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
[...numbers()] // [1, 2]
Array.from(numbers()) // [1, 2]
let [x, y] = numbers();
x // 1
y // 2
for (let n of numbers()) {
console.log(n)
}
// 1
// 2

Generator.prototype.throw()

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
let i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

如果Generator内部没有部署try…catch代码块,那么抛出的错误直接被外部catch代码块捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let g = function* () {
while (true) {
yield;
console.log('内部捕获', e);
}
};
let i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 a

throw 方法被捕获以后,会附带执行下一条 yield 语句。也就是说,会附带执行一次 next 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
let gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
let g = gen();
g.next() // a
g.throw() // b
g.next() // c

这种函数体内捕获错误的机制,大大方便了对错误的处理。多个 yield 语句,可以只用一个try…catch 代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在Generator函数内部写一次 catch 语句就可以了。

Generator函数体外抛出的错误,可以在函数体内捕获;反过来,Generator函数体内抛出的错误,也可以被函数体外的 catch 捕获。

一旦Generator执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用 next 方法,将返回一个 value 属性等于 undefined 、 done 属性等于 true 的对象,即JavaScript引擎认为这个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
function* g() {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
yield 2;
yield 3;
}

function log(generator) {
let v;
console.log('starting generator');
try {
v = generator.next();
console.log('第一次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第二次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第三次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done

上面代码一共三次运行 next 方法,第二次运行的时候会抛出错误,然后第三次运行的时候,Generator函数就已经结束了,不再执行下去了。

Generator.prototype.return()

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

1
2
3
4
5
6
7
8
9
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }

如果Generator函数内部有 try…finally 代码块,那么 return 方法会推迟到 finally 代码块执行完再执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* numbers() {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
let g = numbers()
g.next() // { done: false, value: 1 }
g.next() // { done: false, value: 2 }
g.return(7) // { done: false, value: 4 }
g.next() // { done: false, value: 5 }
g.next() // { done: true, value: 7 }

上面代码中,调用 return 方法后,就开始执行 finally 代码块,然后等到 finally 代码块执行完,再执行 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
function* inner() {
yield 'hello!';
}

function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
let gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一个遍历器对象
gen.next().value // "close"
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
let gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"

任何数据结构只要有Iterator接口,就可以被yield*遍历

1
2
3
4
5
6
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"

如果被代理的Generator函数有 return 语句,那么就可以向代理它的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
function* foo() {
yield 2;
yield 3;
return "foo";
}

function* bar() {
yield 1;
let v = yield* foo();
console.log("v: " + v);
yield 4;
}
let it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}

上面代码在第四次调用 next 方法的时候,屏幕上会有输出,这是因为函数 foo 的 return 语句,向函数 bar 提供了返回值。

作为对象属性的Generator函数

1
2
3
4
5
let obj = {
* myGeneratorMethod() {
···
}
};

上面代码中, myGeneratorMethod 属性前面有一个星号,表示这个属性是一个Generator函数。

它的完整形式如下,与上面的写法是等价的。

1
2
3
4
5
let obj = {
myGeneratorMethod: function* () {
// ···
}
};

Generator函数的this

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

1
2
3
4
5
6
7
8
function* g() {}
g.prototype.hello = function () {
return 'hi!';
}

let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'

上面代码表明,Generator函数 g 返回的遍历器 obj ,是 g 的实例,而且继承了 g.prototype 。但是,如果把 g 当作普通的构造函数,并不会生效,因为 g 返回的总是遍历器对象,而不是 this 对象。

1
2
3
4
5
function* g() {
this.a = 11;
}
let obj = g();
obj.a // undefined

那么如何让Generator函数返回一个正常的对象实例

  1. 让其绑定一个空对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function* F() {
    this.a = 1;
    yield this.b = 2;
    yield this.c = 3;
    }
    let obj = {};
    let f = F.call(obj);

    console.log(f.next());//{ value: 2, done: false }
    console.log(f.next());//{ value: 3, done: false }
    console.log(f.next());//{ value: undefined, done: true }
    console.log(obj.a);//1
    console.log(obj.b);//2
    console.log(obj.c);//3
  2. 绑定F.prototype

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function* F() {
    this.a = 1;
    yield this.b = 2;
    yield this.c = 3;
    }
    let f = F.call(F.prototype);
    f.next(); // Object {value: 2, done: false}
    f.next(); // Object {value: 3, done: false}
    f.next(); // Object {value: undefined, done: true}
    f.a // 1
    f.b // 2
    f.c // 3
  3. 再将 F 改成构造函数,就可以对它执行 new 命令了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function* gen() {
    this.a = 1;
    yield this.b = 2;
    yield this.c = 3;
    }
    function F() {
    return gen.call(gen.prototype);
    }
    let f = new F();
    f.next(); // Object {value: 2, done: false}
    f.next(); // Object {value: 3, done: false}
    f.next(); // Object {value: undefined, done: true}
    f.a // 1
    f.b // 2
    f.c // 3

应用

  1. Generator与状态机

    1
    2
    3
    4
    5
    6
    7
    8
    let ticking = true;
    let clock = function() {
    if (ticking)
    console.log('Tick!');
    else
    console.log('Tock!');
    ticking = !ticking;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    let clock = function* () {
    while (true) {
    console.log('Tick!');
    yield;
    console.log('Tock!');
    yield;
    }
    };

    上面的Generator实现与ES5实现对比,可以看到少了用来保存状态的外部变量 ticking ,这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。Generator之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。

  1. 控制流管理

    如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    step1(function (value1) {
    step2(value1, function(value2) {
    step3(value2, function(value3) {
    step4(value3, function(value4) {
    // Do something with value4
    });
    });
    });
    });

    采用Promise改写上面的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Q.fcall(step1)
    .then(step2)
    .then(step3)
    .then(step4)
    .then(function (value4) {
    // Do something with value4
    }, function (error) {
    // Handle any error from step1 through step4
    })
    .done();

    上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量Promise的语法。Generator函数可以进一步改善代码运行流程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function* longRunningTask() {
    try {
    let value1 = yield step1();
    let value2 = yield step2(value1);
    let value3 = yield step3(value2);
    let value4 = yield step4(value3);
    // Do something with value4
    } catch (e) {
    // Handle any error from step1 through step4
    }
    }
  2. 部署iterator接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = {
foo: 3,
bar: 7
};
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7

myObj是一个普通对象,通过iterEntries函数,就有了iterator接口。也就是说,可以在任意对象上部署next方法。

  1. 作为数据结构

    Generator可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为

    Generator函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数

    组的接口。

    1
    2
    3
    4
    5
    function* doStuff() {
    yield fs.readFile.bind(null, 'hello.txt');
    yield fs.readFile.bind(null, 'world.txt');
    yield fs.readFile.bind(null, 'and-such.txt');
    }

    上面代码就是依次返回三个函数,但是由于使用了Generator函数,导致可以像处

    理数组那样,处理这三个返回的函数。

    1
    2
    3
    for (task of doStuff()) {
    // task是一个函数,可以像回调函数那样使用它
    }

Promise

概念

Promise 翻译过来就是承诺的意思,这个承诺会在未来有一个确切的答复,并且该承诺有三种状态,分别是:

  1. 等待中(pending)
  2. 完成了 (resolved)
  3. 拒绝了(rejected)

这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再次改变

1
2
3
4
5
new Promise((resolve, reject) => {
resolve('success')
// 无效
reject('reject')
})

当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的

1
2
3
4
5
6
new Promise((resolve, reject) => {
console.log('new Promise')
resolve('success')
})
console.log('finifsh')
// new Promise -> finifsh

Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装

1
2
3
4
5
6
7
8
Promise.resolve(1)
.then(res => {
console.log(res) // => 1
return 2 // 包装成 Promise.resolve(2)
})
.then(res => {
console.log(res) // => 2
})

当然了,Promise 也很好地解决了回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:

1
2
3
4
5
6
7
8
ajax(url)
.then(res => {
console.log(res)
return ajax(url1)
}).then(res => {
console.log(res)
return ajax(url2)
}).then(res => console.log(res))

前面都是在讲述 Promise 的一些优点和特点,其实它也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。

Promise是异步编程的解决方案,是一个容器,里面保存着未来才会结束的事件的结果。

Promise的俩个特点:

  • 对象的状态不受外界影响
  • 一旦状态改变,就不会再变
1
2
3
4
5
6
7
8
let p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
let p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2.then(result => console.log(result)).catch(error => console.log(error))
// Error: fail

上面代码中, p1 是一个Promise,3秒之后变为 rejected 。 p2 的状态在1秒之后改变, resolve 方法返回的是 p1 。此时,由于 p2 返回的是另一个Promise,所以后面的 then 语句都变成针对后者( p1 )。又过了2秒, p1 变为 rejected ,导致触发 catch 方法指定的回调函数。

Promise.prototype.then()

Promise实例具有 then 方法,也就是说, then 方法是定义在原型对象Promise.prototype上的。它的作用是为Promise实例添加状态改变时的回调函数。前面说过, then 方法的第一个参数是Resolved状态的回调函数,第二个参数(可选)是Rejected状态的回调函数。then 方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即 then 方法后面再调用另一个 then 方法。

Promise.prototype.catch()

Promise.prototype.catch 方法是 .then(null, rejection) 的别名,用于指定发生错误时的回调函数。

1
2
3
4
5
p.then((val) => console.log("fulfilled:", val))
.catch((err) => console.log("rejected:", err));
// 等同于
p.then((val) => console.log(fulfilled:", val))
.then(null, (err) => console.log("rejected:", err));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});

// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});

第二种写法要好于第一种写法,理由是第二种写法可以捕获前面 then 方法执行中的错误,也更接近同步的写法( try/catch )。因此,建议总是使用 catch 方法,而不使用 then 方法的第二个参数。跟传统的 try/catch 代码块不同的是,如果没有使用 catch 方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。

Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个 catch 语句捕获。

Promise.all()

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

let p = Promise.all([p1, p2, p3]);

上面代码中, Promise.all 方法接受一个数组作为参数, p1 、 p2 、 p3 都是Promise对象的实例,如果不是,就会先调用下面讲到的 Promise.resolve 方法,将参数转为Promise实例,再进一步处理。Promise.all 方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例。

p 的状态由 p1 、 p2 、 p3 决定,分成两种情况。

(1)只有 p1 、 p2 、 p3 的状态都变成 fulfilled , p 的状态才会变成 fulfilled ,此时 p1 、 p2 、 p3 的返回值组成一个数组,传递给 p 的回调函数。

(2)只要 p1 、 p2 、 p3 之中有一个被 rejected , p 的状态就变成 rejected ,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。

Promise.race()

Promise.race 方法同样是将多个Promise实例,包装成一个新的Promise实例。

let p = Promise.race([p1,p2,p3]);

上面代码中,只要 p1 、 p2 、 p3 之中有一个实例率先改变状态, p 的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给 p 的回调函数。

Promise.race 方法的参数与 Promise.all 方法一样,如果不是Promise实例,就会先调用下面讲到的 Promise.resolve 方法,将参数转为Promise实例,再进一步处理。

Promise.resolve()

参数分成四种情况:

  1. 参数是一个Promise实例

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

  2. 参数是一个thenable对象

    thenable 对象指的是具有 then 方法的对象,比如下面这个对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let thenable = {
    then: function(resolve, reject) {
    resolve(42);
    }
    };
    let p1 = Promise.resolve(thenable);
    p1.then(function(value) {
    console.log(value); // 42
    });

    Promise.resolve 方法会将这个对象转为Promise对象,然后就立即执行 thenable 对象的 then 方法。

  3. 参数不是具有 then 方法的对象,或根本就不是对象

    如果参数是一个原始值,或者是一个不具有 then 方法的对象,则 Promise.resolve 方法返回一个新的Promise对象,状态为 Resolved。

    1
    2
    3
    4
    5
    let p = Promise.resolve('Hello');
    p.then(function (s){
    console.log(s)
    });
    // Hello
  4. 不带有任何参数

    Promise.resolve 方法允许调用时不带参数,直接返回一个 Resolved 状态的Promise对象。所以,如果希望得到一个Promise对象,比较方便的方法就是直接调用 Promise.resolve 方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    setTimeout(function () {
    console.log('three');
    }, 0);
    Promise.resolve().then(function () {
    console.log('two');
    });
    console.log('one');
    // one
    // two
    // three

    需要注意的是,立即 resolve 的Promise对象,是在本轮“事件循环”(eventloop)的结束时,而不是在下一轮“事件循环”的开始时

    上面代码中, setTimeout(fn, 0) 在下一轮“事件循环”开始时执行, Promise.resolve() 在本轮“事件循环”结束时执行, console.log(‘one’) 则是立即执行,因此最先输出。

Promise.reject()

Promise.reject(reason) 方法也会返回一个新的Promise实例,该实例的状态为 rejected 。它的参数用法与 Promise.resolve 方法完全一致。

俩个有用的附加方法

done()

Promise对象的回调链,不管以 then 方法或 catch 方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。因此,我们可以提供一个 done 方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。

1
2
3
4
5
6
7
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function (reason) {
//抛出一个全局错误
setTimeout(() => { throw reason }, 0);
});
};

从上面代码可见, done 方法的使用,可以像 then 方法那样用,提供 Fulfilled 和 Rejected 状态的回调函数,也可以不提供任何参数。但不管怎样, done 都会捕捉到任何可能出现的错误,并向全局抛出。

finally()

finally 方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与 done 方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

1
2
3
4
5
6
7
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};

异步操作和Async

身为大自然的前端搬运工,为了能更好的搬运代码,咱们应该去理解前端中比较重要的异步调用部分。在说异步之前,我想先介绍generator函数,promise对象和async函数。

Generator函数

generator函数是一种异步编程的解决方案,语法会与传统函数有所不同。

在《ES6标准入门》中是这么介绍generator函数函数的,从语法上,首先可以把它理解成一个状态机,封装了多个内部状态。执行generator函数会返回一个遍历器对象,所以generator函数还是一个遍历器对象生成函数,可以通过它依次遍历generator函数内部的每一个状态。

形式上,generator函数有两个特征:

  1. function命令与函数名之间有一个星号。
  2. 函数体内部使用yield语句定义不同内部状态。
1
2
3
4
5
6
7
8
9
10
function* helloGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
let hw = helloGenerator(); //执行该函数
hw.next() //{value: 'hello', done: false}
hw.next() //{value: 'world', done: false}
hw.next() //{value: 'ending', done: true}
hw.next() //{value: 'undefine', done: true}

上面的函数就是一个例子helloGenerator函数内定义了3个状态:hello、world、return语句

generator函数的调用方法与普通函数类似,但是调用generator函数之后,只是返回一个指向内部状态的指针,通过每次调用next方法没事的指针向下一个状态移动,函数会执行一直到下个yield语句(或return语句)为止。就是说generator函数函数是分段执行的,yield语句是暂停执行标记,而next方法会恢复执行。而每次调用next都会返回yield后的语句的内容,若是函数会先执行(这就与异步相似),然后再返回函数执行的返回结果。

接下来说下next的运行逻辑:

  1. 遇到yield语句就暂停执行后面的操作,并紧跟在yield后面的表达式的值作为返回的对象的value属性值
  2. 下一次调用next方法时再继续往下执行,知道遇到下一条yield
  3. 如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式值作为返回的对象的value属性值
  4. 如果没有return语句,也会执行但是返回的value里面是undefined

需要注意的是只有当我们调用next才会执行后续语句,所以很大程度上来说,generator函数的后续操作,都需要人为手动的控制,但是我们会在后续的讨论中说道co模块和thunk函数,关于自启动的说明。

但是我们通过向next传递一个参数这样子它会被当做上一条yield语句的返回值。我们要注意不能在普通函数中使用yield语句,不然会报错。而且yield语句如果用在一个表达式中,必须放在圆括号里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function* f() {
for (let i = 0; true; i++) {
let result = yield i;
console.log('result:' + result);
if (result) {
i = -1;
}
}
}

let g = f();
g.next() // {value: 0, done: false}
// result:undefined

g.next() // {value: 1, done: false}
// result:undefined

g.next(true) // {value: 0, done: false}
// result:true

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
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//实例一
let promise = new Promise(function (resolve, reject) {
console.log('in');
let x = true;
if (x) {
resolve();
} else {
reject();
}
})
promise.then(function () {
return 21;
if (s) {

}
console.log('success');
}, function () {
console.log('error');
})

//实例二
let promise = new Promise(function (resolve, reject) {
console.log('in');
let x = true;
if (x) {
resolve();
} else {
reject();
}
})
promise.then(function () {
return 21; //21 resolve出去到下一个then
if (s) {

}
console.log('success');
}).then(function (a) {
console.log(a)
}).catch(function () {
console.log('error');
})
//in 21

//实例三
let promise = new Promise(function (resolve, reject) {
console.log('in');
let x = true;
if (x) {
resolve();
} else {
reject();
}
})
promise.then(function () {
console.log('success');
}, function () {
console.log('error');
})
//in success

//实例四
let promise = new Promise(function (resolve, reject) {
console.log('in');
let x = false;
if (x) {
resolve();
} else {
reject();
}
})
promise.then(function () {
console.log('success');
}, function () {
console.log('error');
})
//in error

//实例五
let promise = new Promise(function (resolve, reject) {
console.log('in');
let x = false;
if (x) {
resolve();
} else {
reject();
}
})
promise.then(function () {
console.log('success1');
}).then(function () {
console.log('success2');
}).catch(function () {
console.log('error');
})
//in error

//实例六
let promise = new Promise(function (resolve, reject) {
console.log('in');
let x = false;
if (x) {
resolve();
} else {
reject();
}
})
promise.then(function () {
console.log('success1');
}).then(function () {
console.log('success2');
}, function () {
console.log('error2');
})
//in error2

//实例七
let promise = new Promise(function (resolve, reject) {
console.log('in');
let x = false;
if (x) {
resolve();
} else {
reject();
}
})
promise.then(function () {
console.log('success1');
}, function () {
console.log('error1');
}).then(function () {
console.log('success2');
}, function () {
console.log('error2');
})
//in
//error1
//success2

在上述中一共有七个实例,第一个和第二个很类似,但是第一个会在实例浏览器中报错,而第二个不会,因为catch会捕获报错。虽然在promise对象中我们可以通过操作去决定我们到底执行resolved状态还是rejected状态,但是这是相对的,使用then的第二个参数和使用catch是一样,都会被当做rejected状态,所以为了代码的健壮性,我更加建议使用catch。

但是看到第七个实例它的输出,你会发现,好像和预想的不一样,所以这就是要注意的地方,then和catch返回的都是promise对象,所以可以在后续中执行resolved状态。在异步操作中一定要记住这一点。

promise对象的错误是有冒泡性质的,会一直向后传递,直到被捕获为止,也就是说,错误总是会被下一个catch语句或者then的第二个参数捕获。这也就解释实例七的结果。在promise对象抛出的错误会一直冒泡到最外层,如果没有捕获,但是在谷歌浏览器中会抛出到最外层,只是不会影响后续操作。

我们需要理解当调用 Promise 的 then(..) 会自动创建一个新的 Promise 从调用返回,而其中的promise是怎么产生的。

promise.resolve()会返回一个新的promise对象,且其状态为resolved。

当调用promise.resolve的时候会对对第一个参数转换为一个promise,当参数不是promise的值时,会将其展开提取一个非类promise的最终值。与then类似,如果调用的是then通过return返回一个值,那么该值也会类似将其展开转换为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
27
28
29
30
31
//实例八
Promise.resolve('1').then((v) => {
console.log(v)
}) //1

new Promise(function (resolve, reject) {
resolve(1);
}).then((v) => {
console.log(v);
return v * 2;
}).then((v) => {
console.log(v);
})
// 1
// 2

//实例九
new Promise(function (resolve, reject) {
resolve(1);
}).then((v) => {
console.log(v);
return v * 2;
}).then((v) => {
console.log(v);
})
Promise.resolve('3').then((v) => {
console.log(v)
})
//1
//3
//2

async函数

要记住async函数是generator函数的语法糖。

  1. 具有内置执行器。
  2. 能自动执行,输出最后结果。
  3. 更好的语法。
  4. 更广的适用性。
  5. 返回值为Promise

这时候我给出一个实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function f1() {
console.log('1');
}

function f2() {
console.log('2');
}

function f3() {
console.log('3');
}
async function fn() {
await f1();
await f2();
await f3();
return 1;
}
let g = fn();
console.log(g);

这个实例的执行结果是怎么样的,可以吧它复制到控制面板中执行,你会发现输出的顺序是1,promise对象,2,3。这是为什么,为什么1之后是promise,展开promise对象,会发现里面的promiseValue值是1,那这个值是不是和yield一样返回的是后面表达式的值呢,然后选择吧await f1();注释掉,重新执行就发现其实promise的promiseValue值没有改变,所以这不是和yield一样但是不可否认的是会返回一个Promise值。

但是为什么是promise对象之后才是2,3呢,不是先1,2,3才promise对象,因为async就是一个异步函数,我们可以这么理解,当我们fn()的时候,会执行到第一个await后面的语句,然后停止执行,也就是输出了1,然后发现函数外还有代码需要执行,就会暂停,执行函数外的后续代码,等到系统空闲,就会继续执行函数内后续的代码。

所以结果就是1,promise对象,2,3。

异步操作

其实“异步”在我看来就是一部分先执行,一部分后执行。而在js中最简单的异步就是ajax中的回调函数,回调函数就是把任务的第二个阶段单独写在一个函数中,当做参数传递进去也就是callback参数。而异步的核心就是现在运行的代码和未来运行的关系。程序中将来执行的部分并不一定在现在运行的部分执行完之后就立即执行。无法完成的任务就会异步完成。比如在一开始的页面请求中,为避免页面的卡死,会将向后台请求的代码进行异步处理,一般利用ajax就是回调。而真正的异步代码,直到es6才开始内建起来(Promise对象)。

JS的引擎并不是独立运行的,它运行在宿主换将中(一般指浏览器)。这些环境都有一个共同点,它们都提供了一种机制来处理程序中多个快的执行,且执行每块时调用JS引擎,这种机制就是事件循环。

可以参考这个图:

并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不同 的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。但是事件循环吧自身的工作分成一个个任务,内存不会共享。但是通过分立线程中彼此合作的事件循环,可以共享。

一个任务可能引起更多任务被添加到同一个队列末尾。所以,理论上说,任务循环(job loop)可能无限循环(一个任务总是添加另一个任务,以此类推),进而导致程序的 饿死,无法转移到下一个事件循环tick。一旦有事件需要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个 tick。用户交互、IO 和定时器会向事件队列中加入事件。任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引发一 个或多个后续事件。

但是JS是单线程运行特性。不会进行并行执行除非使用web worker。单线程事件循环是并发的一种形式。

异步总结

js的异步其实是为了解决浏览器单线程问题,而提出的,如果没有异步,那么浏览器在发送请求的时候就会一直卡死在等待服务器response,从而造成资源利用地下的问题。而异步就是为了解决这个问题,而最早应用的异步应该就是ajax里面的回调函数了,但是回调函数依然有问题,那就是如果需要多次应用callback就要在里面一层层的添加回调函数从而而造成回调地狱,又或者是如果在多个ajax执行完成时才能执行下一步,那可能就需要添加许多额外的变量或者判断。而到了es6异步才算是真正的诞生,其实际应该是generate函数、promise函数的应用以及asyn和await的应用了。

对于generate函数其实就是一个函数内,利用yield定义一连串的状态机,然后手动的去执行,也就是用next执行下一个步骤,而next可以传参进去替代原本的yeild的位置。

promise函数可以说是现在应用的最多的一个函数了,我们在平时的开发请求中,也基本会用到它,首先我们知道的是promise其实有3个状态pending、fullified、reject,3种状态,而3种状态的转换是不可逆的,promise的首个状态一般是pending也就是在new Promise的时候传递进去的函数参数所在的状态就是pending,要知道的一个点是从pending到fullified状态其实是需要调用resolve参数的,而如果从pending到reject状态到达的路径有两条,函数内有语法或者数据导致报错,另一条就是手动调用reject,但是我们有一点需要注意的,对于promise的报错是逐渐冒泡的,比如我在promise函数内报错,那这个错误就会一直向下冒泡,经历所有的then直到后续有than的第二个参数或者catch对其进行捕获,如果没有,就会一直抛到最外层形成错误,但是这个错误并不会影响或者停止浏览器执行除这个promise之外的其他代码,其实then和catch都会返回一个promise函数让后续继续执行,而参数就是return的内容。所以我们最好的方式就是我们可以在末尾添加一个catch保证错误是能被我们捕获的,然后再添加一个done告诉别人该函数已经执行完毕,我们知道的一个点就是。其实promise在执行完之后会一直保持现有状态,除非后续有调用,否则只有在关掉页面时才会消除。

而async和await其实可以理解为进阶版的generate,async和await会自动执行,其实就是每一次都会把剩余部分都放到微任务队中等待下次的执行,并且会返回await后的内容。

所以其实promise与async其实都会把后续部分放入到微任务中,而原本的callback其实就是一个宏任务的简易调用。

Thunk函数

问题的引入:函数的参数到底应该何时求值?

1
2
3
4
5
var x = 1;
function f(m){
return m * 2;
}
f(x + 5)
  1. 一种意见是”传值调用”(call by value),即在进入函数体之前,就计算 x + 5 的值(等于6),再将这个值传入函数f 。C语言就采用这种策略。

    1
    2
    3
    f(x + 5)
    // 传值调用时,等同于
    f(6)
  2. 另一种意见是”传名调用”(call by name),即直接将表达式 x + 5 传入函数体,只在用到它的时候求值。Haskell语言采用这种策略。

    1
    2
    3
    f(x + 5)
    // 传名调用时,等同于
    (x + 5) * 2

传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。

比如:

1
2
3
4
function f(a, b){
return b;
}
f(3 * x * x - 2 * x - 1, x);

f函数的第一参数是很复杂的表达式,但是函数体内根本没用到。对这个参数求值其实是没必要的。所以我们更倾向于传名调用这种高效率的参数求值方式。

编译器的传名调用实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体,这个临时函数就叫做Thunk函数

1
2
3
4
5
6
7
8
9
10
11
function f(m){
return m * 2;
}
f(x + 5);
// 等同于
let thunk = function () {
return x + 5;
};
function f(thunk){
return thunk() * 2;
}

JavaScript语言的Thunk函数

JavaScript语言是传值调用,它的Thunk函数含义有所不同。在JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let Thunk = function (fn) { //一个thunk函数转换器
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};

function f(a, cb) {
cb(a);
}

let ft = Thunk(f);

let log = console.log.bind(console);
ft(1)(log) // 1

async函数

语法

  1. async 函数返回一个Promise对象。

    async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。

    1
    2
    3
    4
    5
    async function f() {
    return 'hello world';
    }
    f().then(v => console.log(v))
    // "hello world"

    async内部抛出错误,会导致返回的Promise对象变为reject对象,抛出的错误对象会被catch方法回调函数接收到。

  2. async 函数返回的Promise对象,必须等到内部所有 await 命令的Promise对象执行完,才会发生状态改变。也就是说,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数。

    1
    2
    3
    4
    5
    6
    7
    async function getTitle(url) {
    let response = await fetch(url);
    let html = await response.text();
    return html.match(/<title>([\s\S]+)<\/title>/i)[1];
    }
    getTitle('https://tc39.github.io/ecma262/').then(console.log)
    // "ECMAScript 2017 Language Specification"
  3. 正常情况下, await 命令后面是一个Promise对象。如果不是,会被转成一个立即 resolve 的Promise对象。

    1
    2
    3
    4
    5
    async function f() {
    return await 123;
    }
    f().then(v => console.log(v))
    // 123

    只要一个 await 语句后面的Promise变为 reject ,那么整个 async 函数都会中断执行。为了避免这个问题,可以将第一个 await 放在 try...catch 结构里面,这样第二个 await 就会执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    async function f() {
    try {
    await Promise.reject('出错了');
    } catch(e) {
    }
    return await Promise.resolve('hello world');
    }
    f()
    .then(v => console.log(v))
    // hello world

    另一种方法是 await 后面的Promise对象再跟一个 catch 方面,处理前面可能出现的错误。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    async function f() {
    await Promise.reject('出错了')
    .catch(e => console.log(e));
    return await Promise.resolve('hello world');
    }
    f()
    .then(v => console.log(v))
    // 出错了
    // hello world
  4. 如果 await 后面的异步操作出错,那么等同于 async 函数返回的Promise对象被 reject 。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    async function f() {
    await new Promise(function (resolve, reject) {
    throw new Error('出错了');
    });
    }
    f()
    .then(v => console.log(v))
    .catch(e => console.log(e))
    // Error:出错了

    上面代码中, async 函数 f 执行后, await 后面的Promise对象会抛出一个错误对象,导致 catch 方法的回调函数被调用,它的参数就是抛出的错误对象。

多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

1
2
let foo = await getFoo();
let bar = await getBar();

上面代码中, getFoo 和 getBar 是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有 getFoo 完成以后,才会执行 getBar ,完全可以让它们同时触发。

1
2
3
4
5
6
7
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。

Class

不同点

  • Class的内部定义的方法,都是不可枚举的(non-enumerable),这一点和ES5中不一样。

  • ES6定义对象时,可以用表达式作为属性名 如[‘a’+’bc’]。

  • 类的构造函数,不使用new无法调用,会报错。跟普通构造函数不一样,后者不用new也可以执行。

  • Class不存在变量提升(hoist),这一点与ES5完全不同。

    1
    2
    new Foo(); //ReferenceError
    class Foo {};

    这种规定的原因主要是与继承有关:必须保证子类在父类之后定义。

    1
    2
    3
    4
    {
    let Foo = class {};
    class Bar extends Foo {};
    }

    如上:如果class此时变量提升,但是由于let是不会提升的,所以导致Bar继承Foo的时候,Foo还没有定义。

通过new生成对象实例时,自动调用constructor方法,constructor方法默认返回实例对象this,也可以指定返回另外一个对象,如:

1
2
3
4
5
6
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo //false

不推荐通过实例的proto属性为Class添加方法,因为修改的话,会影响所有实例。如:

1
2
3
4
5
var p1=new Point(2,3);
var p2=new Point(3,2);
p1.__proto__.printName=function () {return 'Oops'};
p1.printName(); //Oops
p2.printName(); //Oops

class表达式

与函数一样,Class也可以使用表达式的形式定义。

1
2
3
4
5
const MyClass = class Me {
getClassName() {
return Me.name;
}
};

注意:

MyClass才是类名,Me只在Class的内部代码可用,指代当前类,也可以省略。

可以是立即执行的Class

1
2
3
4
5
6
7
8
9
const person = new class Me {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('zj');
person.sayName(); //zj

私有方法

  1. 方法前面加_,_privateFunc() {}; 这种方法在类的外部还是可以调用,不安全。
  2. 将私有方法模块移出模块。
  3. Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。

name属性

1
2
class Point {}
Point.name //"Point"

name属性总是返回紧跟在class关键字后面的类名。

Class的继承

子类必须在constructor方法中调用super方法,否则新建实例会出错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。

子类的构造函数中,只有调用super之后,才可以使用this关键字。

俩条继承链:

  1. 子类的__proto__属性,表示构造函数的继承,总是指向父类
  2. 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
-------------本文结束感谢您的阅读-------------