前端基础知识整理——手写JS

113 阅读15分钟

类的继承

原型和原型链中说到,JavaScript 中是没有类的,只能用原型模拟类,继承也是如此。

继承即是子类获得父类的属性和方法。在 JavaScript 中,我们可以通过原型链访问原型对象的属性的原理,将子类(对象)的原型指向父类(对象)来模拟继承。

function Parent () {}
function Child () {}

Child.prototype = new Parent()

这种继承方式称为原型继承,其缺点是子类属性只是对父类属性的引用,不是子类自己的属性,修改会影响父类。

因此我们可以使用父类的构造函数,将父类的属性在子类中复制一遍,以解决引用的问题。

function Parent () {}
function Child () {
    Parent.call(this)
}

这种方式又带来新的问题,那就是无法实现对象方法的共用,浪费了内存。

我们结合上面两种继承的优点,就有以下的代码,称为组合继承

function Parent () {}
function Child () {
    Parent.call(this)
}

Child.prototype = new Parent()
Child.prototype.constructor = Child

上面有一行代码修改了 Child.prototype.constructor,这是因为通过 new Child() 创建的对象,其 constructor 属性是在 Child.prototype 上,通过原型链访问。而我们手动改变了 Child.prototypeconstructor 也指向了 Parent,这明显不是我们想要的效果,因此手动修复 constructor 的指向。

组合继承解决了引用问题和方法无法共用的问题。但需要调用两次父类的构造函数,存在多一份父类的属性。

最终的方法是通过 Object.create() 创建一个父类原型的对象,两个对象互不关联,但拥有共同的原型。

function Parent () {}
function Child () {
    Parent.call(this)
}

Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

上面的方法称为寄生组合继承,是最好的继承方法。当然,ES6 已经有了类的语法糖,可以很方便地实现继承。

浅拷贝

赋值

Javascript 中的数据类型可分为两类,基础类型和引用类型。基础类型如 NumberStringBoolean 等。基础类型的数据在内存中是保存在栈中的。引用类型为 Object。引用类型的数据是存储在堆中的,并在栈中保存堆地址。

赋值时会给新变量分配一块新的内存空间,将值复制给新变量。但引用类型在栈中保存的是内存地址,因此赋值时会直接将内存地址赋值给新变量,两个变量指向同一个对象。

var a = { foo: 1 }
var b = a // a 和 b都指向 { foo: 1 } 这个对象
a.foo = 2
console.log(b.foo) // 2。修改其中一个变量的属性,另一个也会改变
a = { bar: 1 }
console.log(b) // { foo: 2 } a 指向了新的对象,不会对 b 造成影响

拷贝

拷贝即复制,是针对引用类型的一种操作。拷贝会将对象的属性在堆中生成一个备份,分配新的堆内存地址,并将新的地址赋值给新变量。浅拷贝会对对象的每个属性生成一个备份,当对象属性是基础类型时,则复制其值,如果对象属性是引用类型时,则复制其地址。浅拷贝可以理解为复制对象的第一层。

// 遍历复制
function copy(target) {
    if (typeof target !== 'object') {
        return target
    }

    var newObj = {}
    for (var i in target) {
        // 去掉原型链上的属性
        if (target.hasOwnProperty(i)) {
            newObj[i] = target[i]
        }
    }
    return newObj
}

// Object.assign
var target = { a: 1 }
var newObj = Object.assign({}, target)
var arr = [0, 1, 2]
var newArr = [ ...arr ]

// 展开运算符
var target = { a: 1 }
var newObj = { ...target }

// Array.prototype.concat
var arr = [0, 1, 2]
var newArr = arr.slice()

// Array.prototype.slice
var arr = [0, 1, 2]
var newArr = arr.slice()

深拷贝

深拷贝是对整个对象的完整复制,两个对象互不影响

乞丐版实现

乞丐版深拷贝可以应付一般情况。但是一些特殊值在拷贝时会丢失,如 RexExpDate 等,也无法处理循环引用的问题。

var target = { a: 1 }
var newObj = JSON.parse(JSON.stringify(target))

递归拷贝

使用递归对对象进行拷贝,还需要考虑不可拷贝对象,循环应用的问题,以及性能问题。

const deepCopy = (target, map = new WeakMap()) => {
    // 处理 typeof 返回 object,但不能继续拷贝的数据类型,可以多增加几种类型的处理
    if (target === null) return target
    if (target instanceof Date) return new Date(target)
    if (target instanceof RegExp) return new RegExp(target)

    // 处理原始类型及函数类型,直接返回
    if (typeof target !== 'object') return target

    // 判断对象,数组两种需要递归拷贝的数据类型
    let cloneTarget = Array.isArray(target)
        ? []
        : target.constructor() // 处理拷贝时抛弃对象构造函数的问题

    // 处理循环引用的问题,通过WeakMap保存拷贝过的值
    if (map.get(target)) {
        return map.get(target)
    }
    map.set(target, cloneTarget)

    // 递归拷贝
    for (let key in target) {
        // 去掉原型链上的属性
        if (target.hasOwnProperty(key)) {
            cloneTarget[key] = deepCopy(target[key], map)
        }
    }

    return cloneTarget
}

call & apply

函数的callapply方法可以显示绑定函数的 this。这是利用 this 的隐式绑定实现的,即函数作为对象属性调用时,函数内部的this指向该对象。

callapply 的区别在于传参上的不同。call 需要传入函数的参数列表,而apply则是将函数参数作为数组传入。

主要的实现步骤为:

  1. 将函数设置为对象的属性(如果没有上下文则函数指向 window
  2. 执行函数,并将剩余参数传给函数(es6以下用 arguments 获取剩余函数,用 eval 执行函数)
  3. 删除属性

call

// ES5
Function.prototype._call = function (context) {
    // 将函数设置为对象的属性
    context = context || window
    context.fn = this

    var args = []
    // 注意循环的起始为1,需要去掉第一个参数 context
    for (var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']')
    }

    // eval将字符串作为代码执行 context.fn(arguments[1], arguments[2]...)
    var result = eval('context.fn(' + args + ')')
    // 删除属性
    delete context.fn

    return result
}

// ES6
Function.prototype._call = function (context, ...args) {
    // es6可通过...运算符收集剩余参数,args为一个数组
    context = context || window
    context.fn = this
    // 将args数组通过...运算符展开,传递给fn
    let result = context.fn(...args)
    delete context.fn
    return result
}

apply

// ES5
Function.prototype._apply = function (context, arr) {
    context = context || window
    context.fn = this
    var result

    if (typeof arr === 'undefined') {
        // 如果第二个参数不存在则直接调用,获取结果
        result = context.fn()
    } else {
        if (!(arr instanceof Array)) {
            throw new Error('params must be array')
        }

        var args = []
        for (var i = 0; i < arr.length; i++) {
            args.push('arr[' + i + ']')
        }

        var result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result
}

// ES6
Function.prototype._apply = function (context, args) {
    context = context || window
    context.fn = this
    let result
    
    if (typeof args === 'undefined') {
        result = context.fn()
    } else {
        if (!Array.isArray(args)) throw new Error('params must be array')
        
        result = context.fn(...args)
    }
    
    delete context.fn
    return result
}

bind

bind 方法会创建一个新函数,当这个新函数被调用时,bind 的第一个参数将作为其 this,剩余的参数将作为新函数的实参。

bind 函数的有以下的特点:

  1. 返回一个函数,将其 this 绑定到第一个参数
  2. 传递预设参数
  3. bind 返回的函数可当作构造函数使用。此时 bind 绑定的 this 无效,但传入的参数仍有效
Function.prototype._bind = function (context) {
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable')
    }

    // 保存绑定的函数
    var self = this
    // 获取预设参数
    var args = Array.prototype.slice.call(arguments, 1)
    var fNOP = function () {}

    var fBound = function () {
        // 判断是普通调用还是构造函数调用
        return self.apply(
            this instanceof fBound ? this : context,
            args.concat(Array.prototype.slice.call(arguments))
        )
    }

    // 作为构造函数调用时,将其原型指向绑定函数的原型
    // fNOP 作为中转,直接绑定原型(fBound.prototype = this.prototype)容易修改绑定函数的原型
    fNOP.prototype = self.prototype
    fBound.prototype = new fNOP()

    return fBound
}

函数柯里化

通过闭包收集参数,如果多次传入的参数等于 fn 的形参数目,则返回 fn 的执行结果,否则返回一个函数

function curry(fn) {
  var length = fn.length // fn的形参数目
  var args = Array.prototype.slice(arguments, 1)

  return function() {
    var _args = args.concat(Array.prototype.slice.call(arguments))
    if (_args.length < length) {
      return curry.call(this, fn, _args)
    }
    
    return fn.apply(this, _args)
  }
}

ES6 版代码:

  const curry = (fn, ...args) => args.length < fn.length
    ? (..._args) => curry(fn, ...args, ..._args)
    : fn(...args)

数组去重

Set

[...new Set(arr)]

splice

const unique = arr => {
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] === arr[j]) {
        arr.splice(j, 1)
        j --
      }
    }
  }
  return arr
}

indexOf + filter

数组的 indexOf 方法返回指定元素第一个索引,与遍历的 i 不相等时说明存在多个相同元素。

const unique = arr => arr.filter((v, i) => arr.indexOf(v) === i)

includes

const unique = arr => {
  const res = []
  arr.forEach(v => !res.includes(v) && res.push(v))
  return res
}

Map

const unique = arr => {
  const map = new Map()
  const res = []
  arr.forEach(v => {
    if (!map.get(v)) {
      res.push(v)
      map.set(v, 1)
    }
  })
  return res
}

数组扁平化

数组扁平化即将多为数组转化为一维数组。可通过 Array.prototype.flat() 实现。也有其他实现数组扁平化的方法。

递归实现

const flat = (array) => {
    let result = []
    
    for(let i = 0, len = array.length; i < len; i++) {
        array[i] instanceof Array
            ? result.concat(flat(array[i]))
            : result.push(i)
    }

    return result
}

循环实现

数组扁平化其实也是一种 DFS。

const flat = (array) => {
    let result = []
    let stack = [...array]

    while (stack.length) {
        let item = stack.pop()
        item instanceof Array
            ? stack.push(...item)
            : result.push(item)
    }

    return result.reverse()
}

reduce

数组的 reduce 方法结合递归也可以实现数组扁平化。

const flat = (array) => {
    return array.reduce((prev, next) => prev.concat(
        Array.isArray(next) ? flat(next) : next
    ), [])
}

扩展运算符

扩展运算符能展开数组,根据这个原理可以实现扁平化。

const flat = (array) => {
    let result = [...array]
    while (result.some(v => Array.isArray(v))) {
        result = [].concat(...result)
    }
    return result
}

toString

如果数组元素都是数字,可以用toString方法实现扁平化。数组的 toString 方法会把数组转成字符串,该字符串为数组元素用,拼接而成。

const flat = (array) => array.toString().split(',').map(Number)

EventBus

event bus(事件总线) 是 node 中各个模块的基石,也是前端组件之间重要的通信手段之一。DOM 的事件也是一种发布订阅模式,event bus可以看成自定义事件,可以模拟 DOM2 级事件的接口,即提供注册事件处理函数,触发事件,移除事件处理函数三个接口。

event bus 基于发布订阅模式。该设计模式维护一个消息中心,当注册事件处理函数是,往消息中心中添加事件及其处理函数,如果一个事件有多个处理函数,则用数组保存这些处理函数。当事件触发时,调用该事件的所有处理函数。

class EventEmeitter {
    constructor () {
        // 消息(事件)中心,用来保存事件及其处理函数
        this._events = this._events || new Map()
    }
    // 添加事件处理函数
    addListener (type, fn) {
        if (typeof fn !== 'function') {
            throw new Error('The second params must be function!')
        }

        let handler = this._events.get(type)

        if (!handler) {
            // 该事件还未被注册
            this._events.set(type, fn)
        } else if (handler && typeof handler === 'function') {
            // 该事件已有一个处理函数
            this._events.set(type, [handler, fn])
        } else {
            // 该事件有多个处理函数
            handler.push(fn)
        }
    }
    // 触发事件
    emit (type, ...args) {
        let handler = this._events.get(type)

        // 未注册过该事件,直接返回
        if (!handler) return

        if (Array.isArray(handler)) {
            // 事件存在多个处理函数
            for (let i = 0, len = handler.length; i < len; i++) {
                handler[i].apply(this, args)
            }
        } else {
            // 事件只有一个处理函数
            handler.apply(args)
        }
    }
    // 移除事件处理函数
    removeListener (type, fn) {
        let handler = this._events.get(type)

        if (!handler) return

        if (Array.isArray(handler)) {
            let position = handler.findIndex(v => v === fn)
            if (~position) {
                handler.splice(position, 1)
            }
            if (handler.length === 1) {
                this._events.set(type, handler[0])
            }
        } else {
            this._events.delete(type)
        }
    }
}

instanceof

instanceof 操作符的作用为:判断构造函数是否在对象的原型链上。

function instanceOf(L, R) {
    var O = R.prototype // 构造函数的原型
    // 原型链的顶级为 null,到头了退出循环
    while (L !== null) {
        if (L === O) return true
        L = L.__proto__ // L 的隐式原型
    }
    return false
}

new

new 作用于构造函数上,用于生成一个新对象。new 操作符主要完成以下几件事:

  1. 创建一个新对象
  2. 将新对象内部不可访问的 [[prototype]](即__proto__)设置为外部可访问的 prototype(链接原型)
  3. 将 this 指向创建的新对象
  4. 用新创建的对象执行构造函数
  5. 如果构造函数没有返回一个非 null 的对象,那将返回该新创建的对象

new的模拟实现

function New (func) {
    var res = {}
    // 将对象的原型指向构造函数的原型
    if (func.prototype !== null) {
        res.__proto__ = func.prototype
    }

    // 将构造函数的this指向新对象,并执行构造函数
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1))

    // 如果构造函数不返回一个非空对象,则返回创建的新对象
    if ((typeof ret === 'object' || typeof ret === 'function') && ret !== null) {
        return ret
    }

    return res
}

Object.create

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__

function ObjectCreate(proto) {
    function F () {}
    F.prototype = proto

    return new F()
}

async/await

async/await 是生成器 generator 的语法糖。

function* gen() {
  yield 1
  yield 2
  yield 3
}

const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: 3, done: false }
console.log(g.next()) // { value: undefined, done: true }

generator 是一个带星号的函数,其内部的 yield 是函数执行的中断点。每次调用 next 方法可以让函数运行到下一个中断点。

generator 与 async 的区别在于:

  1. async 不需要手动调用 next() 就能自动执行下一步
  2. async 函数返回值是 Promise 对象,而 generator 返回的是生成器对象
  3. await 能够返回 Promise 的 resolve/reject 的值

可以实现一个 co 模块来自动执行 generator:

function co(fn) {
  return function(...args) {
    // 执行结果返回一个 Promise
    return new Promise((resolve, reject) => {
      // generator 函数
      const gen = fn.apply(this, args)

      function step(val) {
        let result

        try {
          // 运行函数到中断点 yield
          result = gen.next(val)
        } catch(e) {
          reject(e)
        }

        const { value, done } = result
        // 如果完成则改变 Promise 状态
        if (done) {
          return resolve(value)
        }

        Promise.resolve(value).then(
          // 未完成则递归调用 step
          v => step(val), 
          e => gen.throw(e)
        )
      }
      step()
    })
  }
}

ajax 封装

function ajax = function (params) {
    params = params || {}
    params.data = params.data || {}
    params.type = (params.type || 'GET').toUpperCase()
    params.data = formatParams(params.data)
    
    var xhr = window.XMLHttpRequest
      ? new XMLHttpRerquest()
      : new ActiveXObject('Microsoft.XMLHTTP')
    
    // 请求响应回调
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            var status = xhr.status
            if (status >= 200 && status <= 300) {
                var type = xhr.getResponseHeader('Content-type')
                var response
                if (~type.indexOf('xml') && xhr.responseXML) {
                    // xml格式
                    response = xhr.responseXML
                } else if (~type.indexOf('application/json')) {
                    // json格式
                    response = JSON.parse(xhr.responseText)
                } else {
                    response = xhr.responseText
                }
                // 请求成功回调
                params.success && params.success(response)
            } else {
                // 请求失败回调
                params.fail && params.fail(status)
            }
        }
    }
    
    // 发送请求
    if (params.type === 'GET') {
        xhr.open('GET', params.url + '?' + params.data, true)
        xhr.send(null)
    } else {
        xhr.open('POST', params.url, true)
        xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded; charset=UTF-8')
        xhr.send(params.data)
    }
}

// 格式化参数
function formatParams (data) {
    var arr = []
    for (var key in data) {
        arr.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
    }
    return arr.join('&')
}

jsonp

jsonp 原理为,在 window 上绑定一个回调函数,然后将请求拼装成 script 的请求地址,后端在这个 script 脚本中返回回调函数的调用并传参。

例如:

  1. 在 window 上绑定一个回调函数
    window.getNum = function (num) {
        console.log(num)
    }
    
  2. 通过 js 网页插入一个这样的 script 标签 <script src="/api/getNum?callback=getNum"></script>
  3. 后端返回的 script 的内容为
    getNum(100)
    
  4. script 加载完成即调用返回的 getNum 函数,而该函数已在 window 上注册,从而前端能顺利拿到后端返回的内容。

jsonp 封装:

function jsonp (options) {
    options = options || {}
    if (!options.url || !options.callback) {
        throw new Error('invaild params')
    }

    // 添加回调函数名
    var callbackName = ('jsonp_' + Math.random()).replace('.', '')
    options.data[callback] = callbackName
    // 格式化参数
    var params = formatParams(options.data)

    // 插入空的script标签
    var oHead = document.getElementsByTagName('head')[0]
    var oS = document.createElement('script')
    oHead.appendChild(oS)

    // 回调函数-移除script标签,回调函数和计时器
    window[callbackName] = function(json) {
        oHead.removeChild(oS)
        clearTimeout(oS.timer)
        window[callbackName] = null
        options.success && options.success(json)
    }

    // 发送请求
    oS.src = options.url + '?' + params

    // 超时处理
    if (options.time) {
        oS.timer = setTimeout(function () {
            window[callbackName] = null
            oHead.removeChild(oS)
            options.fail && options.fail({ message: 'timeout' })
        }, options.time)
    }
}

function formatParams (data) {
    var arr = []
    for (var key in data) {
        arr.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
    }
    return arr.join('&')
}

防抖

防抖即一个事件短时间内被触发多次,函数只会执行一次。原理为,触发事件时初始化一个定时器,计时器计时结束时执行函数。如果定时器计时未结束时再次触发事件,则重置计数器,重新开始计时。

简版

const debounce = (fn, wait) => {
    let timer = null

    return function (...args) => {
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, args)
        }, wait)
    }
}

完整

完善简版的一些问题:

  1. 第一次触发后需要等计时结束函数才会执行,我们可以传入一个参数决定第一次是否马上执行。
  2. 如果第一次触发事件后马上执行,有些函数我们可以返回其返回值。
  3. 无法取消防抖。
// immediate = true 时第一次马上执行
const debounce = (fn, wait, immediate) => {
    let timer, result
    
    let debounced = function () {
        let context = this
        let args = arguments
        
        if (immediate) {
            // 第一次立即执行,我们可以保存其返回值
            if (!timer) result = fn.apply(context, args)
            timer = setTimeout(function () {
                timer = null
            })
        } else {
            clearTimeout(timer)
            timer = setTimeout(function () {
                fn.apply(context, args)
            }, wait)
        }
        
        return result
    }
    
    // 取消防抖
    debounced.cancel = function () {
        clearTimeout(timer)
        timer = null
    }
    
    return debounced
}

节流

节流是当一个事件被不断触发,在规定时间内,函数只执行一次。节流的原理是每次函数执行时创建一个定时器,当定时器计时未结束是再次触发事件,则不进行任何操作。只有当定时器计时结束时,再次触发事件才能执行函数。节流也可用时间戳实现。

简版

// 定时器版
const throttle = (fn, wait) => {
    let timer = null

    return function (...args) {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args)
                timer = null
            }, wait)
        }
    }
}

// 时间戳版
const throttle = (func, wait) => {
    let lastTime = 0

    return function (...args) {
        let now = +new Date()
        if (now - lastTime > wait) {
            lastTime = now
            func.apply(this, args)
        }
    }
}

完整

完善简版的一些问题:

  1. 第一次触发事件时无法立即执行函数。
  2. 第一次立即执行函数,可以得到其返回值。
  3. 无法取消节流。

两个版本的区别在于:

  • 定时器版本:第一次触发时需要延时 wait 事件后再执行,即停止触发后还会执行一次
  • 时间戳版本:第一次触发后立即执行,但是停止触发后就不会执行

结合两个版本的代码如下,leading = false 时第一次不会执行,trailing = false 时最后一次不会执行

const throttle = (fn, wait, options) => {
    let timer, context, args, result
    var previous = 0
    if (!options) options = {}
    
    let later = function () {
        previous = options.leading === false ? 0 : +new Date()
        timer = null
        fn.apply(context, args)
    }
    
    let throttled = function () {
        let now  = +new Date()
        if (!pervious && options.leading === false) pervious = now
        
        // remaining 时间后才可再次执行函数
        let remaining = wait - (now - previous)
        
        context = this
        args = arguments
        
        if (remaining <= 0 || remaining > wait) {
            // 时间戳计时结束
            // 重置定时器
            if (timer) {
                clearTimeout(timer)
                timer = null
            }
            // 重置时间戳
            previous = now
            // 执行函数
            result = fn.apply(context, args)
        } else if (!timer && options.trailing !== false) {
            // 定时器计时结束
            // later 中重置定时器与时间戳
             timer = setTimeout(later, wait)
        }
        return result
    }
    
    // 取消节流
    throttled.cancel = function () {
        clearTimeout(timer)
        timer = null
    }
    
    return throttled
}

promise

Promise 是一种异步的解决方案。Promise 存在多种规范,ES6 采用的是 Promise/A+ 规范。下面的实现也是基于这个规范。

Promise 及 then 方法的实现

// resolvePromsie 用于实现 then 的链式调用。
// then 需要返回一个新的 Promise,即 promise2。
// 如果 then 有返回值(x),则需要将 x 与  promise2 进行比较,同时决定 then 返回的 Promise 的状态。
const resolvePromise = (promise2, x, resolve, reject) => {
    // 防止循环调用,即等待自身 Promise 状态改变
    // 如 const y = new Promise(resolve => setTimeout(resolve(y)))
    if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected for promise!'))
    }

    // 防止多次调用
    let called

    // 后续的判断保证实现的 Promise 能和别的库的实现兼容
    if ((typeof x === 'object' && x !== null)
        || typeof x === 'function') {
        try {
            // 保存 then,防止 then 可能是一个 getter,多次读取可能有不同的结果
            let then = x.then

            if (typeof then === 'function') {
                // 如果 x 对象有 then 方法,则 x 对象为 thenable 对象
                // 根据鸭式辩型,可把 x 对象视为 Promise
                then.call(x, y => {
                    if (called) return
                    called = true

                    // 通过递归来链式调用 then
                    resolvePromise(promise2, y, resolve, reject)
                }, r => {
                    if (called) return
                    called = true
                    reject(r)
                })
            } else {
                // 如果 then 不是函数,则直接返回 resolve 作为结果
                resolve(x)
            }
        } catch (e) {
            if (called) return
            called = true
            reject(e)
        }
    } else {
        resolve(x)
    }
}

class Promise {
    constructor (executor) {
        this.status = 'pending' // promise 状态
        this.value = undefined
        this.reason = undefined
        this.onResolvedCallbacks = [] // resolve 回调
        this.onRejectedCallbacks = [] // reject 回调

        const resolve = (value) => {
            if (this.status === 'pending') {
                this.status = 'fulfilled'
                this.value = value
                this.onResolvedCallbacks.forEach(fn => fn())
            }
        }

        const reject = (reason) => {
            if (this.status === 'pending') {
                this.status = 'rejected'
                this.reason = reason
                this.onRejectedCallbacks.forEach(fn => fn())
            }
        }

        try {
            executor(resolve, reject)
        } catch (e) {
            reject(e)
        }
    }

    then (onFulfilled, onRejected) {
        // 处理 then 值透传的问题
        // 如 new Promise(resolve => resolve(42)).then().then().then(value => console.log(value))
        onFulfilled = typeof onFulfilled === 'function'
            ? onFulfilled
            : value => value
        onRejected = typeof onRejected === 'function'
            ? onRejected
            : err => { throw err }

        // 调用 then 返回一个新的 Promise
        let promise2 = new Promise((resolve, reject) => {
            // 根据标准异步调用,这里用 setTimeout 模拟
            // 规范并没有限制用宏任务还是微任务实现异步
            // 微任务可以尝试 mutationObserver
            if (this.status === 'fulfilled') {
                setTimeout(() => {
                    try {
                        let x = onFulfilled(this.value)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                })
            }

            if (this.status === 'rejected') {
                setTimeout(() => {
                    try {
                        let x = onRejected(this.reason)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                })
            }

            if (this.status === 'pending') {
                this.onResolvedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onFulfilled(this.value)
                            resolvePromise(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e)
                        }
                    })
                })
                this.onRejectedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onRejected(this.reason)
                            resolvePromise(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e)
                        }
                    })
                })
            }
        })

        return promise2
    }
}

Promise 测试

Promise/A+ 提供了一个测试脚本,测试编写的 Promise 是否符合规范。其中一共有 872 条测试用例,并会对不通过的测试用例提示代码不符合哪条规范。

在 Promise 中加上下面代码 ,并对外暴露 Promise 对象。

Promise.defer = Promise.deferred = function () {
    let dfd = {};
    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve;
        dfd.reject = reject;
    })
    return dfd;
}

全局安装脚本并进行测试。

npm install -g promises-aplus-tests
promises-aplus-tests promise.js

Promise API

promise/A+ 规范只给出了 promise 对象和 then 方法的实现,有了 then 方法,其他可以很简单地实现。

  • Promise.resolve
  • Promise.reject
  • Promise.prototype.catch
  • Promise.prototype.finally
  • Promise.all
  • Promise.race

Promise.resolve

Promise.resolve 返回一个 fulfilled 状态的 promise。

class Promise {
    // ...
    static resolve (value) {
        return new Promise(resolve => {
            resolve(value)
        })
    }
}

Promise.reject

Promise.reject 返回一个 rejected 状态的 promise。

class Promise {
    // ...
    static reject (reason) {
        return new Promise((resolve, reject) => {
            reject(reason)
        })
    }
}

Promise.prototype.catch

Promise.prototype.catch 当 promise 的状态变成 rejected 时会被调用,其实就是 then 的第二个参数

class Promise {
    // ...
    catch (onRejected) {
        return this.then(null, onRejected)
    }
}

Promise.prototype.finally

Promise.prototype.finally 会在 promise 改变状态时被调用,不管是 fulfilled 还是 rejected

class Promise {
    // ...
    finally (callback) {
        return this.then(value => {
            return new Promise.resolve(callback()).then(() => value)
        }, reason => {
            return new Promise.resolve(callback()).then(() => { throw reason })
        })
    }
}

Promise.all

Promise.all 并行执行传入的 promise,并返回一个 promise 作为结果,结果的 data,有一个失败则视为失败。

class Promise {
    // ...
    static all (promises) {
        if (!Array.isArray(promises)) {
            return new TypeError(`TypeError: ${values} is not iterable`)
        }
        let result = []
        let index = 0 // 计数,完成的 promise 数量

        const process = (value, i) => {
            result[i] = value
            if (++index === promises.length) {
                resolve(result)
            }
        }

        return new Promise((resolve, reject) => {
            for (let i = 0; i < promises.length; i++) {
                let promise = promises[i]
                if (promise && typeof promise.then === 'function') {
                    promise.then(value => {
                        process(value, i)
                    }, reject)
                } else {
                    process(value, i) 
                }
            }
        })
    }
}

Promise.race

Promise.race 并行执行传入的 promise,并返回最快完成的那一个

class Promise {
    // ...
    static race (promises) {
        return new Promise((resolve, reject) => {
            for (let i = 0; i < promises.length; i++) {
                let promise = promises[i]
                if (promise && typeof promise.then === 'function') {
                    promise.then(resolve, reject)
                } else {
                    resolve(promise)
                }
            }
        })
    }
}