类的继承
原型和原型链中说到,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.prototype,constructor 也指向了 Parent,这明显不是我们想要的效果,因此手动修复 constructor 的指向。
组合继承解决了引用问题和方法无法共用的问题。但需要调用两次父类的构造函数,存在多一份父类的属性。
最终的方法是通过 Object.create() 创建一个父类原型的对象,两个对象互不关联,但拥有共同的原型。
function Parent () {}
function Child () {
Parent.call(this)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
上面的方法称为寄生组合继承,是最好的继承方法。当然,ES6 已经有了类的语法糖,可以很方便地实现继承。
浅拷贝
赋值
Javascript 中的数据类型可分为两类,基础类型和引用类型。基础类型如 Number,String,Boolean 等。基础类型的数据在内存中是保存在栈中的。引用类型为 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()
深拷贝
深拷贝是对整个对象的完整复制,两个对象互不影响
乞丐版实现
乞丐版深拷贝可以应付一般情况。但是一些特殊值在拷贝时会丢失,如 RexExp,Date 等,也无法处理循环引用的问题。
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
函数的call和apply方法可以显示绑定函数的 this。这是利用 this 的隐式绑定实现的,即函数作为对象属性调用时,函数内部的this指向该对象。
call 和 apply 的区别在于传参上的不同。call 需要传入函数的参数列表,而apply则是将函数参数作为数组传入。
主要的实现步骤为:
- 将函数设置为对象的属性(如果没有上下文则函数指向
window) - 执行函数,并将剩余参数传给函数(es6以下用
arguments获取剩余函数,用eval执行函数) - 删除属性
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 函数的有以下的特点:
- 返回一个函数,将其 this 绑定到第一个参数
- 传递预设参数
- 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 操作符主要完成以下几件事:
- 创建一个新对象
- 将新对象内部不可访问的
[[prototype]](即__proto__)设置为外部可访问的prototype(链接原型) - 将 this 指向创建的新对象
- 用新创建的对象执行构造函数
- 如果构造函数没有返回一个非 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 的区别在于:
- async 不需要手动调用 next() 就能自动执行下一步
- async 函数返回值是 Promise 对象,而 generator 返回的是生成器对象
- 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 脚本中返回回调函数的调用并传参。
例如:
- 在 window 上绑定一个回调函数
window.getNum = function (num) { console.log(num) } - 通过 js 网页插入一个这样的 script 标签
<script src="/api/getNum?callback=getNum"></script> - 后端返回的 script 的内容为
getNum(100) - 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)
}
}
完整
完善简版的一些问题:
- 第一次触发后需要等计时结束函数才会执行,我们可以传入一个参数决定第一次是否马上执行。
- 如果第一次触发事件后马上执行,有些函数我们可以返回其返回值。
- 无法取消防抖。
// 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)
}
}
}
完整
完善简版的一些问题:
- 第一次触发事件时无法立即执行函数。
- 第一次立即执行函数,可以得到其返回值。
- 无法取消节流。
两个版本的区别在于:
- 定时器版本:第一次触发时需要延时 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)
}
}
})
}
}