前端面试基础知识总结(四):ES6 & 异步

ES6+

ECMAScript5,即 ES5,是 ECMAScript 的第五次修订,于 2009 年完成标准化。
ECMAScript6,即 ES6,是 ECMAScript 的第六次修订,于 2015 年完成,也称 ES2015。ES6 是继 ES5 之后的一次改进,相对于 ES5 更加简洁,提高了开发效率。至此之后,每一年都会出新版本,如 ES7(ES2016)、ES8(ES2017)以此类推。

ES6+大纲

Symbol

ES6 引入了一种新的原始数据类型 Symbol ,表示独一无二的值,最大的用法是用来定义对象的唯一属性名。

Symbol 函数栈不能用 new 命令,因为 Symbol 是原始数据类型,不是对象。可以接受一个字符串作为参数,为新创建的 Symbol 提供描述。

1
2
3
let sy1 = Symbol("KK");
let sy2 = Symbol("kk");
sy1 === sy2; // false,相同参数 Symbol() 返回的值不相等

使用场景

由于每一个 Symbol 的值都是不相等的,所以 Symbol 作为对象的属性名,可以保证属性不重名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let sy = Symbol("key1");

// 写法1
let syObject = {};
syObject[sy] = "kk";
console.log(syObject); // {Symbol(key1): "kk"}

// 写法2
let syObject = {
[sy]: "kk"
};
console.log(syObject); // {Symbol(key1): "kk"}

// 写法3
let syObject = {};
Object.defineProperty(syObject, sy, {value: "kk"});
console.log(syObject); // {Symbol(key1): "kk"}

Symbol 作为对象属性名时不能用.运算符,要用方括号。因为.运算符后面是字符串,所以取到的是字符串 sy 属性,而不是 Symbol 值 sy 属性。

Map 与 Set

Map

Map 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。

Map 与 Object 的区别:

  • Map 的键可以是任意值,而 Object 的键只能是字符串或者 Symbol。
  • Map 中的键值是有序的(FIFO 原则),而添加到对象中的键则不是。
  • 在性能上,Map在频繁增删键值对的场景下表现更好。(原因是Map底层是哈希表,哈希表的核心思想是使用哈希函数将键转换成数组索引,然后在该索引处存储对应的值。)
1
2
3
4
5
let map = new Map([['key1', 'value1']); //default key-value
map.set('key2', "value2");
map.get('key2'); // value2
// 将 Map 转 Array
let outArray = Array.from(myMap);

Set

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

Set 对象存储的值总是唯一的,所以需要判断两个值是否恒等。有几个特殊值需要特殊对待:

  • +0 与 -0 在存储判断唯一性的时候是恒等的,所以重复;
  • undefined 与 undefined 是恒等的,所以重复;
  • NaN 与 NaN 是不恒等的,但是在 Set 中只能存一个。
1
2
3
4
5
let set = new Set([1, 2, 3]); // default value
set.add(3); // set(3) {1, 2, 3}
set.add({a: 1}); // set(4) {1, 2, 3, {a: 1}}
// 用...操作符,将 Set 转 Array
var outArray = [...set];

Set 可以实现数组去重

1
2
var mySet = new Set([1, 2, 3, 4, 4]);
[...mySet]; // [1, 2, 3, 4]

Reflect 与 Proxy

Proxy 与 Reflect 是 ES6 为了操作对象引入的 API 。

Proxy 可以对目标对象的读取、函数调用等操作进行拦截,然后进行操作处理。它不直接操作对象,而是像代理模式,通过对象的代理对象进行操作。

Reflect 可以用于获取目标对象的行为,使用函数的方式实现了 Object 的命令式操作。它的方法与 Proxy 是对应的。

new Proxy(target, handler)

  • target:目标对象
  • handler:声明代理 target 的指定行为。

应用:代理模式

代理模式 (Proxy Pattern)又称委托模式,它为目标对象创造了一个代理对象,以控制对目标对象的访问。
明星就相当于被代理的目标对象(Target),而经纪人就相当于代理对象(Proxy),希望找明星的人是访问者(Visitor),他们直接找不到明星,只能找明星的经纪人来进行业务商洽。主要有以下几个概念:

  • Target: 目标对象,也是被代理对象,是具体业务的实际执行者;
  • Proxy: 代理对象,负责引用目标对象,以及对访问的过滤和预处理;
1
2
3
4
5
const handler = {
get: function(target, propKey, receiver){},
set: function(target, propKey, value, receiver){}
apply: function(target, ctx, args){}
}

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const superStar = {
name: "坤坤",
act: (...actions) => {
console.log("坤坤表演", ...actions);
},
};
const ProxyAssistant = {
name: "assistant",
schedule: (...actions) => {
return new Proxy(superStar.act, {
apply(target, ctx, args) {
console.log("经纪人已通知" + superStar.name);
return Reflect.apply(target, ctx, [...actions]);
},
})();
},
};

ProxyAssistant.schedule("唱", "跳", "RAP", "篮球");
/*
经纪人已通知坤坤
坤坤表演 唱 跳 RAP 篮球
*/

应用:Vue3 的双向数据绑定

详见这篇文章: TODO.

Class 类

使用原型实现类

语法糖,本质为对原型链的二次包装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用class:
class Dog {
constructor(name ,age) {
this.name = name
this.age = age
}

eat() {
console.log('肉骨头真好吃')
}
}
// 使用原型:
function Dog(name, age) {
this.name = name
this.age = age
}
Dog.prototype.eat = function() {
console.log('肉骨头真好吃')
}

类的用法

类定义不会被提升,且不可重复声明。

  • 静态属性:class 本身的属性,即直接定义在类内部的属性,不需要实例化。
    1
    2
    3
    4
    class Example {
    static a = 2; // 新提案
    }
    Example.a = 3; // 改变类的静态属性
  • 公共属性:定义在类上的属性。
    1
    2
    class Example{}
    Example.prototype.a = 2;
  • 实例属性:定义在实例对象上的属性。
    ES6 中实例属性只能通过构造函数中的this.xxx来定义,ES7 提案中可以直接在类里面定义:
    1
    2
    3
    4
    5
    6
    class Example {
    a = 2; // ES7,直接在类中定义属性
    constructor () {
    console.log(this.a);
    }
    }

类的访问(JS和TS)

在 JavaScript 中,类属性在默认情况下是公有的,但可以使用增加哈希前缀#的方法来定义私有类字段,这一隐秘封装的类特性由 JS 自身强制执行。

而在 TypeScript 中,可以使用三种访问修饰符(Access Modifiers),分别是 publicprivateprotected

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中是允许被访问的

值得注意的是:访问修饰符仅在TS层有效,编译成JS后private修饰的字段并没有转化成原生私有字段。

decorator

decorator 是一个函数,用来修改类的行为,在代码编译时产生作用。

3 个参数:target(类的原型对象)、name(修饰的属性名)、descriptor(该属性的描述对象)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function readonly(target, name, descriptor) {
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor; // 必须返回
}
class Example {
@readonly
sum(a, b) {
return a + b;
}
}
// 类似于
// Object.defineProperty(Example.prototype, 'sum', descriptor);

修饰器执行顺序:由外向内进入,由内向外执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Example {
@logMethod(1)
@logMethod(2)
sum(a, b){
return a + b;
}
}
function logMethod(id) {
console.log('evaluated logMethod'+id);
return (target, name, desctiptor) => console.log('excuted logMethod '+id);
}
// evaluated logMethod 1
// evaluated logMethod 2
// excuted logMethod 2
// excuted logMethod 1

类的继承

es5 分两种继承方式:构造函数继承和原型继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Parents(){
this.country = 'CHINA'
}

Parents.prototype.eat = function(food){
console.log('eat ' + food);
}

function Child(){
// 1. 构造函数继承,把this绑定到child上
Parents.apply(this);
this.name = 'little John'
}
// 2. 原型继承
Child.prototype = new Parents();
Child.prototype.constructor = Child;

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
25
class Parents {
constructor(props){
this.name = props.name || 'UNKONWN'
}
eat(){
console.log(this.name + '就喜欢吃')
}
}

// extends实现类的继承
class Child extends Parents {
// props是继承过来的属性,myAttribute是自己的属性
constructor(props) {
super(props) // 模拟构造函数继承,模拟ES5的Parent.apply(this, args)
this.height = props.height; // 自己的私有属性
this.weight = props.weight; // 自己的私有属性
}
info() { //自己私有的方法
console.log(this.name, this.height, this.weight);
}
}

const child = new Child({name: '小明', height: 183, weight: 75 });
child.eat(); // 小明就喜欢吃
child.info(); // 小明 183 75

ES6 模块

AMD、CMD 与 CommonJS

在 ES6 之前,实现模块化的是 RequireJS(AMD)和 SeaJS(CMD):

  • RequireJS(AMD):异步加载 JS 文件,通过 define()函数定义
  • SeaJS(CMD):同步模块

上述这俩都是基于浏览器的模块化,基于 Node 的模块化是 CommonJS 规范,是通过 module.exports 定义。

ES6 模块化特点

ES6 引入模块化,设计思想是:静态加载,在编译时就确定模块的依赖关系。ES6 的模块化分为导出(export)与导入(import)两个模块。

  • ES6 的模块自动开启严格模式,不管你有没有在模块头部加上 use strict;。
  • 模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等。
  • 每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域。
  • 每一个模块只加载一次(是单例的), 若再去加载同目录下同文件,直接从内存中读取。

迭代器

迭代器的概念和遍历过程

迭代器是一种接口,为各种不同的数据结构提供统一的遍历机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

遍历过程:

  1. 通过Symbol.iterator创建迭代器
  2. 通过 next 方法向下迭代,返回当前位置的对象,对象包含了 value 和 done 两个属性
  3. 当 done 为 true 时遍历结束

下面这个例子是模拟next的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return this.hasNext() ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
},
hasNext: function() {
return nextIndex < array.length;
}
};
}

默认 Iterator 接口

一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性。

原生具备 Iterator 接口的数据结构有:Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象。

1
2
3
4
5
6
7
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator](); //得到数组的遍历器对象。
iter.next(); // {value: 'a', done: false}
// String也差不多:
let str = 'hello world';
let iter2 = str[Symbol.iterator](); //得到字符串的遍历器对象。
iter2.next(); // {value: 'h', done: false}

可迭代对象

要成为可迭代对象, 一个对象必须具有一个带 Symbol.iterator 键(key)的属性。
生成器函数使用 function* 语法编写,返回 generator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};

for (let x of obj) {
console.log(x);
}
// "hello"
// "world"
// ------ 转换成数组 ------
[...obj] // ['hello', 'world']

异步

异步-大纲

对异步的理解

JS 是单线程的,但是却能执行异步任务,这主要是因为 JS 中存在事件循环和任务队列。简单来说,就是主线程循环往复地从”任务队列”中读取事件。

  • 主线程:JS 只有一个线程,称之为主线程。而事件循环是主线程中执行栈里的代码执行完毕之后,才开始执行的。所以,主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行。只有当主线程中执行栈为空的时候(即同步代码执行完后),才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。
  • 事件循环(Event Loop):JS 会创建一个类似于while (true)的循环,每执行一次循环体的过程称之为Tick。每次 Tick 的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中,也就是每次 Tick 会查看任务队列中是否有需要执行的任务。
  • 任务队列(Task Queue):异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如onclick, setTimeout, ajax 处理的方式都不同,这些异步操作是由浏览器内核的 webcore 来执行的,webcore 包含上图中的 3 种 webAPI,分别是 DOM Binding、timer、network 模块。
    • onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。
    • setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
    • ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。

事件循环此处不作详细解释,可参考下面的大纲:
事件循环

异步解决方案

异步在实现上,依赖一些特殊的语法规则。从整体上来说,异步方案经历了如下的四个进化阶段:

回调函数 —> Promise —> Generator —> async/await。

回调地狱

当回调只有一层的时候,看起来感觉没什么问题。但是一旦回调函数嵌套的层级变多了之后,代码的可读性和可维护性将面临严峻的挑战。比如当我们想发起连环网络请求时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const https = require('https');


https.get('目标接口1', (res) => {
console.log(data)
https.get('目标接口2', (res) => {
https.get('目标接口3'), (res) => {
console.log(data)
https.get('目标接口4', (res) => {
https.get('目标接口5', (res) => {
console.log(data)
.....
// 无尽的回调
}
}
}
}
})

面对无穷无尽的“回调地狱”,新的解决方案亟需开发。Promise 的出现解决了什么痛点?
提出链式调用,解决回调地狱的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 链式调用
httpPromise(url1)
.then(res => {
console.log(res);
return httpPromise(url2);
})
.then(res => {
console.log(res);
return httpPromise(url3);
})
.then(res => {
console.log(res);
return httpPromise(url4);
})
.then(res => console.log(res));。

Promise

代理对象

new Promise(executor)接受传入的 executor(执行器)作为入参,允许你把异步任务的成功和失败分别绑定到对应的处理方法(resolve、reject)上去。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
const https = require('https');

function httpPromise(url){
return new Promise(function(resolve,reject){
https.get(url, (res) => {
resolve(data);
}).on("error", (err) => {
reject(error);
});
})
}

httpPromise().then(function(data){}, function(error){});

状态

在 Promise 实例创建后,执行器里的逻辑会立刻执行,在执行的过程中,根据异步返回的结果,决定如何使用 resolve 或 reject 来改变 Promise实例的状态。 Promise 实例有三种状态:

  • pending 状态,表示进行中。这是 Promise 实例创建后的一个初始态。可以转变成 fulfilled 或 rejected。
  • fulfilled 状态,表示成功完成。这是我们在执行器中调用 resolve 后达成的状态。必须有一个 value,并且不可再转变为其他状态。
  • rejected 状态,表示操作失败、被拒绝。这是我们在执行器中调用 reject 后达成的状态。必须有一个 reason,并且不可再转变为其他状态。

在上面这个例子里,当我们用 resolve 切换到了成功态后,Promise 的逻辑就会走到 then 中的第一个参数传入的方法里去;用 reject 切换到失败态后,Promise 的逻辑就会走到 then 中的第二个参数传入的方法里。

状态转换机制:状态是可以改变的,但它只允许被改变一次。

方法

Promise.then:接受两个参数,onFulfilled 或 onRejected。可以被链式调用。
Promise.catch:处理 onRejected 的情况。
Promise.all:全部 promise 对象都成功触发成功,任何一个失败则失败。
Promise.race:任意一个子 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
// 假设已实现满足Promise/A+规范的myPromise
// 1.手写Promise.all
// 接受一个promise数组,当所有promise状态resolve后,执行resolve
myPromise.prototype.all = function (promises) {
return new Promise((resolve, reject) => {
const len = promises.length;
const res = new Array(len);
let done = 0;

for(let i = 0; i < len; i++){
promises[i].then((value) => {
done++;
res[i] = value;
if (done === len) resolve(res);
}, (reason) => {
reject(reason);
return;
})
}
})
};
// 2.手写Promise.race
// 接受一个promise数组,当有一个promise状态resolve后,执行resolve
myPromise.prototype.race = function(promises) {
return new Promise((resolve, reject) => {
const len = promises.length;
for(let i = 0; i < len; i++) {
promises[i].then((value) => {
resolve(value);
}, (reason) => {
reject(reason);
})
}
})
}
// 3.手写Promise.catch
myPromise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
};

【高阶】Promise/A+规范

如果你要手写一个Promise,它必须要满足Promise/A+规范,概况如下:

  1. 有三个状态:pending、fulfilled、rejected
  2. 支持链式调用:提供then方法
  3. 实现Promise决议程序:约束的就是 resolve 应该如何表现

第三点有点抽象,先放置一旁。让我们先看前两点。如何实现一个Promise,能够支持前两点呢。

要想实现链式调用,要考虑以下几个重点:

  • then方法中应该直接把 this 给 return 出去(链式调用常规操作);
  • 链式调用允许我们多次调用 then,多个 then 中传入的 onResolved 和 onRejected 任务,我们需要把它们维护在一个队列里;
  • 要想办法确保 then 方法执行的时机,务必在 onResolved 队列 和 onRejected 队列批量执行前。不然队列任务批量执行的时候,任务本身都还没收集完,就乌龙了。一个比较容易想到的办法就是把批量执行这个动作包装成异步任务,这样就能确保它一定可以在同步代码之后执行了。
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
function myPromise(executor) {
this.value = null;
this.reason = null;
this.status = "pending";

// 缓存两个队列,维护 resolved 和 rejected 各自对应的处理函数
this.onResolvedQueue = [];
this.onRejectedQueue = [];

const self = this;
function resolve(value) {
if (self.status !== "pending") return;
self.value = value;
self.status = "fulfilled";
setTimeout(() => {
self.onResolvedQueue.forEach((resolved) => resolved());
});
}

function reject(reason) {
if (self.status !== "pending") return;
self.reason = reason;
self.status = "rejected";
setTimeout(() => {
self.onRejectedQueue.forEach((rejected) => rejected());
});
}

executor(resolve, reject);
}

myPromise.prototype.then = function (onResolved, onRejected) {
// 注意,onResolved 和 onRejected 必须是函数;如果不是,我们此处用一个透传来兜底
if (typeof onResolved !== "function") onResolved = (x) => x;
if (typeof onRejected !== "function")
onRejected = (x) => {
throw x;
};

const self = this;
// 如果是 fulfilled 状态,执行对应的处理方法
if (self.status === "fulfilled") onResolved(self.value);
// 若是 rejected 状态,则执行 rejected 对应方法
else if (self.status === "rejected") onRejected(self.reason);
// 若是 pending 状态,则只对任务做入队处理
else if (self.status === "pending") {
self.onResolvedQueue.push(() => {onResolved(self.value)});
self.onRejectedQueue.push(() => {onRejected(self.reason)});
}
return this;
};

测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
new myPromise((resolve, reject) => {
resolve("成功!");
})
.then((value) => {
console.log(value);
console.log("我是第 1 个任务");
return '123';
})
.then((value) => {
console.log(value); // 这里应该输出 123
console.log("我是第 2 个任务");
});
/*
成功!
我是第 1 个任务
成功!
我是第 2 个任务
*/

我们实现的myPromise,最明显的一个缺陷就是下一个then拿不到上一个then的结果。这是因为我们对then函数的处理太过粗糙,这也迫使我们重新审视上文中的第三点,如何实现Promise决议程序。

决议程序处理是以一个promise和一个value为输入的抽象操作,我们把它表示为[[Resolve]](promise, x)

可以理解成:promise2 = promise1.then(onFulfilled, onRejected);

意思是说如果onFulfilled 或 onRejected 返回了值x, 则执行 Promise 解析流程 [[Resolve]](promise2, x)

只要都实现了promise/A+标准,那么不同的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
function myPromise(executor) {
this.value = null;
this.reason = null;
this.status = "pending";

this.onResolvedQueue = [];
this.onRejectedQueue = [];

const self = this;
function resolve(value) {
if (self.status !== "pending") return;
self.value = value;
self.status = "fulfilled";
// 把异步处理放到 then 方法中的 resolveByStatus/ rejectByStatus 里面来做。
self.onResolvedQueue.forEach((resolved) => resolved());
}

function reject(reason) {
if (self.status !== "pending") return;
self.reason = reason;
self.status = "rejected";
// 把异步处理放到 then 方法中的 resolveByStatus/ rejectByStatus 里面来做。
self.onRejectedQueue.forEach((rejected) => rejected());
}

executor(resolve, reject);
}

function resolutionProcedure(promise2, x, resolve, reject) {
// 这里 hasCalled 这个标识,是为了确保 resolve、reject 不要被重复执行
let hasCalled;
if (x === promise2) {
// 决议程序规范:如果 resolve 结果和 promise2相同则reject,这是为了避免死循环
return reject(new TypeError("为避免死循环,此处抛错"));
} else if (x !== null && (typeof x === "object" || typeof x === "function")) {
// 决议程序规范:如果x是一个对象或者函数,则需要额外处理下
try {
// 首先是看它有没有 then 方法(是不是 thenable 对象)
let then = x.then;
// 如果是 thenable 对象,则将promise的then方法指向x.then。
if (typeof then === "function") {
// 如果 then 是是一个函数,那么用x为this来调用它,第一个参数为 resolvePromise,第二个参数为rejectPromise
then.call(
x,
(y) => {
// 如果已经被 resolve/reject 过了,那么直接 return
if (hasCalled) return;
hasCalled = true;
// 进入决议程序(递归调用自身)
resolutionProcedure(promise2, y, resolve, reject);
},
(err) => {
// 这里 hascalled 用法和上面意思一样
if (hasCalled) return;
hasCalled = true;
reject(err);
}
);
} else {
// 如果then不是function,用x为参数执行promise
resolve(x);
}
} catch (e) {
if (hasCalled) return;
hasCalled = true;
reject(e);
}
} else {
// 如果x不是一个object或者function,用x为参数执行promise
resolve(x);
}
}

myPromise.prototype.then = function (onResolved, onRejected) {
if (typeof onResolved !== "function") onResolved = (x) => x;
if (typeof onRejected !== "function")
onRejected = (x) => {
throw x;
};

var self = this;
// 这个变量用来存返回值 x
let x;

// resolve态的处理函数
function resolveByStatus(resolve, reject) {
setTimeout(function () {
try {
// 返回值赋值给 x
x = onResolved(self.value);
// 进入决议程序
resolutionProcedure(promise2, x, resolve, reject);
} catch (e) {
// 如果onResolved或者onRejected抛出异常error,则promise2必须被rejected,用error做reason
reject(e);
}
});
}

// reject态的处理函数
function rejectByStatus(resolve, reject) {
// 包装成异步任务,确保决议程序在 then 后执行
setTimeout(function () {
try {
// 返回值赋值给 x
x = onRejected(self.reason);
// 进入决议程序
resolutionProcedure(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}

// 注意,这里我们不能再简单粗暴 return this 了,需要 return 一个符合规范的 Promise 对象
var promise2 = new myPromise(function (resolve, reject) {
if (self.status === "resolved") resolveByStatus(resolve, reject);
else if (self.status === "rejected") rejectByStatus(resolve, reject);
else if (self.status === "pending") {
self.onResolvedQueue.push(function () { resolveByStatus(resolve, reject); });
self.onRejectedQueue.push(function () { rejectByStatus(resolve, reject); });
}
});

return promise2;
};

总而言之,手写Promise能达到第一个例子的程度就已经足够通过校招面试了。第二个例子供学习参考使用。

Generator

Generator 函数可以在执行中被中断、然后等待一段时间再被我们唤醒。利用 yield 关键字,实现对异步任务的等待。

Generator 有两个区分于普通函数的部分:

  • 一是在 function 后面,函数名之前有个 * ;
  • 函数内部有 yield 表达式。

执行机制:调用遍历器对象 Iterator 的 next 方法

方法:

  1. next 方法不传入参数的时候,yield 表达式的返回值是 undefined 。当 next 传入参数的时候,该参数会作为上一步yield的返回值
  2. return 方法返回给定值,并结束遍历 Generator 函数。

举个例子:

1
2
3
4
5
6
7
8
function* sendParameter(){
console.log("start");
var x = yield '2';
console.log("one:" + x);
var y = yield '3';
console.log("two:" + y);
console.log("total:" + (x + y));
}

next传参:

1
2
3
4
5
6
7
8
9
10
11
var sendp1 = sendParameter();
sendp1.next(10);
// start
// {value: "2", done: false}
sendp1.next(20);
// one:20
// {value: "3", done: false}
sendp1.next(30);
// two:30
// total:50
// {value: undefined, done: true}

return结束函数:

1
2
3
4
5
6
var sendp2 = sendParameter();
sendp2.next();
// start
// {value: "2", done: false}
sendp2.return(100);
// {value: 100, done: true}

async/await

await 关键字仅在 async function 中有效。await 返回 Promise 对象的处理结果。如果等待的不是 Promise 对象,则返回该值本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function testAwait (x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}

async function helloAsync() {
var x = await testAwait ("hello world");
console.log(x);
}
helloAsync ();
// hello world

面试题

for infor of 的区别

for in

  • 循环的是 key
  • 推荐循环对象属性

for of(ES6 新引入的特性,修复 for in 的不足)

  • 循环的是 value
  • 推荐遍历数组
  • 不能循环普通的对象,需要通过和Object.keys()搭配使用

Promise串行

保证执行时串行。迭代这个任务数组,第一个任务执行完后才能执行下一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
const sequence = function (arr) {
let result = [];

return new Promise((resolve, reject) => {
(async function next(){
for(let item of arr){
let temp = await item();
result.push(temp)
}
resolve(result);
})()
});
};

Promise并发调度器

要求:
实现一个js限流调度器
实现JS限流调度器,方法add接收一个返回Promise的函数,同时执行的任务数量不能超过两个。
实现:

  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
class Scheduler {
constructor(maxNum) {
this.taskList = []; // 待执行的异步任务
this.count = 0; // 正在执行的任务数
this.concurrency = concurrency;
}
async addTask(task) {
if (this.count >= this.concurrency) {
// 阻塞任务执行,等等待队列有空位
await new Promise((resolve) => {
this.taskList.push(resolve);
});
}
this.count++;
const result = await task();
this.count--;
if (this.taskList.length > 0) {
// 相当于resolve(), 通知等待队列的任务去执行
this.taskList.shift()?.();
}
return result;
}
}

参考

  1. 菜鸟教程:ES6 教程 简单全面,适合复习
  2. 《ECMAScript 6 入门教程》by 阮一峰
  3. 异步编程模型与异步解决方案