Skip to content
当前页导航

数据类型

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 执行上下文。

  1. 全局执行上下文:当 JavaScript 代码首次执行时,会创建全局执行上下文。全局执行上下文是最外层的执行上下文,包含着整个代码文件中的全局变量和函数声明。全局执行上下文一直存在于代码执行过程中,直到程序退出。

  2. 函数执行上下文:每当一个函数被调用时,都会创建一个函数执行上下文。函数执行上下文与该函数的相关变量和函数声明有关。当函数执行完毕,函数执行上下文会被销毁。如果函数内部存在嵌套的函数调用,会创建多个嵌套的函数执行上下文。上下文栈执行的是先进后出机制。

  3. eval 执行上下文:当代码包含 eval 函数时,会创建一个 eval 执行上下文。eval 执行上下文与 eval 函数中的代码相关联。

无论是全局执行上下文、函数执行上下文还是 eval 执行上下文,都具有相似的执行顺序:

  1. 创建变量对象:变量对象包含了在上下文中定义的所有变量和函数声明。
  2. 创建作用域链:作用域链是一个指向父级执行上下文的指针列表,用于变量查找。
  3. 确定 this 值:this 值指向当前执行代码的对象。
  4. 执行代码:按照代码的顺序执行。

当函数执行上下文创建时,还会进行额外的步骤:

  1. 创建活动对象:活动对象是函数的局部变量、参数和内部函数的存储空间。
  2. 设置函数的 arguments 对象:arguments 对象包含了函数被调用时传递的参数。
  3. 在作用域链的顶部添加活动对象。

作用域基本介绍

当我们在 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()

f74797065352e706e67

图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。

知识点:

  • 引用类型都是对象,每个对象都有原型对象。
  • 对象都是由构造函数创建,对象的 __proto__ 属性等于创建它的构造函数的 prototype 属性。
  • 所有通过字面量表示法创建的普通对象的构造函数为 Object
  • 所有原型对象都是普通对象,构造函数为 Object
  • 所有函数的构造函数是 Function
  • Object.prototype 没有原型对象

constructor

回忆一下之前的描述,构造函数都有一个 prototype 属性,指向使用这个构造函数创建的对象实例的原型对象。

这个原型对象中默认有一个 constructor 属性,指回该构造函数。

js
function Person() {}
console.log(Person === Person.prototype.constructor) // true

6f746f74797065332e706e67

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()) // 会报错

执行上面的这段代码,可以得到这样的结果。

20230802220642

可以看到最后打印的 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 在这个生成实例的过程中到底进行了哪些步骤来实现呢?总结下来大致分为以下几个步骤。

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(this 指向新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

那么问题来了,如果不用 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)又不同,它不会立即执行函数。

应用场景:

  1. 判断数据类型
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'
  1. 类数组借用数组方法,比如借用数组的 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}
  1. 获取数组的最大 / 最小值
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
  1. 继承

实现 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)
  }
}