参考:
- 因为实现不了Promise.all,一场面试凉凉了
- Promise.all和promise.race的应用场景举例
- 从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节链接
- 【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理)(第八章是手写题,注意!)
- 写一个符合 Promises/A+ 规范并可配合 ES7 async/await 使用的 Promise
[ ] 看了就会,手写Promise原理,最通俗易懂的版本!!!
1. JavaScript基础
1.1 手写call()
参考:细说 call、apply 以及 bind 的区别和用法
call
和apply
的共同点:都能够改变函数执行时的上下文,将一个对象的方法交给另一个对象来执行,并且是立即执行的。call
和apply
区别:主要体现在参数的写法上。call的写法
Function.call(obj,[param1[,param2[,…[,paramN]]]])
注意:
调用call的对象,必须是一个函数Function
- call的第一个参数,是一个对象。Function的调用者,将会指向这个对象。如果不传,则默认为全局对象window
- 第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的Function的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到Function对应的第一个参数上,之后参数都为空。 ```javascript function func (a,b,c) {}
func.call(obj, 1,2,3) // func 接收到的参数实际上是 1,2,3
func.call(obj, [1,2,3]) // func 接收到的参数实际上是 [1,2,3],undefined,undefined
<a name="TuhAJ"></a>
#### call的使用场景
对象的继承
```javascript
function superClass () {
this.a = 1
this.print = function () {
console.log(this.a);
}
}
function subClass () {
superClass.call(this)
this.print()
}
subClass()
subClass
通过call
方法,继承了superClass
的print
方法和a
变量。此外,subClass
还可以扩展自己的其他方法。
借用方法
类数组如果想使用Array原型链上的方法,可以:
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
1.2 手写apply()
1.3 手写bind()
bind()方法创建一个新的函数,在调用时设置this关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。
bind方法与call和apply比较类似,也能改变函数体内的this指向。不同的是bind方法的返回值是函数,并且需要稍后调用,才会执行。而apply和call则是立即调用。
如果bind的第一个参数是Null或者undefined,this就指向全局对象window。
1.4 函数柯里化
什么是柯里化
柯里化含义:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数,这个过程就称之为柯里化。
柯里化的结构
// 柯里化处理的函数
function add2(x) {
return function (y) {
return function (z) {
return x + y + z
}
}
}
console.log(add2(10)(20)(30))
const add3 = x => y => z => x + y + z
console.log(add3(1)(2)(3))
为什么需要有柯里化?
让函数的职责单一。
在函数式编程中,希望一个函数处理的问题尽可能单一,而不是将一大堆的处理过程交给一个函数来处理。
柯里化作用
参数复用、提前返回、延迟执行
复用参数逻辑makeAdder函数要求我们传入一个num
- 在之后使用返回的函数时,我们不需要再继续传入num。 ```javascript function makeAdder(num) { return function(count) { return num + count } }
var add5 = makeAdder(5)
console.log(add5(10)) console.log(add5(100))
<a name="pHrRR"></a>
#### 柯里化函数的实现
```javascript
function add1(x, y, z) {
return x + y + z
}
// 柯里化函数的实现hyCurrying
// 传入一个函数,返回一个新函数
function hyCurrying(fn) {
return function curried(...args) {
// 判断当前已经接收的参数的个数和参数本身需要接收的参数是否一致
// 获取fn参数个数--fn.length
// 1.当已经传入的参数大于等于需要的参数时,就执行函数
if(args.length >= fn.length) {
return fn.apply(this, args)
} else {
// 没有达到个数时,需要返回一个新的函数,继续来接收参数
return function(...args2) {
// 接收到参数后,需要递归调用curried来检查函数的个数是否达到
return curried.apply(this, [...args, ...args2])
}
}
}
}
var curryAdd = hyCurrying(add1)
console.log(curryAdd(10, 20, 30))
console.log(curryAdd(10, 20)(30))
console.log(curryAdd(10)(20)(30))
手写浅拷贝
1.2 手写深拷贝(考虑正则,日期,数组等类型)
对象相互赋值的一些关系:
- 引入的赋值:指向同一个对象,相互之间会影响
- 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互响应
-
JSON.parse实现深拷贝
这种深拷贝的方式其实对于函数、Symbol等是无法处理的
-
自定义深拷贝函数
自定义深拷贝的基本功能
- 对Symbol的key进行处理
- 其他数据类型的值进行处理:数组、函数、Symbol、Set、Map
- 对循环引用的处理
1.3 手写EventHub
1.4 手写reduce
arr.reduce(function(prev, cur, index, arr){}, initialValue)
1.5 手写JSONP
1.6 手写new
- new运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一。
- 构造函数返回一个对象,在实例中只能访问返回的对象中的属性
- 构造函数返回一个基本类型,相当于没有返回值进行处理
- 用
new Object()
的方法新建一个对象obj
- 取出第一个参数,就是我们要传入的构造函数。此外因为
shift
会修改原数组,所以arguments
会被去除第一个参数。 - 将obj的原型指向构造函数,这样obj就可以访问到构造函数原型中的属性
- 使用apply,改变构造函数this的指向到新的对象,这样obj就可以访问到构造函数中的属性
- 返回obj
默写:function myNew() {
// 1. 创建空对象
let obj = new Object()
// 获取第一个参数
let Constructor = [...arguments][0]
// 获取除掉第一个参数以外的参数
let arg = [...arguments].slice(1)
// 2. 空对象的原型指向构造函数的原型
obj.__proto__ = Constructor.prototype
// 获取外部传入构造器的返回值
let ret = Constructor.apply(obj, arg)
// 判断构造器返回值,如果是对象则return返回的对象
return ret instanceof Object ? ret : obj
}
20220109
点击查看【codepen】
1.7 debounce防抖
防抖原理:触发完事件n秒内不再触发该事件,才执行,否则不执行。如果在触发事件n秒内再次触发,则从新的事件触发时间开始计时。
防抖场景:
- 登录、发短信等按钮避免用户点击太快,以至于发送了多次请求,需要防抖。(我觉得这个节流也可以)
- 调整浏览器窗口大小时,resize次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖。
- 文本编辑器实时保存,当无任何更改操作一秒后进行保存。
防抖重在清零clearTimeout(timer)
const debounce = (func, n) => {
let timer_id
return function(){
let arg = arguments
clearTimeout(timer_id)
timer_id = setTimeout(() => {
func.apply(this, arg)
}, n)
}
}
1.8 throttle节流
节流原理:如果持续触发事件,每隔一段事件,只执行一次事件。
节流场景:
scroll
事件,每隔一秒计算一次位置信息- 浏览器播放事件,每隔一秒计算一次进度
input
框实时搜索并发送请求展示下拉列表,每隔一秒发送一次请求(也可做防抖)
节流重在开关锁timer=null
const throttle = (func, wait) => {
let timer_id
return function() {
let args = arguments
if (!timer_id) {
timer_id = setTimeout(() => {
timer_id = null
func.apply(this, args)
}, wait)
}
}
}
2. Promise
2.1 实现 Promise 同步立即返回状态的函数:输入一个promise,立即返回当前状态(pending,resolve,reject)
2.2 手写promise.all
// 判断是不是promise
const isPromise = (value) => {
if((typeof value === 'object' && value !== null) || typeof value === 'function') {
if(typeof value.then === 'function') {
return true
}
else {
return false
}
}
}
Promise.all = function (values) {
// 返回一个then,所以返回一个new Promise
return new Promise((resolve, reject) => {
let arr = [], index = 0 // 解决多个异步的并发问题,要使用计数器
function processData(key, value) {
index++
arr[key] = value
if(index == values.length){
resolve(arr)
}
}
// 因为最终同步执行,所以要依次把数组中的值取出来,所以要用一个for循环
for (let i=0; i<values.length; i++) {
let current = values[i]
if(isPromise(current)) {
current.then((data) => {
processData(i, data)
}, reject)
} else {
processData(i, current)
}
}
})
}
// 类上的方法叫静态方法 全部成功就成功,有任何一个失败就失败了
Promise.all([1,2,3,6, 7]).then(data => {
console.log(data);
})
2.3 promise实现sleep
2.4 一道 promise 的题,要求实现一个请求重发器,就是一旦失败就不断的重新请求直到请求超过最大次数限制,每次请求之间有固定的时间间隔
2.5 并发Promise,一次并发6次,如果中途reject一次,整体函数返回Promise.reject。整体成功返回成功的结果列表。
/**
*
* @param {Array<Promise>} asyncList
*/
async function promiseAll(asyncList) {
let list = [];
let res = [];
for (let i = 0; i < asyncList.length;) {
if (i + 6 < asyncList.length) {
list = asyncList.slice(i, i+ 6);
i += 6;
}
else {
list = asyncList.slice(i);
i = asyncList.length;
}
try {
let _res = await Promise.all(list);
res = res.concat(_res);
} catch (error) {
return Promise.reject(error);
}
}
return res;
}