0%

题目介绍

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
实现一个LazyMan,可以按照以下方式调用:
LazyMan("Hank")输出:
Hi! This is Hank!

LazyMan("Hank").sleep(10).eat("dinner")
输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~

LazyMan("Hank").eat("dinner").eat("supper")
输出
Hi This is Hank!
Eat dinner~
Eat supper~

LazyMan("Hank").sleepFirst(5).eat("supper")
输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper

以此类推

考察知识点

  • 方法链式调用
  • 类的使用和面向对象编程的思路
  • 设计模式引用
  • 代码解耦
  • 最少知识原则
  • 代码的书写结果和命名

题目解析

  1. 看题目输出示例,这是一个拟人化的输出 => 定义一个名为LazyMan的类,类里面有sayNameeatsleep等方法
  2. 从输出结果来看:sleepFirst的优先级最高,其余的行为优先级一致
  3. LazyMan首先需要初始化人,才能继续后面的行为,因此LazyMan是一个接口 => sayName方法一定在构造函数中
  4. 按调用的方法次序进行顺序执行,是一个队列

实现思路及代码

(1)任务队列实现

这种模式类似于中间件模式,其核心是next方法,每当队列中的一个方法执行完都会调用next方法来执行队列中的另一个方法,直到全部执行完成

构造函数中需要setTimeout保证队列开始执行的时间是在下一个事件循环中,从而确保当前的链式调用中的所有行为在调用之前被加载进队列中

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
class _LazyMan {
constructor(name) {
this.tasks = [];
this.sayName(name);

setTimeout(() => {
this.next();
})
};

next() {
const task = this.tasks.shift(); // 拿出队列最前面的方法,并且要在队列里面删除它
task && task();
};

sayName(name) {
const task = () => {
console.log(`Hi! This is ${name}!`);
this.next();
};
this.tasks.push(task);
};

sleep(time) {
this.tasks.push(this._sleepWrapper(time));
return this;
};

sleepFirst(time) {
this.tasks.unshift(this._sleepWrapper(time));
return this;
};

_sleepWrapper(time) {
return () => {
setTimeout(() => {
console.log(`Wake up afeter ${time} second`);
this.next();
}, time * 1000);
}
};


eat(type) {
const task = () => {
console.log(`Eat ${type}~`)
this.next();
};
this.tasks.push(task);
return this;
};
}


const LazyMan = (name) => new _LazyMan(name);

LazyMan("Hank").sleepFirst(5).eat("supper")

/*
Wake up afeter 5 second
Hi! This is Hank!
Eat supper~
*/

(2)任务队列 + Promise实现

用Promise代替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
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
class _LazyMan {
constructor(name) {
this.tasks = [];
this.sayName(name);

Promise.resolve().then(() => {
let task = Promise.resolve();
this.tasks.forEach(item => {
task = task.then(item);
})
})
};

sayName(name) {
const task = () => {
console.log(`Hi! This is ${name}!`);
};
this.tasks.push(task);
};

sleep(time) {
this.tasks.push(this._sleepWrapper(time));
return this;
};

sleepFirst(time) {
this.tasks.unshift(this._sleepWrapper(time));
return this;
};

_sleepWrapper(time) {
return () => new Promise(resolve => {
setTimeout(() => {
console.log(`Wake up afeter ${time} second`);
resolve();
}, time * 1000);
})
};


eat(type) {
this.tasks.push(() => {
console.log(`eat ${type}`);
})
return this;
};
}


const LazyMan = (name) => new _LazyMan(name);

LazyMan("Hank").sleepFirst(5).eat("supper")

/*
Wake up afeter 5 second
Hi! This is Hank!
Eat supper~
*/

(3)任务队列 + async实现

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
class _LazyMan {
constructor(name) {
this.tasks = [];
this.sayName(name);

setTimeout(async () => {
for (let task of this.tasks) {
await task();
}
})
};

sayName(name) {
const task = () => {
console.log(`Hi! This is ${name}!`);
};
this.tasks.push(task);
};

sleep(time) {
this.tasks.push(this._sleepWrapper(time));
return this;
};

sleepFirst(time) {
this.tasks.unshift(this._sleepWrapper(time));
return this;
};

_sleepWrapper(time) {
return () => new Promise(resolve => {
setTimeout(() => {
console.log(`Wake up afeter ${time} second`);
resolve();
}, time * 1000);
})
};


eat(type) {
this.tasks.push(() => {
console.log(`eat ${type}`);
})
return this;
};
}


const LazyMan = (name) => new _LazyMan(name);

LazyMan("Hank").sleepFirst(5).eat("supper")

/*
Wake up afeter 5 second
Hi! This is Hank!
Eat supper~
*/

参考

LazyMan的深入解析和实现

多种方式实现 LazyMan

如何实现一个LazyMan?

LazyMan 有几样写法,你知道么?

ES8提出的async/await关键词旨在解决利用异步结构组织代码的问题

异步函数基础

async

async关键字用于声明异步函数

  1. async关键字可以在函数声明、函数表达式、箭头函数、函数方法上使用
1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数声明
async function foo() {}

// 函数表达式
let foo = async function() {}

// 箭头函数
let foo = async () => {}

// 函数方法
let o = {
async foo() {}
}
  1. 使用async关键字可以让函数具有异步特征,但总体上函数仍然是同步求值的,在参数或者闭包方面,异步函数仍然具有普通JavaScript函数的正常行为

  2. 异步函数始终返回Promise对象

    • 如果异步函数内部有显式的return语句并且返回值(没有return 值就是undefined),这个值会被Promise.resolve()包装成一个Promsie对象
    • 如果异步函数直接return一个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
    async function foo() {
    console.log(1);
    return 3;
    }

    // 给返回的promise添加一个then方法的解决处理程序
    foo().then(console.log);

    console.log(2);

    // 1 这个是foo()调用得到的
    // 2
    // 3 这个是then()处理的结果


    async function foo() {
    console.log(1);
    return Promise.resolve(3);
    }

    foo().then(console.log);

    console.log(2);

    // 1 这个是foo()调用得到的
    // 2
    // 3 这个是then()处理的结果
  3. 异步函数的返回值期待一个实现thenable接口的对象,但常规值也可以

    • 如果返回的是实现thenable接口的对象,则这个对象可以由提供给then()的处理程序“解包”
    • 如果不是,则返回子和就被当作已解决的Promise
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 返回一个原始值
    async function foo() {
    return 'foo';
    }
    foo().then(value => console.log(value)); // 'foo'

    // 返回一个没有实现thenable接口的对象
    async function bar() {
    return ['bar']
    }
    bar().then(value => console.log(value)); // ['bar']

    // 返回一个实现thenabke接口的对象
    async function baz() {
    const thenable = {
    then(callback) {
    callback('baz');
    }
    };

    return thenable;
    }
    baz().then(value => console.log(value)); // 'baz'
  4. 在异步函数中抛出错误会返回拒绝的Promise

1
2
3
4
5
6
7
8
9
async function foo() {
console.log(1);
throw 3;
} // foo为 Promise{<rejected>:3}

foo().catch(reason => console.log(reason));

// 1 foo()的结果
// 3 catch()的结果
  1. 拒绝Promise的错误不会被异步函数捕获
1
2
3
4
5
6
7
8
9
10
async function foo() {
console.log(1);
Promise.reject(3);
}

foo().catch(console.log);
console.log(2);
// 1
// 2
// Uncaught (in promise): 3

await

await关键词可以暂停异步函数代码的执行,等待Promise的解决

  1. await可以单独使用也可以在表达式中使用

  2. await 关键字会暂停执行异步函数后面的代码,让出JavaScript 运行时的执行线程,这个行为与生成器函数中的yield 关键字是一样的,await 关键字同样是尝试“解包”对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行

  3. await关键字期待(但实际上并不要求)一个实现thenable 接口的对象,但常规的值也可以,如果是实现thenable 接口的对象,则这个对象可以由await 来“解包”,如果不是,则这个值就被当作已经解决的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
// 等待一个原始值
async function foo() {
console.log(await 'foo');
}
foo();
// foo

// 等待一个没有实现thenable 接口的对象
async function bar() {
console.log(await ['bar']);
}
bar();
// ['bar']

// 等待一个实现了thenable 接口的非期约对象
async function baz() {
const thenable = {
then(callback) { callback('baz'); }
};
console.log(await thenable);
}
baz();
// baz

// 等待一个期约
async function qux() {
console.log(await Promise.resolve('qux'));
}
qux();
// qux
  1. await会抛出错误的同步操作,会返回拒绝的Promise
1
2
3
4
5
6
7
8
9
10
11
12
13
async function foo() {
console.log(1);
await (() => {throw 3})();
}

foo().catch(reason => {
console.log(reason);
})
console.log(2);

// 1
// 2
// 3
  1. 对拒绝的Promise使用await则会释放(unwrap)错误值(将拒绝Promise返回)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function foo() {
console.log(1);
await Promise.reject(3);
console.log(4); // 这行代码不会执行
}

foo().catch(reason => {
console.log(reason);
})
console.log(2);

// 1
// 2
// 3
  1. await关键字有一定限制

    • await关键字必须在异步函数中使用,不能在顶级上下文如

super这个关键词既可以当对象使用,也可以当函数使用

super作为函数使用

super作为函数使用时,只能在子类的构造函数中【目的是为了调用父类的构造函数】 => super表示父类构造函数,但是this指向当前子类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Father {
constructor(name, age) {
this.name = name;
this.age = age;
console.log(new.target.name); // new.target 指向当前正在执行的函数
}
}

class Son extends Father {
constructor(name, age) {
super(name, age);
}
}

new Father(); // Father
new Son(); // Son

在子类的constructor中必须调用super()方法,因为子类没有自己的this对象,而是要继承父类的this对象,但是此时this指向的是子类的构造函数,此时super就表示了父类的构造函数,super()此时相当于Father.prototype.constructor.call(this, props)

super作为对象使用

super作为普通方法使用

(1)super指向的是父类的原型对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Father {
static c() {
return 1;
};
c() {
return 11;
}
constructor() {};
}

class Son extends Father {
constructor() {
super();
console.log(super.c()); // 11
}
}

new Son();

在上面的代码中,子类中的super.c(),就是把super当作一个对象使用,对象里面有c方法,因此super在普通函数中指向,Father.prototype,也就是说super.c()相当于Father.prototype.c()

(2)通过super调用父类方法时,super内部的this指向子类的实例
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
class Father {
x = 7;
fun() {
this.x = 77;
};
print() {
console.log(this.x);
};
}

class Son extends Father {
x = 777;
fn() {
super.fun();
this.x = 777;
super.print(); // 777
}
}

new Son().fn();

/*
如果没有 this.x = 7777 则返回77
如果没有 super.fun() 则返回 777
如果没有 this.x = 777 则返回 7
*/

此时的super可以替换this,与this.print()等价

(3)当通过super为子类属性赋值时,super就是this
1
2
3
4
5
6
7
8
9
10
11
class Father {}

Father.prototype.c = 77;

class Son extends Father {
fn() {
super.c = 7; // 为子类赋值的情况下,super为this
console.log(super.c); // 7 super获取的是父类的原型
console.log(this.c); // 7
}
}

赋值的情况下super就是this

super作为静态方法使用

(1)super指向的是父类
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
// 例1
class Father {}

Father.prototype.c = function() {
return 11;
}

class Son extends Father {
fn() {
super.c = function() { // 为子类赋值的情况下super为this
return 1
};
console.log(super.c()); // 11 super为父类的prototype
console.log(this.c()); // 1 this指向子类构造函数
}
}

new Son().fn();


// 例2
class Father {
static c() { // 静态方法
console.log(2);
}

c() { // 原型对象中的方法
console.log(22);
}
}

class Son extends Father {
static s() {
super.c(); // super指向父类 调用的是静态方法
}

s() {
super.c(); // super指向父类原型对象
}
}

Son.son(); // 2 调用的是Son的静态方法,指向父类静态方法
new Son().c(); // 22 调用的是Son的原型方法,指向父类原型方法
(2)在子类的静态方法中通过super调用父类方法是,super内部的this指向子类
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
class Father {
static x = 3;
constructor() {
this.x = 2;
};
static print() {
console.log(this.x);
}
}

class Son extends Father {
constructor() {
super(); // 构造函数内调用
this.x = 4; // this指向子类构造函数
}

static fn() {
this.x = 5;
// 如果没有this.x = 5,则 Son.x 打印为1
// 如果既没有this.x = 5,也没有Bar.x = 1,则打印3
// 如果既没有this.x = 5,也没有Bar.x = 1 和 x =3,则打印undefined,不会输出构造函数中的2
super.print(); // super
}
}

Son.x = 1;
Son.fn();

例题

注意看注释~

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
class Father {
name = 'Father';
age = 18;

getName() {
return this.name;
}

getAge() {
return this.age;
}
}

class Son extends Father {
constructor() {
super(); // 这里调用super()是为了调用父类的构造函数
// console.log(this); // this指向的是子类的构造函数 this 为 Son {name:'Father', age: 18}
}

getAge() {
// console.log(this.getAge)
return this.getAge() + 10;
}
}

console.log(new Son().getName()); // Father
// console.log(new Son().getAge()); // 报错 因为 new Son().getAge()调用的是子类构造函数的getAge函数,它一直在调用自身
console.log(new Son().getAge.call({age:2})); // 报错 这里this是{age:2}, 此时this.getAge为undefined

参考

理解 es6 class 中 constructor 方法 和 super 的作用

前端面试之关于super的全方位解读

跨域问题很常见,同时也是面试常问的题目之一,出现的机率super plus【自创词~就是超级多次啦】

跨域问题主要是浏览器的同源策略造成的

因此,本文结构如下:

  1. 什么是同源策略(别说你还不知道URL的组成!)
  2. 如何解决跨域问题?

同源策略

url主要由协议域名端口三部分组成【端口一般是隐藏的~】

同源指的是:协议 域名 端口号 必须一致,其中任意一个不一致就会引起跨域

同源策略限制了从一个源加载的文档或脚本如何与另一个文档进行交互

  • 当前域下的js脚本不能够访问其他域下的cookie localStorage indexDB
  • 当前域下的js脚本不能够操作访问其他域下的DOM
  • 当前域下ajax无法发送跨域请求

同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作

如何解决跨域问题

解决跨域问题主要有如下几种方式:

  • JSONP:通过动态创建<script>元素并为src属性执行跨域URL实现的【<script><img>类似,能够不受限制地从其他域加载资源】

  • CORSCross-Origin Resource Sharing跨站资源共享,使用自定义的HTTP 头部允许浏览器和服务器相互了解,以确实请求或响应
    应该成功还是失败

1. JSONP

jsonp原理就是利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据

JSONP介绍

  • JSONP

    1
    callback({'name': 'Katrina'});
  • JSONP格式包含两部分:回调和数据

    • 回调是在页面接收到响应之后应该调用的函数,通常回调函数的名称是通过请求来动态指定的
    • 而数据就是作为参数传给回调函数的JSON 数据
  • 典型的JSONP请求

    1
    'http://freegeoip.net/json/?callback=handleResponse'   // handleResponse 回调函数的名称

JSONP实现

  • 实现步骤

    • 准备一个回调函数,用于接收JSON数据
    • 动态创建script标签
    • 给创建的script添加src
    • 把创建好的script添加到head中
  • 原生JS实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 准备一个回调函数,用于接收JSON数据
    function handleResponse(data) {
    console.log(JSON.stringify(data));
    }

    // 动态创建script标签
    let script = document.createElement('script');
    // 给创建的script添加src
    script.src = 'http://www.domain.com:8080/login?username=admin&callback=handleResponse'

    // 把创建好的script添加到head中
    document.head.appendChild(script);

JSONP优缺点

  • 优点:
    • 简单易用
    • 使用JSONP可以直接访问响应,实现浏览器与服务器的双向通信
  • 缺点:
    • 不安全,JSONP 是从不同的域拉取可执行代码,如果这个域并不可信,则可能在响应中加入恶意内容【XSS攻击】
    • 不好缺点JSONP请求是否失败
    • 具有局限性, 仅支持get方法

2. CORS

跨站资源共享(Cross Origin Resource Sharing)使用额外的 HTTP 头来告诉浏览器 :

  • 让运行在一个 origin (domain)上的Web应用被准许访问来自不同源服务器上的指定的资源
  • 当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域HTTP 请求

因此,CORS需要浏览器和服务器同时支持,实现CORS的关键就是服务器,只要服务器实现了CORS请求,就可以跨源通信

请求头头部配置

1
Origin: url;   // url:浏览器url

服务器配置

1
2
3
Access-Control-Allow-Origin: url;  // 允许这个url跨域
// 如果资源公开:
Access-Control-Allow-Origin: *;
  • 如果没有这个头部或者有源但不匹配,则表明不会响应浏览器请求,否则,服务器就会处理这个请求

注意:无论请求还是响应都不会包含cookie信息

在CORS请求中,如果想要传递Cookie,就要满足以下三个条件:

  • 在请求中设置 withCredentials

默认情况下在跨域请求,浏览器是不带 cookie 的,但是我们可以通过设置withCredentials来进行传递 cookie.

1
2
3
4
5
// 原生 xml 的设置方式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// axios 设置方式
axios.defaults.withCredentials = true;
  • Access-Control-Allow-Credentials 设置为 true
  • Access-Control-Allow-Origin 设置为 false
  • 跨域XHR 对象允许访问statusstatusText 属性,也允许同步请求
  • 出于安全考虑,跨域XHR对象也施加了一些额外限制
    • 不能使用setRequestHeader()设置自定义头部
    • 不能发送和接收cookie
    • getAllResponseHeaders()方法始终返回空字符串

参考

URL的概念与组成

回顾一下红宝书第四版P330中的一句话:”传给then()的任何非函数类型的参数都会被静默忽略“,这个忽略是单纯的忽略吗?其实不然,应该考虑到值穿透的问题

.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透

举例1:

1
2
3
4
5
6
Promise.resolve(1)
.then(2) // 注意这里
.then(Promise.resolve(3))
.then(console.log)

// 打印结果为1

举例2:

1
2
3
4
5
6
7
Promise.resolve('foo')
.then(Promise.resolve('bar')) // 注意这里
.then(function(res) {
console.log(res);
})

// 打印结果为foo

举例3:

1
2
3
4
5
6
Promise.resolve(1)
.then(function() {return 2})
.then(Promise.resolve(3)) // 注意这里
.then(console.log)

// 打印结果为2

举例4:

1
2
3
4
5
6
7
8
Promise.resolve(1)
.then(function() {return 2})
.then(function() {return Promise.resolve(3)})
.then(console.log)

// 输出3

// 这个不是穿透的例子,主要是为了要理解:.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透

拓展:catch的穿透

举例1:一个很容易理解的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
new Promise((resolve, reject) => {
reject(1)
}) // 返回的Promise实例: Promise {<rejected>:1}
.then(value => {
console.log('success', value); // 这里不起作用因为这个是onResolved处理程序
}, reason => {
console.log('failture', reason); // 打印结果为:filture 1
}) // 返回的Promise实例: Promise {<resolved>:undefined} 因为没有显示的返回语句
.then(value => {
console.log('success', value); // 打印结果为:success
}, reason => {
console.log('failture', reason); // 不起作用
}) // 返回的Promise实例: Promise {<resolved>:undefined} 因为没有显示的返回语句

举例2:增加catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Promise((resolve, reject) => {
reject(1)
}) // 返回的Promise实例: Promise {<rejected>:1}
.then(value => {
console.log('success', value); // 这里不起作用因为这个是onResolved处理程序
}, reason => {
console.log('failture', reason); // 打印结果为:filture 1
}) // 返回的Promise实例: Promise {<resolved>:undefined} 因为没有显示的返回语句
.then(value => {
console.log('success', value); // 打印结果为:success
}, reason => {
console.log('failture', reason); // 不起作用
}) // 返回的Promise实例: Promise {<resolved>:undefined} 因为没有显示的返回语句
.catch(reason => {
console.log('failture', reason); // 不会走到这里的,因为Promise实例的状态为resolved
})

举例3:then里面没有onRejected处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
new Promise((resolve, reject) => {
reject(1);
}) // 返回的Promise实例: Promise {<rejected>:1}
.then(value => {
console.log('success', value); //没有指定失败的回调函数,不执行代码,去往下一级寻找失败状态回调函数
})
.then(value => {
console.log('success', value); //没有指定失败的回调函数,不执行代码,去往下一级寻找失败状态回调函数
})
.catch(reason => {
console.log('failture', reason); // 打印结果为: failture 3
})

/*
当.then方法中没有指定失败的回调函数时
使用.catch会默认为没有指定失败回调函数的.then指定失败的回调函数
reason => {
throw reason // 注意这里不是return reason 而是throw reason throw保证了返回结果为失败
}
*/

举例4:在举例3上拓展,注意看变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
new Promise((resolve, reject) => {
reject(1);
}) // 返回的Promise实例: Promise {<rejected>:1}
.then(value => {
console.log('success', value); //没有指定失败的回调函数,不执行代码,去往下一级寻找失败状态回调函数
})
.then(value => {
console.log('success', value);
}, reason => { // 指定了失败的回调函数,执行失败的回调函数,而不会执行catch
console.log('failture111', reason); // 打印结果为: failture111 3
}) // 返回的Promise实例: Promise {<resolved>:undefined}
.catch(reason => {
console.log('failture', reason);
})

举例5:当一个promise是reject状态,但是没有失败回调,也没有写catch捕获,那么系统会默认捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Promise((resolve, reject) => {
reject(1);
}) // 返回的Promise实例: Promise {<rejected>:1}
.then(value => {
console.log('success', value); //没有指定失败的回调函数,不执行代码,去往下一级寻找失败状态回调函数
})
.then(value => {
console.log('success', value); //没有指定失败的回调函数,不执行代码,去往下一级寻找失败状态回调函数
})

/*
系统默认捕获的形式如下
reason => {
throw reason // 注意这里不是return reason 而是throw reason throw保证了返回结果为失败
}
*/

等价代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Promise((resolve, reject) => {
reject(1);
}) // 返回的Promise实例: Promise {<rejected>:1}
.then(value => {
console.log('success', value); //没有指定失败的回调函数,不执行代码,去往下一级寻找失败状态回调函数
})
.then(value => {
console.log('success', value); //没有指定失败的回调函数,不执行代码,去往下一级寻找失败状态回调函数
})
.catch(reason => {throw reason});

/*
[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "1".] {
code: 'ERR_UNHANDLED_REJECTION'
}
*/

总结

当使用.catch时,会默认为没有指定失败回调函数的.then添加一个失败回调函数【举例3,4】

.catch所谓的值穿透并不是一个失败状态就触发.catch,而是一层层传递下来的【举例3,5】

异常穿透的前提是所有.then都没有执行失败状态的回调函数【举例3,5】

如果.catch前所有的.then都制定了失败状态的回调函数,.catch就失去了意义【举例2,4】

参考

Promise 值穿透 特性

期约连锁

因为每个Promise实例的方法(then() catch() finally())都可以返回一个Promise实例,新的Promise实例又有自己的实例方法,这样连缀的方法调用就可以构成所谓的“期约连锁“,也就是平时说的链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let p1 = new Promise((resolve, reject) => {
console.log('p1 executor');
setTimeout(resolve, 1000);
});

p1.then(() => new Promise((resolve, reject) => {
console.log('p2 executor');
setTimeout(resolve, 1000);
}))
.then(() => new Promise((resolve, reject) => {
console.log('p3 executor');
setTimeout(resolve, 1000);
}))
.then(() => new Promise((resolve, reject) => {
console.log('p4 executor');
setTimeout(resolve, 1000);
}));

// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

把生成期约的代码提取到一个工厂函数中,就可以简洁代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function delayedResolved(str) {
return new Promise((resolve, reject) => {
console.log(str);
setTimeout(resolve, 1000);
})
}

delayedResolve('p1 executor')
.then(() => delayedResolve('p2 executor'))
.then(() => delayedResolve('p3 executor'))
.then(() => delayedResolve('p4 executor'))
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

期约连锁很好地解决了回调地狱的问题

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
// A
// / \
// B C
// /\ /\
// D E F G
let A = new Promise((resolve, reject) => {
console.log('A');
resolve();
});

let B = A.then(() => console.log('B'));
let C = A.then(() => console.log('C'));

B.then(() => console.log('D'));
B.then(() => console.log('E'));
C.then(() => console.log('F'));
C.then(() => console.log('G'));
// A
// B
// C
// D
// E
// F
// G

(重点)Promise合成

(1)Promise.all()

手写原理 | Promise.all

Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决

  1. Promise.all()法接收一个可迭代对象,返回一个新Promise
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let p1 = Promise.all([
Promise.reslove(),
Promise.resolve()
]);

// 以下都是在手写的时候要注意的!!!!!!

// 可迭代对象中的元素会通过Promise.resolve()转换为期约
let p2 = Promise.all([3, 4]);
// 空的可迭代对象等价于Promise.resolve()
let p3 = Promise.all([]);
// 无效的语法
let p4 = Promise.all();
// TypeError: cannot read Symbol.iterator of undefined
  1. Promise.all只会在每个包含Promise都解决之后才会解决
1
2
3
4
5
6
7
8
let p = Promise.all([
Promise.resolve(),
new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);

setTimeout(console.log, 0, p); // Promise <pending>
p.then(() => setTimeout(console.log, 0, 'all() resolved!'));
// all() resolved!(大约1 秒后)
  1. 如果至少一个包含的Promise待定,则合成的Promise也会待定
1
2
3
// 永远待定
let p1 = Promise.all([new Promise(() => {})]);
setTimeout(console.log, 0, p1); // Promise <pending>
  1. 如果有一个包含的Promise拒绝,则最终合成的Promise也会拒绝
1
2
3
4
5
6
7
8
9
10
11
12
13
// 永远待定
let p1 = Promise.all([new Promise(() => {})]);
setTimeout(console.log, 0, p1); // Promise <pending>

// 一次拒绝会导致最终期约拒绝
let p2 = Promise.all([
Promise.resolve(),
Promise.reject(),
Promise.resolve()
]);

setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught (in promise) undefined
  1. 如果所有的Promise都解决,则合成的Promise的解决值就是所有Promise解决值的数组,顺序按照迭代顺序
1
2
3
4
5
6
7
let p = Promise.all([
Promise.resolve(3),
Promise.resolve(),
Promise.resolve(4)
]);

p.then((values) => setTimeout(console.log, 0, values)); // [3, undefined, 4]
  1. 如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由,之后再拒绝的期约不会影响最终期约的拒绝理由,不过,这并不影响所有包含期约正常的拒绝操作,合成的期约会静默处理所有包含期约的拒绝操作
1
2
3
4
5
6
7
8
9
10
// 虽然只有第一个期约的拒绝理由会进入
// 拒绝处理程序,第二个期约的拒绝也
// 会被静默处理,不会有错误跑掉
let p = Promise.all([
Promise.reject(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);

p.catch((reason) => setTimeout(console.log, 0, reason)); // 3
// 没有未处理的错误

(2)Promise.race()

Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像

  1. Promise.race()接收一个可迭代数组,返回一个新的Promise
1
2
3
4
5
6
7
8
9
10
11
12
let p1 = Promise.race([
Promise.resolve(),
Promise.resolve()
]);

// 可迭代对象中的元素会通过Promise.resolve()转换为期约
let p2 = Promise.race([3, 4]);
// 空的可迭代对象等价于new Promise(() => {})
let p3 = Promise.race([]);
// 无效的语法
let p4 = Promise.race();
// TypeError: cannot read Symbol.iterator of undefined
  1. Promise.race()不会对解决或拒绝的期约区别对待。无论是解决还是拒绝,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 解决先发生,超时后的拒绝被忽略
let p1 = Promise.race([
Promise.resolve(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);

setTimeout(console.log, 0, p1); // Promise <resolved>: 3

// 拒绝先发生,超时后的解决被忽略
let p2 = Promise.race([
Promise.reject(4),
new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);

setTimeout(console.log, 0, p2); // Promise <rejected>: 4

// 迭代顺序决定了落定顺序
let p3 = Promise.race([
Promise.resolve(5),
Promise.resolve(6),
Promise.resolve(7)
]);
setTimeout(console.log, 0, p3); // Promise <resolved>: 5
  1. 如果有一个期约拒绝,只要它是第一个落定的,就会成为拒绝合成期约的理由,之后再拒绝的期约不会影响最终期约的拒绝理由,不过,这并不影响所有包含期约正常的拒绝操作,与Promise.all()类似,合成的期约会静默处理所有包含期约的拒绝操作
1
2
3
4
5
6
7
8
9
10
// 虽然只有第一个期约的拒绝理由会进入
// 拒绝处理程序,第二个期约的拒绝也
// 会被静默处理,不会有错误跑掉
let p = Promise.race([
Promise.reject(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);

p.catch((reason) => setTimeout(console.log, 0, reason)); // 3
// 没有未处理的错误

串行Promise合成

Promise的一个主要特性是:异步产生值并将其传给处理程序,基于后续期约使用之前期约的返回值来串联期约是期约的基本功能

参考函数合成,我们也可以将Promise进行合成

1
2
3
4
5
6
7
8
9
10
11
function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
function addTen(x) {
return Promise.resolve(x)
.then(addTwo)
.then(addThree)
.then(addFive);
}

addTen(8).then(console.log); // 18
  • reduce改进
1
2
3
4
5
6
7
function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}

function addTen(x) {
return [addTwo, addThree, addFive].reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}
  • 提炼出通用函数
1
2
3
4
5
6
7
8
9
10
11
function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}

function compose(...fns) {
return (x) => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}

let addTen = compose(addTwo, addThree, addFive);

addTen(8).then(console.log); // 18

知识补充

手写原理 | Promise.all

手写原理 | Promise.race

手写原理 | Promise.allSelected

参考

Promises/A+

ES6增加了对Promise/A+规范的完善支持,即Promise实例

Promise基础

1
new Promise(executor)

Promise可以通过new操作符来实例化,创建新Promise的时候需要传入执行器(executor)函数作为参数

1
2
let p = new Promise(() => {});   // 这里使用一个空函数进行模拟
console.log(p) // Promise <pending>

Promise状态

  • Promise是一个有状态的对象,Promise可能处于如下状态

    • 待定(pending)
    • 兑现(fulfilled或者rosolved)
    • 拒绝(rejected)
  • Promise的初始状态是Pending,在Pending状态下,Promise的状态可变为fulfilled或者rejected

  • Promise的状态一经改变不可逆!!!

  • Promise的状态是私有的,是不能直接通过JavaScript检测到的(这主要是为了避免根据读取到的Promise状态,以同步方式处理Promise对象)

  • Promise的也不能为外部JavaScript代码修改(Promise故意将异步行为封装起来,从而隔离外部的同步代码)

Promise用途

(1)改变状态

Promise可以用于抽象地表示一个异步操作,Promise的状态可以表示Promise是否完成,Promise的状态一经改变不可撤销和逆转

1
2
3
4
5
6
let p = new Promise((resolve, reject) => {
resolve();
rejece(); // 没有效果
});

console.log(p); // Promise {<fulfilled>: undefined}

为了避免Promise卡在某个状态,可以添加一个定时器退出功能

1
2
3
4
5
6
7
8
9
10
11
let p = new Promise((resolve, reject) => {
setTimeout(() => {
reject();
}, 10000); // 10秒后调用reject()
// 执行函数的逻辑
});

setTimeout(() => console.log(p), 0); // Promise {<pending>}
setTimeout(() => console.log(p), 11000);
// 10秒后输出 Uncaught (in promise)
// 11秒后输出 Promise {<rejected>: undefined}

因为期约的状态只能改变一次,所以这里的超时拒绝逻辑中可以放心地设置让期约处于待定状态的最长时间

如果执行器中的代码在超时之前已经解决或拒绝,那么超时回调再尝试拒绝也会静默失败(Nice!)

(2)回调处理

Promise封装的异步操作会实际生成某个值,而程序期待Promise状态改变时可以访问这个值,相应地,如果Promise被拒绝,程序就会期待Promise状态改变时可以拿到拒绝的理由

每个Promise只要状态改变为fulfilled,就会有一个私有的内部(value)

每个Promise只要状态改变为rejected,就会有一个私有的内部理由(reason)

无论是值还是理由,都是包含原始值或对象的不可修改的引用,二者都是可选的,而且默认值为undefined

在Promise到达某个落定状态时执行的异步代码始终会收到这个值或理由

通过执行函数控制Promise状态

因为Promise的状态是私有的,所以只能在内部进行操作, 内部操作在Promise的执行器函数中完成

执行器函数主要有两大职责:

  • 初始化Promise的异步行为
  • 控制状态的最终转换【调用resolve()和reject()实现
1
2
3
4
5
let p1 = new Promise((resolve, reject) => resolve());
console.log(p1); // Promise {<fulfilled>: undefined} 因为resolve并没有传入值,所以为undefined

let p2 = new Promise((resolve, reject) => reject());
console.log(p2); // Promise {<rejected>: undefined}
  • 执行器函数是同步执行的,因为执行器函数是Promise的初始化状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
new Promise(() => setTimeout(() => {
console.log('executor');
}, 0))

setTimeout(() => console.log('promise initialized'), 0)

/*
如果Promise是同步执行的,打印结果为:
executor
promise initialized

如果Promise是异步执行的,打印结果为:
promise initialized
executor
*/


/*
实际打印结果为:
executor
promise initialized
*/
  • 添加setTimeout可以推迟切换状态
1
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000));

Promise静态方法

(1)Promise.resolve()

Promise.resolve()可以实例化一个解决的Promise

1
2
3
// p1 和 p2 是等价的
let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();
  • Promise.resolve()的参数对应着解决的Promise的值,Promise.resolve()可以把任何值都转换为一个Promise,传入的参数可能会有如下情况

    • 定值 => 解析或者包装后的Promise对象
    • Promise对象 => Promise对象本身(相当于一个空包装)【幂等】
    • thenable => 返回的Promise会跟随着thenable对象,采用它的最终状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 定值
    let p1 = Promise.resolve(2);
    console.log(p1); // Promise {<fulfilled>: 2}

    // 多余的参数会忽略
    let p2 = Promise.resolve(4);
    console.log(p2); // Promise {<fulfilled>: 4}

    // 空包装
    let p3 = Promise.resolve(p2);
    console.log(p3 === p2); // true
  • Promise.resolve()可以包装任何非Promise的值,包括错误对象,并将其转换为解决的Promise

1
2
3
4
// 可能会导致不符合预期的行为
let p = Promise.resolve(new Error('foo'));

console.log(p); // Promise {<fulfilled>: Error: foo
  • Promise.resolve()是一个幂等方法
1
2
3
4
5
6
7
let p = Promise.resolve(7);

let p2 = Promise.resolve(p);
let p3 = Promise.resolve(p);

console.log(p === p2); // true
console.log(p === p3); // true

(2)Promise.reject()

Promise.reject()可以实例化一个拒绝的Promise并抛出一个异步错误(这个错误不能通过try/catch捕获,而只能通过拒绝处理程序捕获)

1
2
3
// 下面两个实例等价
let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject();
  • Promise.reject()的参数对应着拒绝的Promise的理由

    1
    2
    let p1 = Promise.rereject(3);
    console.log(p1); // Promise {<rejected>: 3}
  • Promise.reject()不是一个幂等方法,如果给它传一个Promise,这个Promise会成为拒绝的理由

    1
    2
    3
    4
    5
    let p1 = Promise.resolve(3);

    let p2 = Promise.reject(p1);

    console.log(p2); // Uncaught (in promise) Promise {<fulfilled>: 3}

Promise同步、异步执行的二元性

1
2
3
4
5
6
7
8
9
10
11
try {
throw new Error('foo');
} catch(e) {
console.log(e); // Error:foo
}

try {
Promise.reject(new Error('bar'))
} catch(e) {
console.log(e); // Uncaught (in promise) Error: bar
}

第二个try/catch没有捕获到错误是因为它没有通过异步模式捕获错误,这是什么意思呢?前面说过:Promise的状态是私有的,是不能直接通过JavaScript检测到的(这主要是为了避免根据读取到的Promise状态,以同步方式处理Promise对象),拒绝Promise的错误并没有抛出到同步代码的线程里,所以try/catch没有捕获到错误,准确地说,要捕获错误就需要用Promise的方法【下面就开始介绍Promise的方法】

Promise的实例方法

实现Thenable接口

在ECMAScript 暴露的异步结构中,任何对象都有一个then()方法,这个方法被认为实现了Thenable 接口

1
2
3
class MyThenable {
then() {}
}

Promise.prototype.then()

Promise.then()返回一个新的Promise实例

Promise.then()最多接收两个参数(可选):onResloved处理程序onRejected处理程序,分别在Promise进入“兑现”和“拒绝”状态时执行

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

function onRejceted(id) {
console.log('reject', id);
}

let p1 = new Promise((resolve, reject) => setTimeout(resolve(), 3000))
let p2 = new Promise((resolve, reject) => setTimeout(reject(), 3000))

p1.then(() => onResolved('p1'),
onRejceted('p1'))

p2.then(() => onResolved('p2'),
onRejceted('p2'))

// 3秒后
// resolve p1
// reject p2

因为Promise的状态不可撤销,所以这两个方法一定是互斥

  • 传给then()的任何非函数类型的参数都会被忽略
  • 如果只想提供onRejected参数,那么就要在onResolved的位置写上null或者undefined

(1)onResolve处理程序

  1. Promise.then()返回一个新的Promise实例,这个新Promise的实例是基于onResolved处理程序的返回值构建的,也就是说,该处理程序的返回值会通过Promise.resolve()去包装来生成新的Promise实例

  2. 那么onResolve()处理程序在不同情况下,Promise新实例的情况也不同,主要分为以下几种情况:

    • 没有提供onResolve处理程序 => 会沿用上一个Promise

    • 提供onResolved处理程序但是没有显式的返回语句 => Promise.resolve()会包装默认返回值为undefined

    • 提供onResolved处理程序且有有显式的返回语句 => Promise.resolve()会包装这个【根据不同值的情况,具体之前讲过了,可以看Promise.resolve()那块】

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 p1 = Promise.resolve(3);    // 此时p1 为  Promise: {<fulfilled>: 3}

// 没有onResolved处理程序
let p2 = p1.then();

// 这个打印p2,必须套上setTimeout,不知道为啥的请自觉补课Even loop
setTimeout(() => {
console.log(p2);
}) // 会沿用上一个Promise p2 为 Promise: {<fulfilled>: 3}

// 提供onResolved处理程序但是没有显式的返回语句 ,Promise.resolve()会包装默认返回值为undefined
let p3 = p1.then(() => {
console.log(3);
// 没有return
}, null);
setTimeout(() => {
console.log(p3);
}) // 值为undefined p3 为 Promise: {<fulfilled>: undefined}

// 提供onResolved处理程序且有有显式的返回语句, Promise.resolve()会包装这个值
let p4 = p1.then(() => 4, null);
setTimeout(() => {
console.log(p4);
}) // 值为4 p4 为 Promise: {<fulfilled>: 4}

let p5 = p1.then(() => Promose.resolve(5), null);
setTimeout(() => {
console.log(p5);
})// 值为5 p5 为 Promise: {<fulfilled>: 5}
  1. onResolved处理程序遇到错误,分为两种情况

    • 抛出异常 => 返回拒绝的Promise

    • 返回错误值 => 不会拒绝而是会把错误值包装成一个解决的Promise

1
2
3
4
5
let p1 = Promise.resolve(1);

let p2 = p1.then(() => {throw new Error(2)}); // Uncaught (in promise) Error: 2

let p3 = p1.then(() => new Error(3)); // p3是一个新的Promise Promise {<fulfilled>: new Error(3)}

(2)onReject处理程序

  1. onRejected处理程序返回的值也会被Promise.resolve()包装,在捕获错误后不抛出异常,而是返回一个解决的Promise
  2. onRejeced处理程序在不同情况下,Promise新实例的情况也不同,主要分为以下几种情况:
    • 没有提供onRejected处理程序 => 会沿用上一个Promise
    • 提供onRejected处理程序但是没有显式的返回语句 => Promise.resolve()会包装默认返回值为undefined
    • 提供onRejected处理程序且有有显式的返回语句 =>Promise.resolve()会包装这个原因
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
let p1 = Promise.reject(3);    // 此时p1 为  Promise: {<rejected>: 3}

// 没有onRejected处理程序
let p2 = p1.then();

setTimeout(() => {
console.log(p2);
}) // 会沿用上一个Promise p2 为 Promise: {<rejected>: 3}

// 提供onRejected处理程序但是没有显式的返回语句 , Promise.resolve()会包装默认返回值为undefined
let p3 = p1.then(null, () => {
console.log(3);
// 没有return
});
setTimeout(() => {
console.log(p3);
}) // 原因为undefined p3 为 Promise: {<fulfilled>: undefined}

// 提供onRejected处理程序且有有显式的返回语句, Promise.resolve()会包装这个原因
let p4 = p1.then(null, () => 4);
setTimeout(() => {
console.log(p4);
}) // 原因为4 p4 为 Promise: {<fulfilled>: 4}

let p5 = p1.then(null, () => Error(5));
setTimeout(() => {
console.log(p5);
})// 原因为5 p5 为 Promise: {<fulfilled>: Error(5)}

补课:手写原理 | Promise.then

Promise.prototype.catch()

Promise.prototype.catch()方法用于给期约添加拒绝处理程序

这个方法只接收一个参数:onRejected 处理程序

事实上,这个方法就是一个语法糖,调用它就相当于调用Promise.prototype.then(null, onRejected)

1
2
3
4
5
6
7
8
let p1 = Promise.reject();
let onRejected = function(e) {
setTimeout(() => console.log('rejected'), 0)
}

// 下面两者处理是一样的
let p2 = p1.then(null, onRejected); // rejected
let p3 = p1.catch(onRejected); // rejected

Promise.catch()返回一个新的Promise实例

1
2
3
4
5
6
7
8
9
10
11
12
let p1 = new Promise(() => {});
let p2 = p1.catch();

setTimeout(() => {
console.log(p1) // Promise {<pending>}
}, 0);

setTimeout(() => {
console.log(p2) // Promise {<pending>}
}, 0);

console.log(p1 === p2); // false

在返回新期约实例方面,Promise.prototype.catch()的行为与Promise.prototype.then()onRejected 处理程序是一样的

Promise.prototype.finally()

Promise.prototype.finally()方法用于给期约添加onFinally 处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行

  1. onFinally 处理程序无法知道Promise的状态是fulfilled还是rejected,所以这个方法主要用于清理代码
1
2
3
4
5
6
7
8
9
10
11
let p1 = Promise.resolve();
let p2 = Promise.reject();

let onFinally = function() {
setTimeout(() => {
console.log('Finally')
}, 0);
}

p1.finally(onFinally); // 'Finally'
p2.finally(onFinally); // 'Finally'
  1. Promise.prototype.finally()返回一个新的Promise实例
1
2
3
4
5
let p1 = Promise.resolve();

let p2 = p1.finally();

console.log(p1 === p2); // false;
  1. onFinally 处理程序和 onResolved和onRejected不同,因为onFinally被设计为一个状态无关的方法,所以在大多数情况下它将表现为父Promise的传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let p1 = Promise.resolve('foo');

// 以下的情况都会往后面传
let p2 = p1.finally();
let p3 = p1.finally(() => {});
let p4 = p1.finally(() => undefined);
let p5 = p1.finally(() => Promise.resolve());
let p6 = p1.finally(() => 'bar');
let p7 = p1.finally(() => Promise.resolve('bar'));
let p8 = p1.finally(() => Error('qux'));

// 打印结果
setTimeout(() => console.log(p2), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p3), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p4), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p5), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p6), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p7), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p8), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p9), 0); // Promise <resolved>: foo
  1. 如果返回的是一个待定的Promise,或者onFinally处理程序抛出了错误(显式抛出或返回了一个拒绝期约),则会返回相应的期约(待定或拒绝)
1
2
3
4
5
6
7
8
9
10
11
12
13
let p1 = Promise.resolve('foo');

// Promise.resolve()保留返回的期约
let p9 = p1.finally(() => new Promise(() => {}));
let p10 = p1.finally(() => Promise.reject());
// Uncaught (in promise): undefined

setTimeout(console.log, 0, p9); // Promise <pending>
setTimeout(console.log, 0, p10); // Promise <rejected>: undefined

let p11 = p1.finally(() => { throw 'baz'; });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p11); // Promise <rejected>: baz

非重入Promise方法

当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行

跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行

即使期约一开始就是与附加处理程序关联的状态,执行顺序也是这样的

这个特性由JavaScript 运行时保证,被称为“非重入”(non-reentrancy)特性

感觉这部分只要搞清楚Even Loop就可以了,所以这里略过~

邻近处理程序的执行顺序

如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行

无论是then()、catch()还是finally()添加的处理程序都是如此

这个执行顺序主要是遵循了队列的先进先出原则,掠过~

传递解决值和拒绝理由

到了落定状态后,期约会提供其解决值(如果兑现)或其拒绝理由(如果拒绝)给相关状态的处理程序

拿到返回值后,就可以进一步对这个值进行操作

在执行函数中,解决的值和拒绝的理由是分别作为resolve()和reject()的第一个参数往后传的,然后,这些值又会传给它们各自的处理程序,作为onResolved 或onRejected 处理程序的唯一参数

把这张图理顺~问题不大

拒绝Promise和拒绝错误处理

拒绝期约类似于throw()表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理

  1. 在期约的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由
1
2
3
4
5
6
7
8
9
let p1 = new Promise((resolve, reject) => reject(Error('foo')));
let p2 = new Promise((resolve, reject) => { throw Error('foo'); });
let p3 = Promise.resolve().then(() => { throw Error('foo'); });
let p4 = Promise.reject(Error('foo'));

setTimeout(console.log, 0, p1); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p2); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p3); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p4); // Promise <rejected>: Error: foo
  1. 期约可以以任何理由拒绝,包括undefined,但最好统一使用错误对象,这样做主要是因为创建错误对象可以让浏览器捕获错误对象中的栈追踪信息,而这些信息对调试是非常关键的

  2. 异步错误只能通过异步的onRejected处理程序进行处理【还记得之前说的:Promise的状态是私有的】

1
2
3
4
5
6
7
// 不正确
Promise.reject(Error('foo')).catch(e => {})

// 不正确
try {
Promise.reject(Error('foo'))
} catch(e) {}
  1. then()catch()onRejected 处理程序在语义上相当于try/catch,出发点都是捕获错误之后将其隔离,同时不影响正常逻辑执行,为此,onRejected 处理程序的任务应该是在捕获异步错误之后返回一个解决的期约
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console.log('begin synchronous execution');
try {
throw Error('foo');
} catch(e) {
console.log('caught error', e);
}
console.log('continue synchronous execution');
// begin synchronous execution
// caught error Error: foo
// continue synchronous execution


new Promise((resolve, reject) => {
console.log('begin asynchronous execution');
reject(Error('bar'));
}).catch((e) => {
console.log('caught error', e);
}).then(() => {
console.log('continue asynchronous execution');
});
// begin asynchronous execution
// caught error Error: bar
// continue asynchronous execution

手写原理 | Promise.finally

知识补充

手写原理 | Promise

手写原理 | Promise.then

手写原理 | Promise.finally

参考

Promises/A+

生成器

生成器是ES6新增的解构,拥有在一个函数块内暂停和恢复代码执行的能力

生成器基础

生成器的形式是一个函数,函数名称前面加一个星号(*)【星号不受两侧空格影响】表示它是一个生成器

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 * generatorFn() {}

// 生成器函数表达式
let generatorFn = function *() {}

// 作为对象字面量方法的生成器函数
let person = {
* generatorFn() {}
}

// 作为类实例方法的生成器函数
class Person {
* generatorFnn() {}
}

// 作为类静态方法的生成器函数
class Person {
static * generatorFn() {}
}
  • 调用生成器函数会生成一个生成器对象

    • 生成器对象一开始处于暂停状态suspended
  • 生成器方法也实现了Iterator接口,因此也可以调用next()方法,一旦调用这个方法,就会让生成器开始或者恢复执行(相当于激活了~)

    • next()方法返回值类似于迭代器,有一个done属性和一个value属性

    • 函数体为空的生成器中间不会停留,调用一次next()就会让生成器达到done:true状态

      1
      2
      3
      4
      5
      6
      function *generatorFn() {}   // 定义一个生成器对象,生成器对象是一个函数

      let g = generatorFn(); // 调用生成器对象

      console.log(g); // generatorFn {<suspended>} 生成器对象处于暂停状态
      console.log(g.next()); // {value: undefined, done: true}
    • value属性是生成器的返回值,默认为undefined,可以通过生成器函数的返回值指定

      1
      2
      3
      4
      5
      6
      7
      8
      function *generatorFn() {
      return 'foo'
      }

      let g = generatorFn();

      console.log(g); // generatorFn {<suspended>}
      console.log(g.next()); // {value: 'foo', done: true}
  • 生成器函数只会在初次调用next()方法后开始执行

    1
    2
    3
    4
    5
    6
    7
    8
    function *generatorFn() {
    console.log('foo');
    }

    let g = generatorFn(); // 并不会打印foo

    console.log(g); // generatorFn {<suspended>}
    g.next() // 'foo'
  • 生成器对象实现了Iterator接口,它们默认的迭代器是自引用的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function *generatorFn() {}

    console.log(generatorFn); // ƒ *generatorFn() {}
    console.log(generatorFn()[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }
    console.log(generatorFn()); // generatorFn {<suspended>}
    console.log(generatorFn()[Symbol.iterator]()); // generatorFn {<suspended>}

    const g = generatorFn();
    console.log(g === g[Symbol.iterator]()); // true

通过yield中断执行

yield关键字可以让生成器停止和开始执行

yield关键字只能在生成器函数内部使用

yield可以作为函数的中间返回语句使用,yield关键字可以作为函数的中间参数使用,yield关键字可以同时用于输入和输出

  • 生成器函数在遇到yield关键字之前正常执行,遇到这个关键字之后,执行会停止,函数作用域的状态会被保留,停止执行的生成器函数只能通过在生成器对象上调用next()方法来恢复执行
1
2
3
4
5
6
7
8
function *generatorFn() {
yield;
}

let g = generatorFn();

console.log(g.next()); // {done: false, value: undefined}
console.log(g.next()); // {done: false, value: undefined}
  • yield关键字有点像函数中间的返回语句,它生成的值会出现在next()方法返回的对象里(不同的是:通过yield关键字退出的生成器函数会处在done:false状态;通过retrun关键字退出的生成器函数会处于done:true状态)
1
2
3
4
5
6
7
8
9
10
11
function *generatorFn() {
yield 'foo';
yield 'bar';
return 'baz';
}

let g = generatorFn();

console.log(g.next()); // {done: false, value: 'foo'}
console.log(g.next()); // {done: false, value: 'bar'}
console.log(g.next()); // {done: true, value: 'baz'}
  • 生成器函数内部的执行流程会针对每个生成器对象区分作用域,在一个生成器对象上调用next()不会影响其他生成器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function *generatorFn() {
yield 'foo';
yield 'bar';
return 'baz';
}

let g1 = generatorFn();
let g2 = generatorFn();

console.log(g1.next()); // {done: false, value: 'foo'}
console.log(g2.next()); // {done: false, value: 'foo'}

console.log(g1.next()); // {done: false, value: 'bar'}
console.log(g2.next()); // {done: false, value: 'bar'}
(1)生成器对象作为可迭代对象
1
2
3
4
5
6
7
8
9
10
11
12
13
function *generatorFn() {
yield 1;
yield 2;
yield 3;
}

for (const x of generatorFn()) {
console.log(x);
}

// 1
// 2
// 3

应用场景:我们需要定义一个可迭代对象,而它会产生一个迭代器,这个迭代器会执行指定的次数。使用生成器,可以通过一个简单的循环来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
function *nTimes(n) {   // 这个函数可以控制迭代循环的次数
while (n--) { // 当n为0时,while判断返回为false,循环退出
yield;
}
}

for (let _ of nTimes(3)) {
console.log('foo');
}

// foo
// foo
// foo
(2)使用yield实现输入和输出

注意: 第一次调用next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// yield关键字可以作为函数的中间参数使用
function *generatorFn(initial) {
console.log(initial);
console.log(yield);
console.log(yield);
}

let g = generatorFn('foo'); // 这时候生成器处于暂停状态,是不会输出任何结果的,必须使用next激活
g.next('bar') // foo 第一次调用next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数
g.next('baz') // baz
g.next('qux') // qux

// yield关键字可以同时用于输入和输出
function *generatorFn() {
return yield 'foo';
}

let g = generatorFn(); // generatorFn {<suspended>}

console.log(g.next()); // 激活生成器函数 {value: 'foo', done: false}
console.log(g.next('bar')); // {value: 'bar', done: true}

因为函数必须对整个表达式求值才能确定要返回的值,所以它在遇到yield关键字时暂停执行并计算出要产生的值:"foo",下一次调用next()传入了"bar",作为交给同一个yield的值,然后这个值被确定为本次生成器函数要返回的值

(3)yield关键字可以允许使用多次

应用场景1:定义一个生成器函数,它会根据配置的值迭代相应次数并产生迭代的索引

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
// 实现1
function * nTimes(n) {
for (let i = 0; i < n; i++) {
yield i;
}
}

for (let x of nTimes(3)) {
console.log(x);
}

// 0
// 1
// 2


// 实现2
function * nTimes(n) {
let i = 0;
while (n--) {
yield i++;
}
}

for (let x of nTimes(3)) {
console.log(x);
}

// 0
// 1
// 2

应用场景2:使用生成器实现范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function *range(start, end) {
while (end > start) {
yield start;
start++;
}
}

for (let x of range(4,7)) {
console.log(x);
}

// 4
// 5
// 6

应用场景3:使用生成器实现填充数组

1
2
3
4
5
6
7
function *zeros(n) {
while (n--) {
yield 0;
}
}

console.log(Array.from(zeros(8))) // [0, 0, 0, 0, 0, 0, 0, 0]
(4)产生可迭代对象

可以使用星号(*)【星号两侧的空格不影响其行为】增强yield的值,让它能够迭代一个可迭代对象,从而一次产出一个值

  • yield*实际上只是将一个可迭代对象序列化为一连串可以单独产出的值
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
// 等价的generatorFn
// function * generatorFn() {
// for (const x of [1, 2, 3]) {
// yeild x;
// }
// }

function * generatorFn() {
yield * [1,2,3];
}

let g = generatorFn();

console.log(g.next().value); // 1
console.log(g.next().value); // 2
console.log(g.next().value); // 3

// 或者是
let g1 = generatorFn();

for (let x of g) {
console.log(x);
}

// 1
// 2
// 3
  • yield*的值是关联迭代器返回done:true时的value属性

    • 对于普通的迭代器来说,这个值是undefined
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function * generatorFn() {
    console.log('iter value:', yield*[1,2,3])
    }

    for (const x of generatorFn()) {
    console.log(x);
    }

    // 1
    // 2
    // 3
    // iter value: undefined
    • 对于生成器函数产生的迭代器来说,这个值就是生成器函数返回的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function *innerGeneratorFn() {
    yield 'foo';
    return 'bar';
    }

    function *outerGeneratorFn(genObj) {
    console.log('iter value:', yield * innerGeneratorFn())
    }

    for (const x of outerGeneratorFn()) {
    console.log('value:', x);
    }

    // value: foo
    // iter value: bar
(5)使用yield*实现递归算法

实现递归操作是yield*最有用的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function * nTimes(n) {
if (n > 0) {
yield * nTimes(n-1);
yield n - 1;
}
}

for (const x of nTimes(3)) {
console.log(x);
}

// 0
// 1
// 2

应用场景:使用生成器测试一个随机的双向图是否联通

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
// 创建节点 节点的邻里关系
class Node {
constructor(id) {
this.id = id;
this.neighbors = new Set();
}

connect(node) {
if (node !== this) {
this.neighbors.add(node);
node.neighbors.add(this);
}
}
}

// 随机连接节点
class RandomGraph {
constructor(size) {
this.nodes = new Set();

// 创建节点
for (let i = 0; i < size; i++) {
this.nodes.add(new Node(i))
}

// 随机连接节点
const threshold = 1 / size;
for (const x of this.nodes) {
for (const y of this.nodes) {
if (Math.random() < threshold) {
x.connect(y); // 连接成功
}
}
}
}

// 这个方法仅用于调试
print() {
for (const node of this.nodes) {
const ids = [...node.neighbors]
.map(n => n.id)
.join(',');

console.log(`${node.id}:${ids}`);
}
}
}

const g = new RandomGraph(6);

g.print();

// 0:1,3,4,5
// 1:0,3,4
// 2:4
// 3:1,0
// 4:2,0,1
// 5:0

生成器函数必须接收一个可迭代对象,产出该对象中的每一个值,并且对每个值进行递归,这个实现可以用来测试某个图是否连通,即是否没有不可到达的节点。只要从一个节点开始,然后尽力访问每个节点就可以了;结果就得到了一个非常简洁的深度优先遍历:

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
// 创建节点 节点的邻里关系
class Node {
constructor(id) {
......
}

connect(node) {
......
}
}

// 随机连接节点
class RandomGraph {
constructor(size) {
......
}

// 这个方法仅用于调试
print() {
......
}

isConnected() {
const visitedNodes = new Set();

function *traverse(nodes) {
for (const node of nodes) {
if (!visitedNodes.has(node)) {
yield node;
yield * traverse(node.beighbors);
}
}
}

// 取得集合中的第一个节点
const firstNode = this.nides[Symbol.iterator]().next.value();

// 使用递归生成器迭代每一个节点
for (const node of traverse([firstNode])) {
visitedNodes.add(node);
}

return visitedNodes.size === this.nodes.size;
}
}

生成器作为默认迭代器

因为生成器实现了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
class Foo {
constructor() {
this.values = [1,2,3];
}

* [Symbol.iterator]() {
yield * this.values;
}
}

const f = new Foo();

for (const x of f) {
console.log(x);
}

// 1
// 2
// 3

/*
for-of 循环调用了默认迭代器(它恰好又是一个生成器函数)并产生了一个生成器对象
这个生成器对象是可迭代的,所以完全可以在迭代中使用
*/

提前终止生成器

return() 和 throw() 方法都可以用于强制生成器进入关闭状态

(1)return()

return()方法会强制生成器进入关闭状态,提供给return的值就是终止迭代器对象的值

1
2
3
4
5
6
7
8
9
10
function * generatorFn() {
for (const x of [1,2,3]) {
yield x;
}
}

const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.return(4)); // {value: 4, done: true};
console.log(g); // generatorFn {<closed>}
  • 与迭代器不同,所有生成器对象都有return()方法,只要通过它进入关闭状态,就无法恢复

  • 后续调用next()会显示done: true 状态,而提供的任何返回值都不会被存储或传播

1
2
3
4
5
6
7
8
9
10
11
12
function * generatorFn() {
for (const x of [1,2,3]) {
yield x;
}
}

const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.return(4)); // {value: 4, done: true};
console.log(g.next()); // {value: undefined, done: true};
console.log(g.next()); // {value: undefined, done: true};
console.log(g.next()); // {value: undefined, done: true};
  • for-of 循环等内置语言结构会忽略状态为done: trueIteratorObject 内部返回的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function * generatorFn() {
for (const x of [1,2,3]) {
yield x;
}
}

const g = generatorFn();

for (let x of g) {
if (x > 1) {
g.return(4);
}
console.log(x);
}

// 1
// 2
(2)throw()

throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中,如果错误未被处理,生成器就会关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function * generatorFn() {
for (const x of [1,2,3]) {
yield x;
}
}

const g = generatorFn();

console.log(g); // generatorFn {<suspended>}

try {
g.throw('foo');
} catch(e) { // 错误会被catch捕获
cnosole.log(e); // foo
}
console.log(g); // generatorFn {<closed>}
  • 假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行,错误处理会跳过对应的yield,因此在这个例子中会跳过一个值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function * generatorFn() {
for (const x of [1,2,3]) {
try {
yield x;
} catch(e) {
console.log(e);
}
}
}

const g = generatorFn();

console.log(g.next()); // {value: 1, done: true};
g.throw('foo'); // foo
console.log(g.next()); // {value: 2, done: true};

/*
跳过了value2
*/

在这个例子中,生成器在try/catch块中的yield关键字处暂停执行。在暂停期间,throw()方法向生成器对象内部注入了一个错误:字符串"foo"。这个错误会被yield关键字抛出。因为错误是在生成器的try/catch块中抛出的,所以仍然在生成器内部被捕获可是,由于yield抛出了那个错误,生成器就不会再产出值2。此时,生成器函数继续执行,在下一次迭代再次遇到yield关键字时产出了值3

注意:如果生成器对象还没有开始执行,那么调用throw()抛出的错误不会在函数内部被捕获,因为这相当于在函数块外部抛出了错误

1
2
3
4
5
6
7
8
9
10
11
12
function * generatorFn() {
for (const x of [1,2,3]) {
try {
yield x;
} catch(e) {
console.log(e);
}
}
}

const g = generatorFn();
g.throw('foo'); // Uncaught foo 相当于在外部抛出了错误

理解迭代

迭代:按照顺序返回多次执行一段程序,通常会有明确的终止条件

迭代会在一个有序集合上进行,“有序”是指集合中所有的项都可以按照既定的顺序被遍历到,特别是开始项和结束项有明确的定义

循环迭代的弊端:

  • 迭代之前需要事先知道如何使用数据结构:数组中的每一项都只能先通过引用取得数组对象,然后再通过[]操作符取得特定索引位置上的项。这种情况并不适用于所有数据结构
  • **遍历顺序并不是数据结构固有的:**通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构

ES5新增了Array.prototyoe.forEach方法,但是仍然有弊端:这个方法解决了单独记录索引和通过数组对象取得值的问题,不过,没有办法标识迭代何时终止,因此这个方法只适用于数组,而且回调结构也比较笨拙

因此迭代器模式诞生

迭代器模式

迭代器模式描述了一个方案,即可以把有些结构称为“可迭代对象”(iterable)(比如:数组或者集合这样的集合类型对象,它们包含的元素都是有限的,并且具有无歧义的遍历顺序),因为它们实现了正式的Iterable 接口,而且可以通过迭代器Iterator 消费

  • 可迭代对象不一定是集合对象,也可以是仅仅具有类似数组行为的其他数据结构,比如计数循环,该循环中生成的值是暂时性的,但循环本身是在执行迭代,计数循环和数组都具有可迭代对象的行为

  • 任何实现Iterable接口的数据结构都可以被实现Iterator接口的结构“消费”(consume),迭代器(iterator)是按需创建的一次性对象,每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的API,迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值,这种概念上的分离正是Iterable Iterator 的强大之处

可迭代协议

实现Iterable接口(可迭代协议)要求同时具备两种能力:

  • 支持迭代的自我识别能力
  • 创建实现Iterator接口的对象的能力

ECMAScript中,这意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的**Symbol.iterator** 作为键,这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器

  • 很多内置类型都实现了Iterable 接口
    • String
    • Array
    • Map
    • arguments对象
    • NodeList等DOM集合类型
  • 检查是否存在默认迭代器可以暴露这个工厂函数
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
let num = 1;
let obj = {};

// 这两种类型没有实现迭代器工厂函数
console.log(num[Symbol.iterator]); // undefined
console.log(obj[Symbol.iterator]); // undefined
let str = 'abc';
let arr = ['a', 'b', 'c'];
let map = new Map().set('a', 1).set('b', 2).set('c', 3);
let set = new Set().add('a').add('b').add('c');
let els = document.querySelectorAll('div');

// 这些类型都实现了迭代器工厂函数
console.log(str[Symbol.iterator]); // f values() { [native code] }
console.log(arr[Symbol.iterator]); // f values() { [native code] }
console.log(map[Symbol.iterator]); // f values() { [native code] }
console.log(set[Symbol.iterator]); // f values() { [native code] }
console.log(els[Symbol.iterator]); // f values() { [native code] }

// 调用这个工厂函数会生成一个迭代器
console.log(str[Symbol.iterator]()); // StringIterator {}
console.log(arr[Symbol.iterator]()); // ArrayIterator {}
console.log(map[Symbol.iterator]()); // MapIterator {}
console.log(set[Symbol.iterator]()); // SetIterator {}
console.log(els[Symbol.iterator]()); // ArrayIterator {}
  • 实际写代码过程中,不需要显式调用这个工厂函数来生成迭代器,实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性

    • 接收可迭代对象的原生语言特性包括:
      • for of 循环
      • 数组解构
      • 扩展运算符
      • Array.from()
      • 创建Set
      • 创建Map
      • Promise.all()接收由Promise组成的可迭代对象
      • Promise.race()接收由Promise组成的可迭代对象
      • yield*操作符,在生成器中的使用

    这些原生语言结构会在后台调用提供的可迭代对象的这个工厂函数,从而创建一个迭代器:

    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
    let arr = ['foo', 'bar', 'baz'];

    // for-of 循环
    for (let el of arr) {
    console.log(el);
    }
    // foo
    // bar
    // baz

    // 数组解构
    let [a, b, c] = arr;
    console.log(a, b, c); // foo, bar, baz

    // 扩展操作符
    let arr2 = [...arr];
    console.log(arr2); // ['foo', 'bar', 'baz']

    // Array.from()
    let arr3 = Array.from(arr);
    console.log(arr3); // ['foo', 'bar', 'baz']

    // Set 构造函数
    let set = new Set(arr);
    console.log(set); // Set(3) {'foo', 'bar', 'baz'}

    // Map 构造函数
    let pairs = arr.map((x, i) => [x, i]);
    console.log(pairs); // [['foo', 0], ['bar', 1], ['baz', 2]]
    let map = new Map(pairs);
    console.log(map); // Map(3) { 'foo'=>0, 'bar'=>1, 'baz'=>2 }
  • 如果对象原型链上的父类实现了Iterable接口,那这个对象也就实现了这个接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class FooArray extends Array {}

    let fooArr = new FooArray('foo', 'bar', 'baz');
    for (let el of fooArr) {
    console.log(el);
    }
    // foo
    // bar
    // baz

迭代器协议

迭代器API使用next()方法在可迭代对象中遍历数据,每次调用next()都会返回一个`IteratorResult1对象,这个对象包含两个属性:

  • done:布尔值,表示是否还可以调用next()取得下一个值,注意:done:true表示耗尽

  • value:包含可迭代对象的下一个值(done:false)或者是undefineddone:true

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 arr = ['foo', 'bar'];

// 迭代器工厂函数
console.log(arr[Symbol.iterator]); // f values() {[native code]}

// 迭代器
let iter = arr[Symbol.iterator]();
console.log(iter); // ArrayIterator();

// 执行迭代
iter.next(); // {done: false, value:'foo'}
iter.next(); // {done: false, value: 'bar'}
iter.next(); // {done: true, value:undefined}

/*
迭代器并不知道怎么可以从可迭代对象取得下一个值,也不知道可迭代对象多大,只要迭代器到达done:true状态,后续调用next()就一直返回同样的值
*/
let arr = ['foo', 'bar'];
let iter = arr[Symbol.iterator]();

// 执行迭代
iter.next(); // {done: false, value:'foo'}
iter.next(); // {done: false, value: 'bar'}
iter.next(); // {done: true, value:undefined}
iter.next(); // {done: true, value:undefined}
iter.next(); // {done: true, value:undefined}
  1. 每个迭代器都表示对可迭代对象的一次性有序遍历,不同迭代器的实例相互之间没有联系,只会独立地遍历可迭代对象
1
2
3
4
5
6
7
8
9
10
let arr = ['foo', 'bar'];

let iter1 = arr[Symbol.iterator]();
let iter2 = arr[Symbol.iterator]();

iter1.next(); // {done: false, value:'foo'}
iter2.next(); // {done: false, value:'foo'}

iter1.next(); // {done: false, value: 'bar'}
iter2.next(); // {done: false, value: 'bar'}
  1. 迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程,如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化
1
2
3
4
5
6
7
8
9
10
let arr = ['foo', 'baz'];
let iter = arr[Symbol.iterator]();

console.log(iter.next()); // { done: false, value: 'foo' }

// 在数组中间插入值
arr.splice(1, 0, 'bar');
console.log(iter.next()); // { done: false, value: 'bar' }
console.log(iter.next()); // { done: false, value: 'baz' }
console.log(iter.next()); // { done: true, value: undefined }

注意:迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象

  1. 显示迭代器和原生迭代器有差别
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
// 这个类实现了可迭代接口(Iterable)
// 调用默认的迭代器工厂函数会返回
// 一个实现迭代器接口(Iterator)的迭代器对象
class Foo {
[Symbol.iterator]() {
return {
next() {
return { done: false, value: 'foo' };
}
}
}
}

let f = new Foo();

// 打印出实现了迭代器接口的对象
console.log(f[Symbol.iterator]()); // { next: f() {} }

// Array 类型实现了可迭代接口(Iterable)
// 调用Array 类型的默认迭代器工厂函数
// 会创建一个ArrayIterator 的实例
let a = new Array();

// 打印出ArrayIterator 的实例
console.log(a[Symbol.iterator]()); // Array Iterator {}

自定义迭代器(手写)

任何实现Iterator接口的对象都可以作为迭代器使用

  1. 初步实现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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 初步实现Iterator接口
class Counter {
constructor(limit) {
this.count = 1;
this.limit = limit;
}

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

[Symbol.iterator]() {
return this;
}
}

let counter = new Counter(3);

/*
let iter = counter[Symbol.iterator]();

console.log(iter); // {count: 1, limit: 3}
console.log(iter.next()); // {done: false, value: 1};
console.log(iter.next()); // {done: false, value: 2};
console.log(iter.next()); // {done: false, value: 3};

*/

console.log(counter); // {count: 1, limit: 3}

for (let i of counter) {
console.log(i)
}

// 1
// 2
// 3



/*
这个迭代器仍有不足的地方,因为它的每个实例都只能迭代一次
*/
console.log(counter) // {count: 4, limit: 3}
// 再次
for (let i of counter) {
console.log(i)
}
// {nothing logged}
  1. 通过闭包改进:实现一个可迭代对象能够创建多个迭代器,必须每创建一个迭代器就对应一个新计数器
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
class Counter {
constructor(limit) {
this.limit = limit;
}

[Symbol.iterator]() {
let count = 1,
limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
}
};
}
}

let counter = new Counter(3);

for (let i of counter) {
console.log(i)
}

// 1
// 2
// 3

for (let i of counter) {
console.log(i)
}

// 1
// 2
// 3

提前终止迭代器

可选的return()方法用于指定在迭代器提前关闭时执行的逻辑,执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器

可能的情况有:

  • for-of 循环通过breakcontinuereturnthrow 提前退出
  • 解构操作并未消费所有值

return()方法必须返回一个有效的IteratorResult对象,简单的情况下,可以只返回{done:true}

  • 在内置结构中,还有值可以迭代,但是不会消费这些值的时候,会自动调用return()方法
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
class Counter {
constructor(limit) {
this.limit = limit;
}

[Symbol.iterator]() {
let count = 1,
limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true };
}
},

return() {
console.log('Exiting early');
return { done: true };
}
};
}
}


let counter1 = new Counter(3);

for (let i of counter1) {
if (i > 2) {
break;
}
console.log(i);
}

// 1
// 2
// Exiting early

let counter2 = new Counter(5);

try {
for (let i of counter2) {
if (i > 2) {
throw 'err';
}
console.log(i);
}
} catch(e) {}
// 1
// 2
// Exiting early

let counter3 = new Counter(5);
let [a, b] = counter3;
// Exiting early
  1. 如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代(比如数组的迭代器就是不能关闭的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let a = [1, 2, 3, 4, 5];
let iter = a[Symbol.iterator]();

for (let i of iter) {
console.log(i);
if (i > 2) {
break
}
}
// 1
// 2
// 3

for (let i of iter) {
console.log(i);
}
// 4
// 5
  1. 因为return()方法是可选的,所以并非所有的迭代器都是可关闭的,要检测某个迭代器是否可关闭,可以测试这个迭代器实例的return属性是不是函数对象,但是,仅仅给一个不可关闭的迭代器增加这个方法并不能让它变可关闭的,这是因为调用return()方法不会强制迭代器进入关闭状态,即便如此,return()方法还是会被调用
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 a = [1, 2, 3, 4, 5];

let iter = a[Symbol.iterator](); // 我们知道数组的迭代器是不可关闭的

iter.return = function() { // 给数组的迭代器增加一个return方法
console.log('Exiting early');
return { done: true };
};


for (let i of iter) {
console.log(i);
if (i > 2) {
break // break满足迭代器调用return()的条件
}
}
// 1
// 2
// 3
// Exiting early 调用return了

for (let i of iter) {
console.log(i);
}
// 4
// 5

继承基础

使用extends关键字就可以继承任何拥有[[Construct]]和原型的对象

  • 不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容)【任何可以解析为一个类或者一个构造函数的表达式都是有效的】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {}

// 继承类
class Women extends Person {}

let w = new Women(); // 新建实例
console.log(w instanceof Women); // true
console.log(w instanceof Person); // true

function Person() {}

// 继承普通的构造函数
class Men extends Person {}

let m = new Men();
console.log(m instanceof Men); // true
console.log(m instanceof Person); // true
  • 派生类可以通过原型链访问到类上的方法和原型上定义的方法,this的值会反映调用相应方法的类或者实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
sayHelloPorototype(name) {
console.log(`${name} say hello, ${this}`);
}

static sayHelloClass(name) {
console.log(`${name} say hello, ${this}`);
}
}

class Women extends Person{}

let p1 = new Person();
let w1 = new Women();

p1.sayHelloPorototype('Katrina'); // Katrina say hello, Person{}
w1.sayHelloPorototype('Jack'); // Jack say hello, Women{}

Person.sayHelloClass('Katrina1'); // Katrina1 say hello, class Person {}
Women.sayHelloClass('Jack1'); // Jack1 say hello, class Women extends Person{}
  • extends也可以在类表达式中使用
1
2
3
4
5
6
7
8
9
10
11
class Person {
sayHelloPorototype(name) {
console.log(`${name} say hello, ${this}`);
}

static sayHelloClass(name) {
console.log(`${name} say hello, ${this}`);
}
}

let Women = class extends Person {}

构造函数、HomeObject和super()

派生类可以通过super关键字引用它们的原型

  • 构造函数:在类构造函数中使用super可以调用父类构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Vehicle {
constructor() {
this.hasEngie = true;
}
}

class Bus extends Vehicle {
constructor() {
super(); // 相当于super.constructor()

// 注意:千万不要在调用super()之前引用this,否则会抛出ReferenceError
console.log(this instanceof Vehicle); // true
console.log(this); // Bus {hasEngie: true}
}
}

new Bus();
  • 静态方法:在静态方法中可以通过super 调用继承的类上定义的静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
class Vehicle {
static identify() {
console.log('vehicle');
}
}

class Bus extends Vehicle {
static identify() {
super.identify();
}
}

Bus.identify(); // 'vehicle'

使用super时要注意的几个问题

  • super只能在派生类的构造函数和静态方法中使用
1
2
3
4
5
6
class Vehicle {
constructor() {
super();
// SyntaxError: 'super' keyword unexpected
}
}
  • 不能单独调用super关键词,要么用它调用构造函数,要么用它引用静态方法
1
2
3
4
5
6
7
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(super);
// SyntaxError: 'super' keyword unexpected here
}
}
  • 调用super()会调用父类构造函数,并将返回的实例赋值给this(也就是this指向实例)
1
2
3
4
5
6
7
8
9
10
11
class Vehicle {}

class Bus extends Vehicle {
constructor() {
super(); // 调用父类构造函数
console.log(this instanceof Vehicle); // true
console.log(this); // Bus{}
}
}

new Bus();
  • super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
1
2
3
4
5
6
7
8
9
10
11
12
13
class Vehicle {
constructor(id) {
this.id = id;
}
}

class Bus extends Vehicle {
constructor(id) {
super(id);
}
}

console.log(new Bus(1234).id); // 1234
  • 如果没有定义派生类构造函数,在实例化派生类时会调用super(),而且会传入所有传给派生类的参数
1
2
3
4
5
6
7
8
9
class Vehicle {
constructor(id) {
this.id = id;
}
}

class Bus extends Vehicle {}

console.log(new Bus(1234).id); // 1234
  • 在类构造函数中,不能在调用super()之前引用this
1
2
3
4
5
6
7
8
9
10
class Vehicle {}

class Bus extends Vehicle {
constructor() {
console.log(this);
}
}
new Bus();
// ReferenceError: Must call super constructor in derived class
// before accessing 'this' or returning from derived constructor
  • 如果在派生类中显式定义了构造函数,则要么必须在其中调用super(),要么必须在其中返回一个对象
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
class Vehicle {}

class Car extends Vehicle {}

class Bus extends Vehicle {
constructor() {
super();
}
}

class Van extends Vehicle {
constructor() {
return {};
}
}

class Jeep extends Vehicle {
constructor() {
name: 'Katrina'
}
}

console.log(new Car()); // Car {}
console.log(new Bus()); // Bus {}
console.log(new Van()); // {}
console.log(new Jeep()); // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

抽象基类

抽象基类:可以供其他类继承,但是本身不会实例化

可以通过判断new.target来阻止实例化

补课:new.target

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 抽象基类
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) { // 就说明抽象基类被用new实例化了
throw new Error('Vehicle cannot be directly instantiated');
}
}
}


// 派生类
class Bus extends Vehicle{} // 抽象基类是可以被继承的

new Bus(); // class Bus {}
new Vehicle(); // class Vehicle {}

// Uncaught Error: Vehicle cannot be directly instantiated
  • 通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法
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
// 抽象基类
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) { // 就说明抽象基类被用new实例化了
throw new Error('Vehicle cannot be directly instantiated');
}

if (!this.foo) {
throw new Error('Inheriting class must define foo()');
};

console.log('success');
}
}

// 派生类
class Bus extends Vehicle {
foo() {}
}


class Van extends Vehicle {}

new Bus(); // success
new Van(); // Inheriting class must define foo()

基础内置类型

类继承内置引用类型方便扩展内置类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SuperArray extends Array {   // 注意:之前提到过extends不仅可以继承一个类,也可以继承普通的构造函数
shuffle() {
// 洗牌算法
for (let i = this.length - 1; i >= 0; i--) {
const j = Math.floor(Math.random() * (i+1));
[this[i], this[j]] = [this[j], this[i]];
}
}
}

let a = new SuperArray(1,2,3,4,5);

console.log(a instanceof SuperArray); // true
console.log(a instanceof Array); // true

console.log(a); // [1, 2, 3, 4, 5]
a.shuffle();
console.log(a); // [3, 1, 2, 5, 4]
  • 有些内置类型的方法会返回新实例,默认情况下,返回实例的类型与原始实例的类型是一致的
1
2
3
4
5
6
7
8
9
class SuperArray extends Array {}

let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x%2))

console.log(a1); // [1, 2, 3, 4, 5]
console.log(a2); // [1, 3, 5]
console.log(a1 instanceof SuperArray); // true
console.log(a2 instanceof SuperArray); // true
  • 如果想覆盖这个默认行为,则可以覆盖Symbol.species访问器,这个访问器决定在创建返回的实例时使用的类
1
2
3
4
5
6
7
8
9
10
11
12
13
class SuperArray extends Array {
static get [Symbol.species]() {
return Array;
}
}

let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x%2))

console.log(a1); // [1, 2, 3, 4, 5]
console.log(a2); // [1, 3, 5]
console.log(a1 instanceof SuperArray); // true
console.log(a2 instanceof SuperArray); // false

类混入

类混入:把不同类的行为集中到一个类

混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类

假设现在有一个需求:如果Person类需要组合ABC,则需要某种机制实现B 继承AC 继承B,而Person再继承C,从而把AB、`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
class Vehicle{}

let FooMixin = (Superclass) => class extends Superclass {
foo() {
console.log('foo');
}
};

let BarMixin = (Superclass) => class extends Superclass {
bar() {
console.log('bar');
}
};

let BazMixin = (Superclass) => class extends Superclass {
baz() {
console.log('baz');
}
};

class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {}

let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz

通过写一个辅助函数,可以把嵌套展开

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
class Vehicle{}

let FooMixin = (Superclass) => class extends Superclass {
foo() {
console.log('foo');
}
};

let BarMixin = (Superclass) => class extends Superclass {
bar() {
console.log('bar');
}
};

let BazMixin = (Superclass) => class extends Superclass {
baz() {
console.log('baz');
}
};


function mix(BassClass, ...Minins) {
return Minins.reduce((prev, curr) => curr(prev), BassClass)
}

class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin);

let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz