Appearance
数据类型
js 的数据类型分为值类型和引用类型。
- 值类型:放在栈内存,被引用或拷贝时,会重新创建一个完全相等的变量;
- 引用类型:放在堆内存中,存储的是地址,被引用或拷贝时,都会指向同一个地址。
TIP
引用类型为什么存储一个地址?
因为如果像值类型那样,引用类型可能是会很庞大的数据,这样占据的内存空间就很多,进行拷贝和引用的时候,就会造成更大的性能开销。
TIP
栈和堆存放数据的方式。
栈是从上到下存放数据。堆是从下到上存放数据。
BigInt 类型
BigInt 是一种特殊的数字类型,它提供了对任意长度整数的支持。
创建 bigint 的方式有两种:
- 在一个整数字面量后面加 n
- 调用 BigInt 函数,该函数从字符串、数字等中生成 bigint。
bigint 不能和 常规数字进行数学运算(+-*/等),只能进行比较运算,且在判断相等的时候,不能使用===进行比较。
bigint 不支持一元加法 +value,但是支持 ++value。
js
const b = BigInt('123243') // 123243n
console.log(1n + 2n) // 3n
console.log(1n + 2) // Cannot mix BigInt and other types
/*
如果有需要,我们应该显式地转换它们:使用 BigInt() 或者 Number(),
但是如果 bigint 太大而数字类型无法容纳,则会截断多余的位
*/
console.log(1n + BigInt(2)) // 3n
console.log(Number(1n) + 2) // 3
console.log(2n > 1n) // true
console.log(2n > 1) // true
console.log(2n == 2) // true
console.log(2n === 2) // false
检测数据类型的方法
typeof
只能检测值类型,和 function ,其他引用类型都是返回 object
js
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'
instanceof
instanceof 运算符用于测试构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
js
let Car = function () {}
let benz = new Car()
benz instanceof Car // true
let car = new String('Mercedes Benz')
car instanceof String // true
let str = 'Covid-19'
str instanceof String // false
自己实现一个 instanceof 。
js
function myInstanceof(obj, contorcur) {
//如果是基本类型,则直接返回 false
if (typeof obj !== 'object' && typeof obj !== 'function') return false
// 不能是 null
if (!obj) return false
const left = obj.__proto__
const right = contorcur.prototype
// 如果找到最顶层了,还没找到则返回 false
if (left === null) return false
// 如果找到了,则返回 true
if (left === right) return true
// 递归查找上一层原型
return myInstanceof(left, contorcur)
}
const Fn = function () {}
const f1 = new Fn()
console.log(myInstanceof(Fn, Object)) // true
console.log(myInstanceof(null, Object)) // false
console.log(myInstanceof(undefined, Object)) // false
console.log(myInstanceof(f1, Fn)) // true
TIP
typeof 和 instanceof 的差异:
instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型;
而 typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了 function 类型以外,其他的也无法判断。
Object.prototype.toString.call() 推荐
toString() 是 Object 的原型方法,调用该方法,可以统一返回格式为 “[object Xxx]” 的字符串,其中 Xxx 就是对象的类型。对于 Object 对象,直接调用 toString() 就能返回 [object Object];而对于其他对象,则需要通过 call 来调用,才能返回正确的类型信息。
js
Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call({}) // 同上结果,加上call也ok
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function () {}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"
'==' 的隐式类型转换规则
如果类型相同,无须进行类型转换;
如果其中一个操作值是 null 或者 undefined,那么另一个操作符必须为 null 或者 undefined,才会返回 true,否则都返回 false;
如果其中一个是 Symbol 类型,那么返回 false;
两个操作值如果为 string 和 number 类型,那么就会将字符串转换为 number;
如果一个操作值是 boolean,那么转换成 number;
如果一个操作值为 object 且另一方为 string、number 或者 symbol,就会把 object 转为原始类型再进行判断(调用 object 的 valueOf/toString 方法进行转换)。
js
null == undefined // true 规则2
null == 0 // false 规则2
'' == null // false 规则2
'' == 0 // true 规则4 字符串转隐式转换成Number之后再对比
'123' == 123 // true 规则4 字符串转隐式转换成Number之后再对比
0 == false // true e规则 布尔型隐式转换成Number之后再对比
1 == true // true e规则 布尔型隐式转换成Number之后再对比
var a = {
value: 0,
valueOf: function () {
this.value++
return this.value
},
}
// 注意这里a又可以等于1、2、3
console.log(a == 1 && a == 2 && a == 3) //true f规则 Object隐式转换
// 注:但是执行过3遍之后,再重新执行a==3或之前的数字就是false,因为value已经加上去了,这里需要注意一下
Reflect 有什么用
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。
它的作用有:
- 改进 Object 对象操作时,一些错误处理,像普通对象进行操作的时候,如果报错会打断后续的代码执行,Reflect 则不会。并且 Reflect 的方法执行失败后是返回 false,而普通对象是直接报错。
- 在使用 Reflect 的 get 和 set 方法时,它还可以传入一个 receiver 参数,即 this 的指向。它和 proxy 正好天然的配合,因为 Reflect 的传参和返回值,都跟 proxy 一样,可以保证正确的 this 指向。如下例子:
js
let mao = {
_name: '猫',
get name() {
return this._name
},
}
let miaoXy = new Proxy(mao, {
get(target, prop, receiver) {
return target[prop]
},
})
let banana = {
__proto__: miaoXy,
_name: '香蕉',
}
// 按照正常结果,这里其实应该返回香蕉,但是 banana 的 this 指向了 mao ,因此结果是猫
console.log(banana.name)
// 为了解决上面的问题,这个时候就需要 reflect 了
let mao = {
_name: '猫',
get name() {
return this._name
},
}
let miaoXy = new Proxy(mao, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver)
},
})
let banana = {
__proto__: miaoXy,
_name: '香蕉',
}
// 结果是香蕉
console.log(banana.name)
执行上下文、作用域、闭包
执行上下文基本介绍
它是执行 js 代码时的运行环境。执行上下文可以分为三种类型:全局执行上下文、函数执行上下文和 eval 执行上下文。
全局执行上下文:当 JavaScript 代码首次执行时,会创建全局执行上下文。全局执行上下文是最外层的执行上下文,包含着整个代码文件中的全局变量和函数声明。全局执行上下文一直存在于代码执行过程中,直到程序退出。
函数执行上下文:每当一个函数被调用时,都会创建一个函数执行上下文。函数执行上下文与该函数的相关变量和函数声明有关。当函数执行完毕,函数执行上下文会被销毁。如果函数内部存在嵌套的函数调用,会创建多个嵌套的函数执行上下文。上下文栈执行的是先进后出机制。
eval 执行上下文:当代码包含 eval 函数时,会创建一个 eval 执行上下文。eval 执行上下文与 eval 函数中的代码相关联。
无论是全局执行上下文、函数执行上下文还是 eval 执行上下文,都具有相似的执行顺序:
- 创建变量对象:变量对象包含了在上下文中定义的所有变量和函数声明。
- 创建作用域链:作用域链是一个指向父级执行上下文的指针列表,用于变量查找。
- 确定 this 值:this 值指向当前执行代码的对象。
- 执行代码:按照代码的顺序执行。
当函数执行上下文创建时,还会进行额外的步骤:
- 创建活动对象:活动对象是函数的局部变量、参数和内部函数的存储空间。
- 设置函数的 arguments 对象:arguments 对象包含了函数被调用时传递的参数。
- 在作用域链的顶部添加活动对象。
作用域基本介绍
当我们在 JavaScript 中声明变量时,它们不是全局可访问的,而是具有特定的可访问范围,这个范围被称为作用域。作用域定义了变量、函数等能够被访问到的范围。
在 JavaScript 中作用域也分为好几种,ES5 之前只有全局作用域和函数作用域
两种。ES6 出现之后,又新增了块级作用域
,下面我们就来看下这三种作用域的概念,为闭包的学习打好基础。
- 全局作用域。在代码的任何地方都能访问到的变量、函数等就在全局作用域下。
- 函数作用域。在函数中定义的变量叫作函数变量,这个时候只能在函数内部才能访问到它,所以它的作用域也就是函数的内部,称为函数作用域。
js
function getName() {
var name = 'inner'
console.log(name) //inner
}
getName()
console.log(name) // 空
- 块级作用域。 if 语句及 for 语句后面 {...} 这里面所包括的,就是块级作用域。有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被使用的。
js
console.log(a) //a is not defined
if (true) {
let a = '123'
console.log(a) // 123
}
console.log(a) //a is not defined
作用域链
作用域链定义了变量在嵌套的作用域中被查找的顺序。当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链。
重点
所有变量在作用域中的查找是在定义的作用域往上查找,而不是执行的地方!!!
在上一条的基础上,还有一条,函数内部访问外部变量时,可以访问函数定义的作用域之前的变量,和函数执行之前的变量。这一点我有点没弄明白,没有找到对应的官方资料
具体看如下几段代码
js
// 例子1
function fn() {
console.log(a) // 100
}
const a = 100
fn()
// 例子2
function fn() {
console.log(a) // ReferenceError: Cannot access 'a' before initialization
}
fn()
const a = 100
// 例子3
function fn() {
return function () {
console.log(a) // 200
}
}
let res = fn()
const a = 200
res()
// 例子4
function fn() {
return function () {
console.log(a) // undefined
}
}
let res = fn()
res()
var a = 200
js
// 例子1
function fn() {
const a = 100
return function () {
console.log(a) // 100
}
}
let res = fn()
const a = 200
res()
// 例子2
function fn() {
return function () {
console.log(a) // 报错 ReferenceError: Cannot access 'a' before initialization
}
}
let res = fn()
res()
const a = 200
// 例子3
function print(fn) {
const a = 100
fn()
}
const a = 200
function quick() {
console.log(a) // 200
}
print(quick)
let、const、var 的区别
- var 定义的变量可以修改,可以重复声明,且会被提升,可以不初始化,默认为 undefined。是全局变量。
- let 定义的变量可以被修改,不能重复声明,不会被提升,可以不初始化,默认为 undefined。是块级作用域。
- const 定义的是一个常量,不能被修改,但是当为对象的时候,可以修改其属性,不能被提升,必须初始化,是块级作用域。
由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化。
暂时性死区
对于使用 let 和 const 声明的变量,在声明之前不能以任何方式引用变量,否则 js 在执行的时候会抛出 ReferenceError 错误,这就叫暂时性死区。
全局声明
与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性( var 声明的变量则会)。
不过, let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。
闭包基本介绍
概念:在一个嵌套函数内,内部函数引用了外部函数作用域中的变量,这时内部函数就是一个闭包。
用途:减少全局变量,防止全局变量过多,导致难以维护;防止变量被修改,只能在当前函数内才能被修改。
适用场景:封装组件、节流 防抖函数、for 循环和 DOM 事件结合、for 循环和定时器结合等。
注意:由于闭包会导致函数内的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,在 ie 中可能导致内存泄漏,解决办法是在退出函数之前,将不用的变量删除,即设置成 null,系统会自动回收。
js
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i)
}, 0)
}
/*
请说出为什么输出5个6的原因
1、setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,
在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
2、因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,
开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。
*/
//如何让它输出 1,2,3,4,5
// 1、利用 IIFE, 当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行
for (var i = 1; i <= 5; i++) {
;(function (j) {
setTimeout(function timer() {
console.log(j)
}, 0)
})(i)
}
// 2、使用 ES6 中的 let
// 3、定时器传入第三个参数,一旦定时器到期,它们会作为参数传递给对应的函数。
for (var i = 1; i <= 5; i++) {
setTimeout(
function (j) {
console.log(j)
},
0,
i
)
}
this 指向问题
重点
this 指向谁,取决于是谁执行的,而不是定义的地方。
看下面的代码:
js
const obj = {
say() {
console.log(this)
},
eat() {
setTimeout(() => {
console.log(this)
})
},
dance() {
setTimeout(function () {
console.log(this)
})
},
}
obj.say() // 函数中的 this 指向 obj
let fn = obj.say
fn() // 函数中的 this 指向 window, 因为 obj.say 赋值 给 fn 后,fn 直接在全局作用域下执行的
obj.eat() // 函数中的 this 指向 obj, 因为定时器中的方法虽然是定时器执行的,但是由于箭头函数的 this 是取上级作用域的值
obj.dance() // 函数中的 this 指向 window, 因为定时器中的方法是定时器执行的,而不是 obj.dance
原型、原型链、继承
原型、原型链
原型:每一个对象从被创建开始就和另一个对象关联,从另一个对象上继承其属性,这个另一个对象就叫原型(prototype)。
原型链:简单的总结就是,当访问对象的一个属性时,如果该对象内部不存在这个属性,就会去对象的原型上查找,如果还找不到就去对象的原型的原型上查找,然后一直重复这个步骤,直到最顶层的原型对象,这条由对象及其原型组成的链路就是原型链。需要注意的是,即使在中途找到了,也会一直往上层找。
总结一下:
- 原型存在的意义就是组成原型链:引用类型皆对象,每个对象都有原型,原型也是对象,也有它自己的原型,一层一层,组成原型链。
- 原型链存在的意义就是继承:访问对象属性时,在对象本身找不到,就在原型链上一层一层找。说白了就是一个对象可以访问其他对象的属性。
- 继承存在的意义就是属性共享:好处有二:一是代码重用,字面意思;二是可扩展,不同对象可能继承相同的属性,也可以定义只属于自己的属性。
每个对象都有原型,那么我们怎么获取到一个对象的原型呢?那就是对象的 __proto__
属性,指向对象的原型。
__proto__
是隐式原型,prototype 是显式原型。
引用类型皆对象,所以引用类型都有__proto__
属性,对象有__proto__
属性,函数有__proto__
属性,数组也有__proto__
属性,只要是引用类型,就有__proto__
属性,都指向它们各自的原型对象。只有函数有 prototype 属性
。
js
function Person() {}
var person = new Person()
图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
知识点:
- 引用类型都是对象,每个对象都有原型对象。
- 对象都是由构造函数创建,对象的
__proto__
属性等于创建它的构造函数的 prototype 属性。 - 所有通过字面量表示法创建的普通对象的构造函数为 Object
- 所有原型对象都是普通对象,构造函数为 Object
- 所有函数的构造函数是 Function
- Object.prototype 没有原型对象
constructor
回忆一下之前的描述,构造函数都有一个 prototype 属性,指向使用这个构造函数创建的对象实例的原型对象。
这个原型对象中默认有一个 constructor 属性,指回该构造函数。
js
function Person() {}
console.log(Person === Person.prototype.constructor) // true
js
function Person() {}
var person = new Person()
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
函数对象的原型链
之前提到过引用类型皆对象,函数也是对象,那么函数对象的原型链是怎么样的呢?
对象都是被构造函数创建的,函数对象的构造函数就是 Function,注意这里 F 是大写。
js
let fn = function () {}
// 函数(包括原生构造函数)的原型对象为Function.prototype
fn.__proto__ === Function.prototype // true
Array.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
// 特例
Function.__proto__ === Function.prototype // true
Function.prototype 也是一个普通对象,所以 Function.prototype.__proto__ === Object.prototype
这里有一个特例,Function 的 __proto__
属性指向 Function.prototype。
TIP
函数都是由 Function 原生构造函数创建的,所以函数的 __proto__
属性指向 Function 的 prototype 属性
参考资料: 深入 JavaScript 系列(六):原型与原型链
继承
6 种继承方式 和 es6 的 extends 继承,总共 7 种,其中寄生组合式继承,是除开 es6 继承的最优解。
- 原型链继承
- 构造函数继承(借助 call)
- 组合继承(前两种组合)
- 原型式继承
- 寄生式继承
- 寄生组合式继承
- es6 的 extends 继承
接下来看具体代码:
原型链继承
缺点:内存空间是共享的,当一个发生变化的时候,另外一个也会变化。
js
function Parent1() {
this.name = 'parent1'
this.play = [1, 2, 3]
}
function Child1() {
this.type = 'child2'
}
Child1.prototype = new Parent1()
console.log(new Child1())
上面的代码看似没有问题,虽然父类的方法和属性都能够访问,但其实有一个潜在的问题,我再举个例子来说明这个问题。
js
var s1 = new Child1()
var s2 = new Child2()
s1.play.push(4)
console.log(s1.play) // [1, 2, 3, 4]
console.log(s2.play) // [1, 2, 3, 4]
明明我只改变了 s1 的 play 属性,为什么 s2 也跟着变了呢?原因很简单,因为两个实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化,这就是使用原型链继承方式的一个缺点。
构造函数继承(借助 call)
缺点:只能继承父类的实例属性和方法,不能继承原型属性或者方法。
js
function Parent1() {
this.name = 'parent1'
}
Parent1.prototype.getName = function () {
return this.name
}
function Child1() {
Parent1.call(this)
this.type = 'child1'
}
let child = new Child1()
console.log('child =>', child) // 没问题
console.log('child.getName() =>', child.getName()) // 会报错
执行上面的这段代码,可以得到这样的结果。
可以看到最后打印的 child 在控制台显示,除了 Child1 的属性 type 之外,也继承了 Parent1 的属性 name。这样写的时候子类虽然能够拿到父类的属性值,解决了第一种继承方式的弊端,但问题是,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法。
可以看到构造函数实现继承的优缺点,它使父类的引用属性不会被共享,优化了第一种继承方式的弊端;但是随之而来的缺点也比较明显——只能继承父类的实例属性和方法,不能继承原型属性或者方法。
上面的两种继承方式各有优缺点,那么结合二者的优点,于是就产生了下面这种组合的继承方式。
组合继承(前两种组合)
js
function Parent3(name) {
this.name = name
this.play = [1, 2, 3]
}
Parent3.prototype.getName = function () {
return this.name
}
function Child3(name) {
// 第二次调用 Parent3()
Parent3.call(this, name)
this.type = 'child3'
}
// 第一次调用 Parent3()
Child3.prototype = new Parent3()
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3
var s3 = new Child3('黄三')
var s4 = new Child3('黄四')
s3.play.push(4)
console.log(s3.play) // [1, 2, 3, 4]
console.log(s4.play) // [1, 2, 3]
console.log(s3.getName()) // '黄三'
console.log(s4.getName()) // '黄四'
可以看到输出结果,之前方法一和方法二的问题都得以解决。
但是这里又增加了一个新问题:通过注释我们可以看到 Parent3 执行了两次,第一次是改变 Child3 的 prototype 的时候,第二次是通过 call 方法调用 Parent3 的时候,那么 Parent3 多构造一次就多进行了一次性能开销,这是我们不愿看到的。
上面介绍的更多是围绕着构造函数的方式,那么对于 JavaScript 的普通对象,怎么实现继承呢?
原型式继承
缺点:多个实例的引用类型属性指向相同的内存,改动一个,另一个也会变化。
利用 Object.create 方法,实现普通对象的继承,继承属性和方法。
js
let parent4 = {
name: 'parent4 name',
friends: ['p1', 'p2', 'p3'],
getName: function () {
return this.name
},
}
let person4 = Object.create(parent4)
person4.name = 'person4 tom'
person4.friends.push('jerry')
let person5 = Object.create(parent4)
person5.friends.push('lucy')
console.log(person4.name) // 'person4 tom'
console.log(person5.name) // 'parent4 name'
console.log(person4.friends) // ['p1', 'p2', 'p3', 'jerry', 'lucy']
console.log(person5.friends) // ['p1', 'p2', 'p3', 'jerry', 'lucy']
寄生式继承
缺点跟原型式继承一样。
使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承。
虽然其优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。
js
let parent5 = {
name: 'parent5',
friends: ['p1', 'p2', 'p3'],
getName: function () {
return this.name
},
}
function clone(original) {
let clone = Object.create(original)
clone.getFriends = function () {
return this.friends
}
return clone
}
let person5 = clone(parent5)
console.log(person5.getName()) // 'parent5'
console.log(person5.getFriends()) // ['p1', 'p2', 'p3']
寄生组合式继承
是相对最优的继承方式,利用 Object.create 方法,再结合组合继承的方法。解决对应的缺点。
js
function clone(parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype)
child.prototype.constructor = child
}
function Parent6(name) {
this.name = name
this.play = [1, 2, 3]
}
Parent6.prototype.getName = function () {
return this.name
}
function Child6(name) {
Parent6.call(this, name)
this.friends = 'child5'
}
clone(Parent6, Child6)
Child6.prototype.getFriends = function () {
return this.friends
}
let person6 = new Child6('person6 name')
person6.play.push('person6')
let person7 = new Child6('person7 name')
person7.play.push('person7')
console.log(person6.name) // 'person6 name'
console.log(person6.play) // [1, 2, 3, 'person6']
console.log(person6.getFriends()) // 'child5'
console.log(person7.name) // 'person7 name'
console.log(person7.play) // [1, 2, 3, 'person7']
这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销。
es6 的 extends 继承
js
class Person {
constructor(name) {
this.name = name
}
// 原型方法 即 Person.prototype.getName = function() { }
getName() {
console.log('Person:', this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(name)
this.age = age
}
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 'Person: Asuna'
下面是 extends 经过 label 转成 es5 的代码:
js
function _possibleConstructorReturn(self, call) {
// ...
return call && (typeof call === 'object' || typeof call === 'function')
? call
: self
}
function _inherits(subClass, superClass) {
// 这里可以看到
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true,
},
})
if (superClass)
Object.setPrototypeOf
? Object.setPrototypeOf(subClass, superClass)
: (subClass.__proto__ = superClass)
}
var Parent = function Parent() {
// 验证是否是 Parent 构造出来的 this
_classCallCheck(this, Parent)
}
var Child = (function (_Parent) {
_inherits(Child, _Parent)
function Child() {
_classCallCheck(this, Child)
return _possibleConstructorReturn(
this,
(Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments)
)
}
return Child
})(Parent)
从上面编译完成的源码中可以看到,它采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式。
实现 new、apply、call、bind
new 的原理与实现
new 关键词的主要作用就是执行一个构造函数、返回一个实例对象。那么 new 在这个生成实例的过程中到底进行了哪些步骤来实现呢?总结下来大致分为以下几个步骤。
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(this 指向新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
那么问题来了,如果不用 new 这个关键词,结合上面的代码改造一下,去掉 new,会发生什么样的变化呢?
js
function Person() {
this.name = 'Jack'
}
var p = Person()
console.log(p) // undefined
console.log(name) // Jack
console.log(p.name) // 'name' of undefined
从上面的代码中可以看到,我们没有使用 new 这个关键词,返回的结果就是 undefined。其中由于 JavaScript 代码在默认情况下 this 的指向是 window,那么 name 的输出结果就为 Jack,这是一种不存在 new 关键词的情况。
那么当构造函数中有 return 一个对象的操作,结果又会是什么样子呢?
js
function Person() {
this.name = 'Jack'
return { age: 18 }
}
var p = new Person()
console.log(p) // {age: 18}
console.log(p.name) // undefined
console.log(p.age) // 18
通过这段代码又可以看出,当构造函数最后 return 出来的是一个和 this 无关的对象时,new 命令会直接返回这个新对象,而不是通过 new 执行步骤生成的 this 对象。
但是这里要求构造函数必须是返回一个对象,如果返回的不是对象,那么还是会按照 new 的实现步骤,返回新生成的对象。
js
function Person() {
this.name = 'Jack'
return 'tom'
}
var p = new Person()
console.log(p) // {name: 'Jack'}
console.log(p.name) // Jack
总结:new 关键词执行之后总是会返回一个对象,要么是实例对象,要么是 return 语句指定的对象。
实现一个 new 方法
js
function Person(name) {
this.name = name
return 'tom'
}
const per = new Person('Jack')
console.log(per) // {name: 'Jack'}
function myNew(fn, ...args) {
if (typeof fn !== 'function') throw `${fn} is not a function`
// 将这个新对象的原型对象,指向构造函数的原型对象
let obj = Object.create(fn.prototype)
// 执行构造函数的代码,(为这个新对象添加属性)
const res = fn.apply(obj, args)
// 如果返回的不是对象,那么还是会按照 new 的实现步骤,返回新生成的对象。
return res instanceof Object ? res : obj
}
const per2 = myNew(Person, 'fdsfs')
console.log(per2) // {name: 'fdsfs'}
call、apply、bind 的原理与实现
call、apply 和 bind 是挂在 Function 对象上的三个方法,调用这三个方法的必须是一个函数。请看这三个函数的基本语法。
js
func.call(thisArg, param1, param2, ...)
func.apply(thisArg, [param1,param2,...])
func.bind(thisArg, param1, param2, ...)
call 和 apply 的区别在于传参的方式不同,它俩都是会立即执行函数。
bind 和这两个(call、apply)又不同,它不会立即执行函数。
应用场景:
- 判断数据类型
js
function getType(obj) {
let type = Object.prototype.toString.call(obj).split(' ')[1]
let res = type.replace(']', '')
return res
}
console.log(getType(12)) // 'Number'
console.log(getType(true)) // 'Boolean'
console.log(getType({ age: 12 })) // 'Object'
- 类数组借用数组方法,比如借用数组的 push 方法
js
let arrayLike = {
0: 'java',
1: 'script',
length: 2,
}
Array.prototype.push.call(arrayLike, 'jack', 'lily')
console.log(typeof arrayLike) // 'object'
console.log(arrayLike) // {0: "java", 1: "script", 2: "jack", 3: "lily", length: 4}
- 获取数组的最大 / 最小值
js
let arr = [13, 6, 10, 11, 16]
const max = Math.max.apply(Math, arr)
const min = Math.min.apply(Math, arr)
console.log(max) // 16
console.log(min) // 6
- 继承
实现 call、apply
它俩都是把函数 this 的指向改变成传入的对象。首先我们要明白 this 的指向问题。谁执行的,this 就指向谁。
接下来分析一下它的实现步骤,
- 要把函数的 this ,改成指向到传入的对象。
- 那么,直接把
函数 变成 传入对象的一个属性
即可,然后执行这个对象的属性(相当于执行函数)。 - 最后把新加的属性去除,防止污染传入的对象。
相当于如下代码:
js
const obj = {
age: 18,
address: '广东',
}
function person() {
this.age = 22
}
// 把 函数 person 的 this 指向 obj,相当于
const obj = {
age: 18,
address: '广东',
fn: function person() {
this.age = 22
console.log(this) // {age: 22, address: '广东', fn: ƒ}
},
}
obj.fn()
接下来自己实现一个 call 和 apply
js
const obj = {
age: 18,
address: '广东',
}
function person(age) {
this.age = age
return this
}
Function.prototype.myCall = function (context, ...args) {
// 如果没传入对象,就取 window
let ctx = context || window
// 设置传入对象的一个属性为当前函数
const sys = Symbol()
ctx[sys] = this
// 利用对象的属性执行函数内容,改变 this 指向
let res = ctx[sys](...args)
// 删除多余的属性
delete ctx[sys]
// 返回结果
return res
}
// 就传参方式不一样而已,其他完全一样
Function.prototype.myApply = function (context, args = []) {
// 如果没传入对象,就取 window
let ctx = context || window
// 设置传入对象的一个属性为当前函数
const sys = Symbol()
ctx[sys] = this
// 利用对象的属性执行函数内容,改变 this 指向
let res = ctx[sys](...args)
// 删除多余的属性
delete ctx[sys]
// 返回结果
return res
}
console.log(person.myCall(obj, 22)) // {age: 22, address: '广东'}
console.log(person.myApply(obj, [33])) // {age: 33, address: '广东'}
实现 bind
解析 bind 的实现步骤,
- 修改 this 指向,返回一个函数
- 使用 bind 的时候可以传参,返回的函数也可以传参,两次的传参需要合并起来。
- 由于返回的函数,即可以当作普通函数直接调用,也可以通过 new 调用。
- 所有 this 指向谁,要处理两种情况,直接调用的时候,this 就指向传入的对象。通过 new 调用的时候,直接指向返回函数本身。
- 返回函数的时候,原来函数的原型链上的属性,不能丢失,所以通过 Object.create 方法,把返回函数的原型,设置成原来函数的原型。
js
const obj = {
age: 18,
address: '广东',
}
function person(age) {
this.age = age
return this
}
Function.prototype.myBind = function (context, ...args) {
// 如果没传入对象,就取 window
let ctx = context || window
let self = this
let fn = function () {
// 获取两次传入参数
let argu = args.concat(...arguments)
// this instanceof self,为什么要执行这一句呢
// 分两种情况: 当这个绑定函数被当做普通函数调用的时候,可以直接用context;
// 而返回的 fn 函数,当做构造函数使用的时候,却是指向这个实例,所以this instanceof self为true时,要用this
return self.apply(this instanceof self ? this : ctx, argu)
}
// 防止原来函数的原型链上的属性丢失
fn.prototype = Object.create(this.prototype)
return fn
}
let resFu1 = person.bind(obj)
const res1 = new resFu1(33)
console.log(resFu1()) // {age: undefined, address: '广东'}
console.log(res1) // person {age: 33}
let resFu2 = person.myBind(obj)
const res2 = new resFu2(33)
console.log(resFu2()) // {age: undefined, address: '广东'}
console.log(res2) // fn {age: 33}
JS 异步编程、EventLoop、消息队列都是做什么的,什么是宏任务,什么是微任务
JS 异步编程
js 是单线程的,一次只能执行一个任务,多任务需要排队等候,这种模式可能会阻塞代码的执行。
为了避免这个问题,出现了异步编程。一般就是通过调用 web api 的方式实现,因为它们是可以多线程的,比如回调函数、事件发布/订阅、promise等
来组织代码,它们的本质都是通过回调函数来实现异步代码的存放和执行。
EventLoop(事件循环)
它是一种运行机制,不断的循环收集和处理事件, 按顺序执行消息队列的宏任务和微任务,以及浏览器 ui 渲染和绘制操作。
执行顺序是:首先是一个 script 标签(宏任务) ==> 同步代码 ==> 微任务 ==> DOM 渲染(浏览器 ui 渲染和绘制操作) ==> 宏任务,以此循环。
可以根据下面代码进行分析,利用 alert 可以阻断 js 执行,也可以阻断 DOM 渲染,可以很好的看出效果。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button onclick="handleStart()">执行</button>
<div id="div"></div>
</body>
<script>
function handleStart() {
const bd = document.getElementById('div')
bd.innerHTML = '<h1>一个标题</h1>'
Promise.resolve().then(() => {
console.log(bd.innerHTML)
alert('promise then') // 弹出的时候,H1 标签还未渲染
})
setTimeout(() => {
console.log(bd.innerHTML)
alert('setTimeout then') // 弹出的时候,H1 标签已经渲染
}, 0)
}
</script>
</html>
消息队列
用来存放宏任务和微任务的队列。一开始整个脚本作为一个宏任务开始执行,同步代码直接执行。当当前宏任务执行成功后,将中间的方法回调,定时器等宏任务添加到宏任务队列,promise 等微任务添加到微任务队列。
执行顺序:
当前主线程的宏任务执行完,检查并执行微任务队列,接着执行浏览器 ui 线程的渲染工作,再检查 web worker 任务。 然后再取出一个宏任务,以此循环...
宏任务(基本都是浏览器相关的 api)
可以理解为每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取的事件回调)。浏览器为了让宏任务与 DOM 操作能够有序的执行,会在一个宏任务执行结束后,在下一个宏任务执行前,对页面重新渲染。
宏任务包括:script(整体代码)、setTimeout、setInterval、I/O、MessageChannel、UI 交互事件(浏览器独有)、requestAnimationFrame(浏览器独有)、setImmediate(node 和 IE 独有)等。
微任务(基本都是 ECMAScript 的 api)
可以理解为在当前任务执行结束后,需要立即执行的任务。也就是说在一次宏任务结束后,在页面渲染之前,执行清空微任务。 所以它的响应速度会比宏任务更快,因为无需等待 UI 渲染。
微任务包括:promise.then(catch)等、MutationObserve、queueMicrotask、process.nextTick(Node 环境)等。注意:new promise 里的内容属于同步代码
。
js
queueMicrotask(() => {
console.log('微任务')
})
简单难度异步执行练习
知识点:
- 使用 async await 的时候,await 背后的代码其实是一个微任务,执行 await 后面的微任务代码后,会阻塞 await 下一行及之后的代码,需等一轮微任务完成后,再执行下一行之后的代码。
- 可以理解为「紧跟着 await 后面的语句相当于放到了 new Promise 中,下一行及之后的语句相当于放在 Promise.then 中」。
- 使用 async await 的时候,await 背后的代码如果没有返回值,即状态一直是 pending 状态的话,await 下一行及之后的代码是不会执行的。
js
const async1 = async () => {
await new Promise((resolve) => {
console.log('promise1')
})
// 下面的两行不会执行
console.log('async1-end')
return 'async1-success'
}
// 因为 async1() 状态是 pending ,所以 .then 也不会执行
async1().then((res) => console.log(res))
- new promise 里的内容属于同步代码。
js
new Promise(() => {
console.log(1)
})
console.log(2)
// 输出顺序:1、2
- 空的 async 函数,返回值是 fulfilled 状态的 promise,返回值是 undefined 。
js
async function fn() {}
console.log(fn()) // Promise {<fulfilled>: undefined}
- async 中如果没有 await,那么它就是一个纯同步函数。
请回答以下所有示例的执行结果
js
async function t1() {
let a = await 1
console.log(a)
console.log(2)
}
t1()
console.log(3)
答案
输出顺序为:3、1、2
解析: await 是一个表达式,如果后面不是一个 promise 对象,就会把后面的值使用 promise 包裹起来再直接返回。即 await 后面的代码其实是一个 微任务
,而不是同步执行的。
async await 是 generator 的语法糖, generator 遇到 yield ,是会暂停执行代码的,需要手动调用 next 方法,上面代码改成 generator 的写法如下:
js
function* t1() {
let a = yield 1
console.log(a)
console.log(2)
}
const generator = t1()
let result = generator.next() // {value: 1, done: false}
result.value = Promise.resolve(result.value) // 转成 promise
result.value.then((data) => {
// 把 data 返回,给 yield 1 当返回值
generator.next(data)
})
console.log(3)
js
async function t1() {
let a = await 1
console.log(a)
console.log(2)
}
t1()
Promise.resolve().then(() => {
console.log(4)
})
console.log(3)
答案
输出顺序为:3、1、2、4
解析: 当执行到 await 后面的代码时,执行完 await 后面的微任务,后面跟着的就是同步代码,所以会先打印 2 ,再去执行另外的微任务,打印 4.
js
async function t2() {
let a = await new Promise((resolve) => {})
console.log(a)
console.log(2)
}
t2()
console.log(3)
答案
输出顺序为:3
解析: await 后面如果跟一个 promise 对象,await 将等待这个 promise 对象的 resolve 状态的值 value,且将这个值返回给前面的变量,此时的 promise 对象的状态是一个 pending 状态,没有 resolve 状态值,所以什么也打印不了。
js
async function t3() {
let a = await new Promise((resolve) => {
resolve()
})
console.log(a)
console.log(2)
}
t3()
console.log(3)
答案
输出顺序为:3、undefined、2
解析: 因为 promise 中的 resolve 没有返回值,所以默认是 undefined。
js
async function t4() {
let a = await new Promise((resolve, reject) => {
reject(new Error('reject'))
})
console.log(a)
console.log(2)
}
t4()
console.log(3)
答案
输出顺序为:3 ,然后报错 Error: reject
解析: 在执行 await 后面的内容时,由于 promise 返回到是错误状态,所以会导致后面的代码不再执行,如需执行可以使用 try catch 包裹。
js
async function t4() {
try {
let a = await new Promise((resolve, reject) => {
reject('reject')
})
console.log(a)
} catch (error) {
console.log(error)
}
console.log(2)
}
t4()
console.log(3)
改造后的输出顺序为:3 、reject、2
js
async function t6() {
let a = await fn().then((res) => {
return res
})
console.log(a)
console.log(2)
}
async function fn() {
await new Promise((resolve) => {
resolve('lagou')
})
}
t6()
console.log(3)
答案
输出顺序为:3、undefined、2
解析: 首先看下面代码的执行结果
js
async function fn() {}
console.log(fn()) // Promise {<fulfilled>: undefined}
由此可以看出,如果执行一个空的 async 函数,返回值是 fulfilled 状态的 promise。
接下来就可以解析题目了:
由于 fn() 返回的是 undefined ,所以 fn().then 返回的也是 undefined 。
js
const fn = () =>
new Promise((resolve, reject) => {
console.log(1)
resolve('success')
})
fn().then((res) => {
console.log(res)
})
console.log('start')
答案
输出顺序:1、'start'、'success'
解析: fn 函数它是直接返回了一个 new Promise 的,而且 fn 函数的调用是在 start 之前,所以它里面的内容应该会先执行。
js
async function t7() {
let a = await fn().then((res) => {
return res
})
console.log(a)
console.log(2)
}
async function fn() {
await new Promise((resolve) => {
resolve('lagou')
})
return 'lala'
}
t7()
console.log(3)
答案
输出顺序为:3、'lala'、2
解析: 因为 fn 中返回的数据会被当成 promise 的返回值。
js
async function async1() {
console.log('A')
async2()
console.log('B')
}
async function async2() {
console.log('C')
}
console.log('D')
setTimeout(function () {
console.log('F')
}, 0)
async1()
new Promise(function (resolve) {
console.log('G')
resolve()
}).then(function () {
console.log('H')
})
console.log('I')
答案
输出顺序为:D、A、C、B、G、I、H、F
解析: 需要注意的只有一点,在于 async 中如果没有 await,那么它就是一个纯同步函数。
js
// 只是在 async2() 前面加了个 await
async function async1() {
console.log('A')
await async2()
console.log('B')
}
async function async2() {
console.log('C')
}
console.log('D')
setTimeout(function () {
console.log('F')
}, 0)
async1()
new Promise(function (resolve) {
console.log('G')
resolve()
}).then(function () {
console.log('H')
})
console.log('I')
答案
输出顺序为:D、A、C、G、I、B、H、F
解析: await async2() 可以替换成如下代码
js
async function async1() {
console.log('A')
await async2()
console.log('B')
}
async function async2() {
console.log('C')
}
// ===>> 替换成如下
async function async1() {
console.log('A')
await new Promise((resolve) => {
console.log('C')
resolve()
})
console.log('B')
}
js
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
console.log('promise1', promise1)
console.log('promise2', promise2)
}, 2000)
答案
输出顺序:
md
promise1 Promise {<pending>}
promise2 Promise {<pending>}
报错 Error: error!!!
promise1 Promise {<fulfilled>: 'success'}
promise2 Promise {<rejected>: Error: error!!!}
中等难度异步执行练习
知识点:
- .then 或者 .catch 中使用
new Error()
创建的错误对象是会走 .then 的,只有使用throw '123231'
,才会被 .catch 捕获。 - .then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
js
const promise = Promise.resolve().then(() => {
return promise
})
promise.then(console.err)
// 报错 Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
- .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传。值透传的意思就是,会把结果一直传递下去,非函数的不起作用。但是.then 或者 .catch 里面的代码还是会执行。
js
Promise.resolve(1)
.then(console.log('我不关心结果'))
.then((res) => console.log(res))
// 我不关心结果、1
- .finally 方法也是返回一个 Promise,他在 Promise 结束的时候,无论结果为 resolved 还是 rejected,都会执行里面的回调函数。
- .finally()方法的回调函数不接受任何的参数。
- .finally 的返回值默认是上一次的 .then 或者 .catch 的返回值,或者在 .finally 返回异常,返回其他的都没用,会直接取默认值。
js
const promise = new Promise((resolve, reject) => {
reject('error')
resolve('success2')
})
promise
.then((res) => {
console.log('then1: ', res)
})
.then((res) => {
console.log('then2: ', res)
})
.catch((err) => {
console.log('catch: ', err)
})
.then((res) => {
console.log('then3: ', res)
})
答案
输出顺序:catch: error、then3: undefined
解析: catch 不管被连接到哪里,都能捕获上层未捕捉过的错误。
至于 then3 也会被执行,那是因为 catch()也会返回一个 Promise,且由于这个 Promise 没有返回值,所以打印出来的是 undefined。
js
Promise.resolve(1)
.then((res) => {
console.log(res)
return 2
})
.catch((err) => {
return 3
})
.then((res) => {
console.log(res)
})
答案
输出顺序:1、2
解析: promise 每次调用 .then 或者 .catch 都会返回一个新的 promise 。
第一个 then 方法执行后,并没有走 catch 里,因为没有报错,所以第二个 then 中的 res 得到的实际上是第一个 then 的返回值。
js
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('timer')
resolve('success1')
resolve('success2')
}, 1000)
})
promise.then((res) => {
console.log(res, Date.now())
})
promise.then((res) => {
console.log(res, Date.now())
})
答案
输出顺序:
md
timer
success1 1691381942349
success1 1691381942350
解析: Promise 的 .then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。
或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。
js
Promise.resolve()
.then(() => {
throw new Error('error!!!') // 语句 1
// new Error('error!!!') // 语句 2
// return new Error('error!!!') // 语句 3
})
.then((res) => {
console.log('then: ', res)
})
.catch((err) => {
console.log('catch: ', err)
})
答案
使用 语句 1 的输出顺序:catch: Error: error!!!
使用 语句 2 的输出顺序:then: undefined
使用 语句 3 的输出顺序:then: Error: error!!!
解析: .then 或者 .catch 中使用 new Error()
创建的错误对象是会走 .then 的,只有使用 throw 'error'
创建的错误对象,才会被 .catch 捕获。
js
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log)
答案
输出顺序:1
解析: .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传。
js
Promise.resolve('1')
.then((res) => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2')
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then((res) => {
console.log('finally2后面的then函数', res)
})
答案
输出顺序:1、finally2、finally、finally2 后面的 then 函数 2
js
function promise1() {
let p = new Promise((resolve) => {
console.log('promise1')
resolve('1')
})
return p
}
function promise2() {
return new Promise((resolve, reject) => {
reject('error')
})
}
promise1()
.then((res) => console.log(res))
.catch((err) => console.log(err))
.then(() => console.log('then1'))
promise2()
.then((res) => console.log(res))
.catch((err) => console.log(err))
.then(() => console.log('then2'))
答案
输出顺序:promise1、1、error、then1、then2
js
async function async1() {
console.log('async1-start')
await async2()
console.log(3)
Promise.resolve().then(() => {
console.log('async1-end')
})
}
async function async2() {
setTimeout(() => {
console.log('timer')
}, 0)
console.log('async2')
}
async1()
console.log('start')
答案
输出顺序:async1-start、async2、start、3、async1-end、timer
js
async function async1() {
console.log('async1-start')
await async2()
console.log('async1-end')
setTimeout(() => {
console.log('timer1')
}, 0)
}
async function async2() {
setTimeout(() => {
console.log('timer2')
}, 0)
console.log('async2')
}
async1()
setTimeout(() => {
console.log('timer3')
}, 0)
console.log('start')
答案
输出顺序:async1-start、async2、start、async1-end、timer2、timer3、timer1
js
async function async1() {
console.log('async1-start')
await new Promise((resolve) => {
console.log('promise1')
})
console.log('async1-success')
return 'async1-end'
}
console.log('srcipt-start')
async1().then((res) => console.log(res))
console.log('srcipt-end')
答案
输出顺序:srcipt-start、async1-start、promise1、srcipt-end
解析:在 async1 中 await 后面的 Promise 是没有返回值的,也就是它的状态始终是 pending 状态,因此 await 下一行之后的代码不会执行,也包括 async1() 后面的 .then 。
js
async function testSometing() {
console.log('执行testSometing')
return 'testSometing'
}
async function testAsync() {
console.log('执行testAsync')
return Promise.resolve('hello-async')
}
async function test() {
console.log('test-start')
const v1 = await testSometing()
console.log(v1)
const v2 = await testAsync()
console.log(v2)
}
test()
var promise = new Promise((resolve) => {
console.log('promise-start')
resolve('promise')
})
promise.then((val) => console.log(val))
console.log('test-end')
答案
输出顺序:test-start、执行 testSometing、promise-start、test-end、testSometing、执行 testAsync、promise、hello-async
js
async function async1() {
await async2()
console.log('async1')
return 'async1-success'
}
async function async2() {
return new Promise((resolve, reject) => {
console.log('async2')
reject('error')
})
}
async1().then((res) => console.log(res))
答案
输出顺序:async2、报错 Uncaught (in promise) error
解析: 因为如果在 async 函数中抛出了错误,则终止错误结果,不会继续向下执行。需要用 try catch 进行处理。
js
const first = () =>
new Promise((resolve, reject) => {
console.log(3)
let p = new Promise((resolve, reject) => {
console.log(7)
setTimeout(() => {
console.log(5)
resolve(6)
console.log(p)
}, 0)
resolve(1)
})
resolve(2)
p.then((arg) => {
console.log(arg)
})
})
first().then((arg) => {
console.log(arg)
})
console.log(4)
答案
输出顺序:3、7、4、1、2、5、Promise {<fulfilled>: 1}
解析: 需要注意的是 p.then 是比 first().then 先进入微任务队列。
js
const async1 = async () => {
console.log('async1')
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise((resolve) => {
console.log('promise1')
})
console.log('async1-end')
return 'async1-success'
}
console.log('script-start')
async1().then((res) => console.log(res))
console.log('script-end')
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.catch(4)
.then((res) => console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)
答案
输出顺序:script-start、async1、promise1、script-end、1、timer2、timer1
解析: async1 函数中的 await new Promise ,由于没有返回值,一直是 pending 状态,后面的代码是不会执行的。
js
const p1 = new Promise((resolve) => {
setTimeout(() => {
resolve('resolve3')
console.log('timer1')
}, 0)
resolve('resovle1')
resolve('resolve2')
})
.then((res) => {
console.log(res)
setTimeout(() => {
console.log(p1)
}, 1000)
})
.finally((res) => {
console.log('finally', res)
})
答案
输出顺序:resovle1、finally undefined、timer1、Promise {<fulfilled>: undefined}
解析: 由于 .finally 接收的是上一个 .then 或 .catch 的返回值,但是这里 .then 没有返回,所以 .finally 是打印 undefined。
最后一个定时器打印出的 p1 其实是 .finally 的返回值
promise 相关的几道面试题
Generator 和 async/await 的区别
- 两者都可以把异步代码变成同步代码。
- Generator 通过 yield 关键字暂停函数执行,并使用 next 方法来恢复函数的执行。
async/await 和 promise 的区别
- 使用方式不同:在使用 async/await 时,需要将异步代码写在 async 函数内部,使用 await 等待异步操作完成。而 Promise 则是直接创建一个 Promise 实例,并在其内部传入异步操作的函数。
- 处理错误的方式不同:在 async/await 中,可以直接使用 try-catch 语句来捕获和处理异步操作抛出的异常,而 Promise 则是使用 catch 方法来处理异常。
- 兼容性不同:async/await 是 ES2017 新增的语法,如果运行环境不支持 async/await,代码就无法正常执行。而 Promise 则是 ES6 新增的语法,但由于该语法兼容性较好,也支持在较低版本的浏览器中使用。
总的来说,async/await 是更加高级、更加易读的异步编程方式,其通过 await 等待异步操作完成,再继续执行下面的代码;而 Promise 则是通过链式调用 then 和 catch 来实现异步操作的管理,其逻辑相对较为复杂,在实际应用中可能会比较难以维护。
Generator 和 promise 的区别
- 异步操作的处理方式:在使用 Generator 实现异步编程时,需要手动控制异步操作的执行流程,通过 yield 关键字暂停函数执行,并使用 next 方法来恢复函数的执行。而 Promise 则是通过 then 方法来处理异步操作的结果,并在异步操作完成后自动调用后续的函数。
- 数据交互方式不同:在 Generator 中,通过 yield 关键字来实现对异步操作输出的数据进行取值,同时也可以使用 next 方法将数据传入 Generator 函数中。而在 Promise 中,则是通过在异步操作后返回一个 Promise 对象,并将异步操作的输出数据传递给 then 方法作为参数来实现数据交互。
- Generator 可以控制异步流程的暂停和恢复,因此可以比 Promise 更灵活地控制异步代码的执行流程。同时,Generator 中的每个 yield 语句都可以看作一个状态,在 Generator 函数执行时可以保存和恢复这个状态,这一点对于一些比较复杂的场景有很大的帮助。
- 兼容性差异较大。 Generator 是 ES6 引入的语法,而 Promise 虽然也是 ES6 中的语法,但 Promise 较少出现不兼容的情况。
使用 Promise 实现每隔 1 秒输出 1,2,3
实现思路:通过链式调用 then 方法,进行每隔 1 秒输出。当然肯定不能直接写 2 个 then 方法,需要进行优化封装。
- 定义一个 clg 方法,返回一个 promise ,用来打印数据。
- 利用数组循环相关的方法,如 forEach、map、reduce 等。去循环打印数组的数据,并且通过把 promise then 方法返回的新 promise ,再去执行 then 方法,从而实现链式调用。
答案
解法 1:
js
function clg(num, time = 1000) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(num)
resolve()
}, time)
})
}
;[1, 2, 3].reduce((res, item) => {
res = res.then(() => clg(item))
return res
}, Promise.resolve())
解法 2:
js
function clg(num, time = 1000) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(num)
resolve()
}, time)
})
}
let promise = Promise.resolve()
;[1, 2, 3].map((item) => {
promise = promise.then(() => clg(item))
})
Promise 实现红绿灯交替重复亮
红灯 3 秒亮一次,黄灯 2 秒亮一次,绿灯 1 秒亮一次;如何让三个灯不断交替重复亮灯?即 3 秒后红灯亮,再过 2 秒黄灯亮,再过 1 秒绿灯亮,再过 3 秒红灯亮,一直循环。
答案
解法 1:
js
function red() {
console.log('red')
}
function yellow() {
console.log('yellow')
}
function green() {
console.log('green')
}
function sleep(fn, time) {
return new Promise((reslove) => {
setTimeout(() => {
fn()
reslove()
}, time)
})
}
async function start() {
while (true) {
await sleep(red, 3000)
await sleep(yellow, 2000)
await sleep(green, 1000)
}
}
start()
解法 2:
js
function red() {
console.log('red')
}
function yellow() {
console.log('yellow')
}
function green() {
console.log('green')
}
function sleep(fn, time) {
return new Promise((reslove) => {
setTimeout(() => {
fn()
reslove()
}, time)
})
}
async function start() {
sleep(red, 3000)
.then(() => {
return sleep(yellow, 2000)
})
.then(() => {
return sleep(green, 1000)
})
.then(() => {
return start()
})
}
start()
实现 mergePromise 函数
实现把传进去的数组按顺序先后执行,并且把返回的数据先后放到新数组 result 中。这原理其实跟第一个练习题一样。
答案
js
// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]
const time = (timer) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, timer)
})
}
const ajax1 = () =>
time(2000).then(() => {
console.log(1)
return 1
})
const ajax2 = () =>
time(1000).then(() => {
console.log(2)
return 2
})
const ajax3 = () =>
time(1000).then(() => {
console.log(3)
return 3
})
function mergePromise(ajaxArray) {
// 存放每个ajax的结果
const data = []
let promise = Promise.resolve()
ajaxArray.forEach((ajax) => {
// 第一次的then为了用来调用ajax
// 第二次的then是为了获取ajax的结果
promise = promise.then(ajax).then((res) => {
data.push(res)
return data // 把每次的结果返回
})
}) // 最后得到的promise它的值就是data
return promise
}
mergePromise([ajax1, ajax2, ajax3]).then((data) => {
console.log('done')
console.log(data) // data 为 [1, 2, 3]
})
封装一个异步加载图片的方法
js
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = function () {
console.log('一张图片加载完成')
resolve(img)
}
img.onerror = function () {
reject(new Error('加载失败' + url))
}
img.src = url
})
}
限制异步操作的并发个数并尽可能快的完成全部
实现同时执行的异步任务最多只能有 2 个,当其中 1 个完成后,再执行下一个,一直到执行完所有任务
js
// 题目,基础代码
class Scheduler {
add(task) {
// .......
}
}
const timeout = (time) => {
return new Promise((resolve) => {
setTimeout(resolve, time)
})
}
const scheduler = new Scheduler()
const addTask = (time, order) => {
scheduler
.add(() => timeout(time))
.then(() => {
console.log(order)
})
}
addTask(1000, '1')
addTask(500, '2')
addTask(200, '3')
addTask(300, '4')
// 要求最终打印 2 3 1 4
答案
js
class Scheduler {
constructor(maxCount = 2) {
this.maxCount = maxCount // 最大同时执行任务数
this.queue = [] // 待执行的任务
this.run = [] // 正在执行的任务
}
add(task) {
return new Promise((resolve) => {
this.queue.push([task, resolve])
this.handleTask()
})
}
handleTask() {
if (this.run.length < this.maxCount && this.queue.length > 0) {
// 执行 待执行任务中的任务,并把它添加到正在执行的任务数组中
const [task, resolve] = this.queue.shift()
const p = task().then(() => {
// 如果正在执行的任务数组中,有当前任务,则删除
this.run.splice(this.run.indexOf(p), 1) // 执行当前任务的 resolve
resolve() // 重复此步骤
this.handleTask()
}) // 添加到正在执行的任务数组中
this.run.push(p)
}
}
}
const timeout = (time) => {
return new Promise((resolve) => {
setTimeout(resolve, time)
})
}
const scheduler = new Scheduler()
const addTask = (time, order) => {
scheduler
.add(() => timeout(time))
.then(() => {
console.log(order)
})
}
addTask(1000, '1')
addTask(500, '2')
addTask(200, '3')
addTask(300, '4')
// 最终打印 2 3 1 4
用 promise 递归实现拉取 100 条数据, 每次拉取 20 条,结束条件为当次拉取不足 20 条或者已经拉取 100 条数据
答案
js
const arr = [
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, 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,
]
function getData(arr, size = 20) {
let result = []
return handleData(arr, size, result)
}
function handleData(arr, size, result) {
return skipData(arr, size, result).then((res) => {
if (result.length < arr.length) {
result = result.concat(res)
return handleData(arr, size, result)
} else {
return result
}
})
}
function skipData(arr, size, result) {
return new Promise((resolve) => {
let d = arr.slice(result.length, result.length + size)
resolve(d)
})
}
/*
接收两个参数:
arr: 需要拉取的数据 Array
size: 每次拉取多少条 Number
*/
getData(arr).then((res) => {
console.log(res)
})
实现 Promise
注意:原生 promise 及相关 api 是微任务,而我实现的都是同步代码。这里只实现功能,不实现相关的执行顺序问题。
首先我们看下原本的代码,根据 promise 去实现功能。
js
let p1 = new Promise((resolve, reject) => {
// setTimeout(() => {
// resolve('setTimeout 成功')
// }, 2000)
resolve('成功')
// reject(new Error('失败'))
})
p1.then((res) => {
console.log(1, res)
throw '失败'
})
.catch((res) => {
console.log(6, res)
return 'aaa'
})
.then((res) => {
console.log(5, res)
})
p1.then(
(res) => {
console.log(2, res)
return 'ccc'
},
(err) => {
console.log(3, err)
}
).then((res) => {
console.log(7, res)
})
- promise 是一个类,传入一个函数,函数会立即执行。有两个固定参数 resolve 和 reject.
- resolve 是用来把状态更改为成功的函数。 reject 是用来把状态更改为失败的函数。
- promise 执行的时候有三种状态 进行中 pending 成功 fulfilled 失败 rejected。 状态变更只有以下两种,且是不可逆的 ① pending => fulfilled ② pending => rejected
js
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
status = PENDING // promise 的状态
value = undefined // 成功的值
reason = undefined // 失败的值
constructor(init) {
const resolveHandler = (value) => {
// 如果状态已经改变了,不执行后面的代码
if (this.status !== PENDING) return
// 修改状态
this.status = FULFILLED
// 保存成功的值
this.value = value
}
const rejectHandler = (reason) => {
if (this.status !== PENDING) return
this.status = REJECTED
this.reason = reason
}
try {
init(resolveHandler, rejectHandler)
} catch (error) {
// 如果报错,就直接调用失败相关逻辑
rejectHandler(error)
}
}
}
接下来实现 then 方法。
- then 方法是根据状态,来执行成功和失败回调函数。
- 成功的回调函数能接收到状态变更时传递的值 失败的回调函数能接收到状态变更为失败时传递的值
- 在状态还是 pending 的情况下,即进行异步操作的时候,要把成功和失败的回调函数存储起来,然后在状态变更时,依次执行回调。同步的时候直接执行成功和失败的回调函数。
- 实现链式调用 then 方法,即在调用 then 方法的时候,要返回一个新的 promise 对象。上一个 then 方法的返回值,是下一个 then 的 value
- 链式调用 then 方法的参数可以不传,变为可选参数。一直把第一个的返回值,原封不动的返回给下一个 then
js
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
status = PENDING // promise 的状态
value = undefined // 成功的值
reason = undefined // 失败的值
resolveCallback = [] // 异步时,存储 then 传递的对应的回调函数,好等状态变更时去执行
rejectCallback = [] // 异步时,存储 then 或 catch 传递的对应的回调函数,好等状态变更时去执行
constructor(init) {
const resolveHandler = (value) => {
// 如果状态已经改变了,不执行后面的代码
if (this.status !== PENDING) return
// 修改状态
this.status = FULFILLED
// 保存成功的值
this.value = value
// 循环存储成功回调的数组,依次执行里面的回调。
while (this.resolveCallback.length) {
this.resolveCallback.shift()()
}
}
const rejectHandler = (reason) => {
if (this.status !== PENDING) return
this.status = REJECTED
this.reason = reason
// 循环存储失败回调的数组,依次执行里面的回调。
while (this.rejectCallback.length) {
this.rejectCallback.shift()()
}
}
try {
init(resolveHandler, rejectHandler)
} catch (error) {
// 如果报错,就直接调用失败相关逻辑
rejectHandler(error)
}
}
then(fn1, fn2) {
// 处理用户传参不规范的问题
fn1 = typeof fn1 === 'function' ? fn1 : (x) => x
fn2 =
typeof fn2 === 'function'
? fn2
: (x) => {
throw x
}
let p = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
try {
// 接收 then方法的返回值,作为下次then的value
const res = fn1(this.value)
resolve(res)
} catch (error) {
reject(error)
}
} else if (this.status === REJECTED) {
try {
const res = fn2(this.reason)
resolve(res)
} catch (error) {
reject(error)
}
} else {
// 这里是处理,调用 then 方法的时候, 状态还未变更时,需把对应的回调存储起来。
this.resolveCallback.push(() => {
// 这里再包一层的原因是为了获取返回值,好链式调用。当这里面的代码被执行的时候,说明状态已经变更了
try {
const res = fn1(this.value)
resolve(res)
} catch (error) {
reject(error)
}
})
this.rejectCallback.push(() => {
try {
const res = fn2(this.reason)
resolve(res)
} catch (error) {
reject(error)
}
})
}
})
return p
}
catch(fn1) {
return this.then(null, fn1)
}
}
上面的代码虽然是可以使用了,但是还没有考虑另外一种情况,即 then 方法中,如果返回 promise,或返回本身的时候,接下来就进行改造一下。
js
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
status = PENDING // promise 的状态
value = undefined // 成功的值
reason = undefined // 失败的值
resolveCallback = [] // 异步时,存储 then 传递的对应的回调函数,好等状态变更时去执行
rejectCallback = [] // 异步时,存储 then 或 catch 传递的对应的回调函数,好等状态变更时去执行
constructor(init) {
const resolveHandler = (value) => {
// 如果状态已经改变了,不执行后面的代码
if (this.status !== PENDING) return
// 修改状态
this.status = FULFILLED
// 保存成功的值
this.value = value
// 循环存储成功回调的数组,依次执行里面的回调。
while (this.resolveCallback.length) {
this.resolveCallback.shift()()
}
}
const rejectHandler = (reason) => {
if (this.status !== PENDING) return
this.status = REJECTED
this.reason = reason
// 循环存储失败回调的数组,依次执行里面的回调。
while (this.rejectCallback.length) {
this.rejectCallback.shift()()
}
}
try {
init(resolveHandler, rejectHandler)
} catch (error) {
// 如果报错,就直接调用失败相关逻辑
rejectHandler(error)
}
}
then(fn1, fn2) {
// 处理用户传参不规范的问题
fn1 = typeof fn1 === 'function' ? fn1 : (x) => x
fn2 =
typeof fn2 === 'function'
? fn2
: (x) => {
throw x
}
let p = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
// 这里使用 setTimeout 包裹的原因是因为 handleThen 需要拿到当前 p 本身,所以加个 setTimeout 变成异步的,就可以拿到了
setTimeout(() => {
try {
// 接收 then方法的返回值,作为下次then的value
const res = fn1(this.value)
// handleThen 是处理 then 中回调的返回值是本身、promise、普通值的方法
handleThen(p, res, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
} else if (this.status === REJECTED) {
setTimeout(() => {
try {
const res = fn2(this.reason)
handleThen(p, res, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
} else {
// 这里是处理,调用 then 方法的时候, 状态还未变更时,需把对应的回调存储起来。
this.resolveCallback.push(() => {
// 这里再包一层的原因是为了获取返回值,好链式调用。当这里面的代码被执行的时候,说明状态已经变更了
setTimeout(() => {
try {
const res = fn1(this.value)
handleThen(p, res, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
this.rejectCallback.push(() => {
setTimeout(() => {
try {
const res = fn2(this.reason)
handleThen(p, res, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
}
})
return p
}
catch(fn1) {
return this.then(null, fn1)
}
}
function handleThen(currentPromise, value, resolve, reject) {
// 如果 返回值 是 promise 本身的话,直接报错,打断运行
if (currentPromise === value) {
reject(new Error('不能返回本身'))
return
}
// 如果返回值是一个promise
if (value instanceof MyPromise) {
value.then(
(res) => resolve(res),
(err) => reject(err)
)
} else {
resolve(value)
}
}
实现其他方法
- finally 方法,传递一个函数,返回一个 promise 对象和上一个 promise 返回的值(如果有的话)
- promise.all 方法 传递一个数组,按照数组里面的顺序,处理完后,按照顺序返回结果。如果是普通值,则直接返回,如果是 promise,则处理后返回。需要等数组里的元素全部变成 resolve 时返回,如果有一个状态为 reject,则直接返回 reject
- promise.race 方法 传递一个数组,如果是普通值,则变成 promise 返回,如果是 promise,则处理后返回。只要有一个的状态变了,就返回那个
- promise.first 方法,将多个 Promise 实例,包装成一个新的 Promise 实例,只要参数实例有一个变成 fulfilled 状态,包装实例就会变成 fulfilled 状态;如果所有参数实例都变成 rejected 状态,包装实例就会变成 rejected 状态。
- promise.last 方法,将多个 Promise 实例,包装成一个新的 Promise 实例,获取最后一个 promise 实例的状态,并将其作为包装实例的状态返回
- promise.none 方法,将多个 Promise 实例,包装成一个新的 Promise 实例,获取第一个状态为 fulfilled 状态的实例,将其作为包装实例的 rejected 状态返回, 如果所有实例状态都为 rejected ,则返回一个数组,将其作为包装实例的 fulfilled 状态返回。
js
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
status = PENDING // promise 的状态
value = undefined // 成功的值
reason = undefined // 失败的值
resolveCallback = [] // 异步时,存储 then 传递的对应的回调函数,好等状态变更时去执行
rejectCallback = [] // 异步时,存储 then 或 catch 传递的对应的回调函数,好等状态变更时去执行
constructor(init) {
const resolveHandler = (value) => {
// 如果状态已经改变了,不执行后面的代码
if (this.status !== PENDING) return
// 修改状态
this.status = FULFILLED
// 保存成功的值
this.value = value
// 循环存储成功回调的数组,依次执行里面的回调。
while (this.resolveCallback.length) {
this.resolveCallback.shift()()
}
}
const rejectHandler = (reason) => {
if (this.status !== PENDING) return
this.status = REJECTED
this.reason = reason
// 循环存储失败回调的数组,依次执行里面的回调。
while (this.rejectCallback.length) {
this.rejectCallback.shift()()
}
}
try {
init(resolveHandler, rejectHandler)
} catch (error) {
// 如果报错,就直接调用失败相关逻辑
rejectHandler(error)
}
}
then(fn1, fn2) {
// 处理用户传参不规范的问题
fn1 = typeof fn1 === 'function' ? fn1 : (x) => x
fn2 =
typeof fn2 === 'function'
? fn2
: (x) => {
throw x
}
let p = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
// 这里使用 setTimeout 包裹的原因是因为 handleThen 需要拿到当前 p 本身,所以加个 setTimeout 变成异步的,就可以拿到了
setTimeout(() => {
try {
// 接收 then方法的返回值,作为下次then的value
const res = fn1(this.value)
// handleThen 是处理 then 中回调的返回值是本身、promise、普通值的方法
handleThen(p, res, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
} else if (this.status === REJECTED) {
setTimeout(() => {
try {
const res = fn2(this.reason)
handleThen(p, res, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
} else {
// 这里是处理,调用 then 方法的时候, 状态还未变更时,需把对应的回调存储起来。
this.resolveCallback.push(() => {
// 这里再包一层的原因是为了获取返回值,好链式调用。当这里面的代码被执行的时候,说明状态已经变更了
setTimeout(() => {
try {
const res = fn1(this.value)
handleThen(p, res, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
this.rejectCallback.push(() => {
setTimeout(() => {
try {
const res = fn2(this.reason)
handleThen(p, res, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
}
})
return p
}
catch(fn1) {
return this.then(null, fn1)
}
finally(fn) {
// 不管是请求成功还是失败都会执行
return this.then(
// 这里是处理传递的参数后,返回一个promise对象,并把上一个返回值原样返回给下一个
(value) => MyPromise.resolve(fn()).then(() => value),
(error) =>
MyPromise.resolve(fn()).then(() => {
throw error
})
)
}
static resolve(data) {
// 如果没有参数,默认返回undefined
data = data ? data : undefined
// 如果参数是promise对象则直接返回,如果是普通值则经过处理后返回一个promise
if (data instanceof MyPromise) return data
return new MyPromise((resolve) => {
resolve(data)
})
}
static reject(data) {
// 如果没有参数,默认返回undefined
data = data ? data : undefined
// 如果参数是promise对象则直接返回,如果是普通值则经过处理后返回一个promise
if (data instanceof MyPromise) return data
return new MyPromise((resolve, reject) => {
reject(data)
})
}
static all(array) {
// 要返回的数组
let result = []
// 用来处理异步情况的中间值
let index = 0
return new MyPromise((resolve, reject) => {
// 往数组按原来的顺序添加返回结果
function addData(key, value) {
result[key] = value
index++
if (index == array.length) {
//判断promise是否全部执行完,因为只有异步操作执行完了,才会执行addData,然后再执行resolve
resolve(result)
}
}
for (let key = 0; key < array.length; key++) {
const element = array[key]
if (element instanceof MyPromise) {
//如果数组里的元素是promise对象,则处理后返回
element.then(
(value) => addData(key, value),
(error) => reject(error)
)
} else {
// 直接返回
addData(key, element)
}
}
})
}
static race(array) {
return new MyPromise((resolve, reject) => {
for (let key = 0; key < array.length; key++) {
const element = array[key]
if (element instanceof MyPromise) {
//如果数组里的元素是promise对象,则处理后返回
element.then(
(value) => resolve(value),
(error) => reject(error)
)
} else {
// 直接返回
resolve(element)
}
}
})
}
static none(promiseList) {
return MyPromise.all(
promiseList.map((pms) => {
return new MyPromise((resolve, reject) => {
// 将pms的resolve和reject反过来
return MyPromise.resolve(pms).then(reject, resolve)
})
})
)
}
}
function handleThen(currentPromise, value, resolve, reject) {
// 如果 返回值 是 promise 本身的话,直接报错,打断运行
if (currentPromise === value) {
reject(new Error('不能返回本身'))
return
}
// 如果返回值是一个promise
if (value instanceof MyPromise) {
value.then(
(res) => resolve(res),
(err) => reject(err)
)
} else {
resolve(value)
}
}