2025前端社招最新面试题汇总- 手写题篇

275 阅读17分钟

1. 数组去重

const arr = [1,1,1,2,3,4,4,4,4,5]
// 第一种方式 
const arr1 = Array.from(new Set(arr))
// 第二种方式 
const arr2 = [...new Set(arr)]
// 第三种方式
const arr3 = arr.filter((item,index) => arr.indexOf(item) === index) 
// 第四种方式
const arr4 = arr.reduce((pre,cur) => {
  if (!pre.includes(cur)) {
    pre.push(cur)
  }
  return pre;
},[])

2. 防抖和节流

  • 防抖 debounce: 在一段时间内,事件只会最后触发一次。
  • 节流 throttle: 事件,按照一段时间的间隔来进行触发。
// 防抖
const debounce = (fn, delay) => {
    let timer = null;
    return function () {
        const ctx = this;
        const args = arguments;
        if (timer) clearTimeout(timer);
        timer = setTimeout(function () {
            fn.apply(ctx, args)
        }, delay);
    }
}

dom.addEventListener('click',debounce(fn,delay));

// 节流
const throttle = (fn,delay) => {
  let start = Date.now();
  return function() {
    const ctx = this;
    const args = arguments;
    if (Date.now() - start > delay) {
      fn.apply(ctx,args);
      start = Date.now();
    }
  }
}

dom.addEventListener('click',throttle(fn,delay));

// fn函数传递参数
const handler = throttle(fn,delay)
dom.addEventListener('click',() => {
  handler(args)
});

3. 不使用a标签,实现a标签的功能

window.open("URL")

4. 深拷贝和浅拷贝

  • 浅拷贝: 对于复杂数据类型,浅拷贝只是把引用地址赋值给了新的对象,改变这个新对象的深层的值,原对象的值也会一起改变。第一层不会改变。
  • 深拷贝: 对于复杂数据类型,拷贝后地址引用都是新的,改变拷贝后新对象的值,不会影响原对象的值
const obj = {
  a: {
    name: 'dd'
  },
  b: 33
}
// 第一种方式
const obj1 = {...obj}; // obj1.a === obj.a
// 第二种方式
const obj2 = Object.assign({},obj); // obj2.a === obj.a
// 第三种方式
function shallowClone(obj) {
    const newObj = {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
        newObj[key] = obj[key];
        }
    }
    return newObj;
}
// 第一种简单方式
const obj3 = JSON.parse(JSON.stringify(obj)); // obj3.a !== obj.a
存在一些问题:
- 无法处理循环引用,遇到会报错。
- 不支持functionundefined,拷贝后会丢失。
- 无法复制某些对象类型, 遇到比如DateRegExpMapSetErrorSymbolJavaScript 内置对象及其属性时,会被转换为基本的字符串或数组形式,导致前后的数据类型不一样,不符合预期。


// 第二种源码方式
const isObj = (obj) => {
  return typeof obj === 'object' && obj !== null;
}
const cloneDeep = (obj, hash = new WeakMap()) => {
    if (isObj(obj)) {
      // 这个hash的作用应该是处理循环引用的。比如说,如果一个对象有一个属性指向自己,或者两个对象互相引用,这时候普通的深克隆会无限递归,导致栈溢出。使用WeakMap来记录已经克隆过的对象,这样当再次遇到同一个对象时,可以直接返回之前克隆的副本,避免循环引用的问题。
      // 另外,WeakMap的键是对象,并且是弱引用的,不会阻止垃圾回收。这样当原对象不再被引用时,WeakMap中的记录也会被自动清除,这有助于内存管理。而如果使用普通的Map,可能会导致内存泄漏,因为即使原对象不再使用,Map中的引用仍然存在,无法被回收。所以这里使用WeakMap是正确的选择。
        if (hash.has(obj)) {
            return hash.get(obj)
        }

        const newObj = Array.isArray(obj) ? [] : {};
        hash.set(obj, newObj); // 记录已克隆对象

        for (let key in obj) { // 这里会克隆原型链上的内容,可以优化一下
          if(obj.hasOwnProperty(key)) {
            newObj[key] = cloneDeep(obj[key], hash);
          }
            
        }
        return newObj;
    } else {
        return obj;
    }
}

5. new 操作符

new 操作符用于创建实例,执行以下步骤:

(1)创建一个新的空对象

(2)将构造函数的作用域传给新对象,这时this指向新对象

(3)执行构造函数中的内容,为这个新对象增加属性

(4)返回新对象

const myNew = (fn,...args) => {
  // newObj.__proto__ === fn.prototype; 新创建这个对象是fn的实例,需要连接到fn的原型对象上
  // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
  const newObj = Object.create(fn.prototype)
  // 作用域赋值,将fn的this指向新的对象,添加属性到新的对象上
  const res = fn.call(newObj,...args);
  // 4. 如果构造函数返回的是一个对象,则返回这个对象;否则返回新创建的对象 
  return res && typeof res === 'object' ? res : newObj;
}
// 使用示例
function Person(name,age) {
  this.name = name;
  this.age = age;
}
Person.prototype.greet = () => {
  console.log('greet');
}

const person1 = myNew(Person,'wang',13)
console.log(person1.name)

6. instanceof

右边构造函数的原型对象是否在左边对象的原型链上

Object.getPrototypeOf(obj) 获取对象的原型

function myInstanceof(left, right) {
    const prototype = right.prototype;
    const proto = Object.getPrototypeOf(left);
    while (proto) {
        if (proto === prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}

7. 实现 Object.create

创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

// 定义一个名为 create 的函数,接受一个参数 obj,该参数是一个对象。
function create(obj) {
    // 定义一个空的构造函数 Func。
    function Func() {}
    // 将传入的对象 obj 设置为 Func 的原型。
    // 这意味着通过 Func 构造函数创建的新对象将继承 obj 的所有属性和方法。
    Func.prototype = obj;

  // 这里不需要
  
    // 修正原型链中的构造函数指向。
    // 因为当我们设置 `Func.prototype = obj` 之后,Func.prototype.constructor 默认指向的是 Object 构造函数,
    // 所以我们需要显式地将其设置回 Func,以保持正确的原型链。
    Func.prototype.constructor = Func;

    // 使用 new Func() 创建一个新的实例,并返回这个实例。
    // 这个新实例将会继承 obj 的所有属性和方法。
    return new Func();
}

8. 手写call,bind,apply

call,bind,apply都是用于改变this指向

  • call 和 apply 立即执行,call 参数为数值,apply参数为数组
  • bind 不立即执行,bind参数为数值

参考:

juejin.cn/post/726809…

 Function.prototype.myCall = function (context, ...args) {
    // this是调用myCall方法的函数
    if (typeof this !== 'function') {
        throw new TypeError('必须是函数');
    }
    // context 如果没有指定的话就会指向全局对象, 使用 globalThis 来访问全局对象。
    context = context || globalThis;
    // 唯一fn。防止冲突
    let fn = Symbol();
    // 使得context拥有 函数的能力
    context[fn] = context;
    // 执行获得结果
    const res = context[fn](...args);
    // 删除新增的属性
    delete context[fn];
    return res;
}
Function.prototype.myApply = function (context, args) {
    if (typeof this !== 'function') {
        throw new TypeError('必须是函数')
    }
    // 参数检测
    if (args && !Array.isArray(args)) {
        throw new TypeError('参数必须是数组');
    }
    // 如果未传值则给默认值
    context = context || globalThis;
    args = args || [];
    let fn = Symbol();
    context[fn] = this;

    const res = context[fn](...args);
    delete context[fn];
    return res;
}
Function.prototype.myBind = function (context, ...args) {
    if (typeof this !== 'function') {
        throw new TypeError('必须是函数')
    }
    context = context || globalThis;
    // 保存原始函数的引用,这里的this是调用bind的函数
    const _this = this;
    // 返回的函数可能会被new 使用,new 之后,fn 内部的this指向新创建的那个对象,
    // 返回的这个 fn就是那时候的构造函数
    // 通过检查 this instanceof fn,可以判断返回出去的函数是否被作为构造函数调用
    return function fn(...args2) {
        if (this instanceof fn) {
            // 作为构造函数,则应该使用New 方法生成一个对象,这个对象是上面的_this的生产的
            return new _this(...args, ...args2);
        }
        // 否则的话就是正常的apply行为 context 夺舍 上面的那个_this
        return _this.apply(context, args.concat(args2));
    }
}

9. promise

(1) promise :处理封装异步操作,防止出现回调地狱。 .then()返回一个新的Promise实例,每个promise对应一个异步任务,所以它可以链式调用,异常传透。

juejin.cn/post/704375…

手写promise

juejin.cn/post/704375…

const PENDING = 'PENDING'
const FULLFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
class MyPromise {
    constructor(executor) {
        this.state = PENDING;
        this.value = '';
        this.reason = '';
        this.onfullfilledCallbackArr = []
        this.onrejectedCallbackArr = []
        const resolve = (value) => {
            if (this.state === PENDING) {
                this.state = FULLFILLED
                this.value = value
                this.onfullfilledCallbackArr.forEach(cb => cb())
            }
        }

        const reject = (reason) => {
            if (this.state === PENDING) {
                this.state = REJECTED
                this.reason = reason
                this.onrejectedCallbackArr.forEach(cb => cb())
            }
        }

        //  这里只会捕获到同步代码的错误
        // 异步执行时已经脱离了当前executor的执行作用域(executor已经执行完了,所以捕获不到)
        // 错误会被抛到全局,导致未捕获的异常
        // 使用unhandledrejection 全局捕获或者在settimeout里手动reject
        // window.addEventListener('unhandledrejection', (event) => {
        //   console.log('全局捕获:', event.reason);
        //   event.preventDefault();
        // })
        try {
            executor(resolve, reject)
        } catch (error) {
            reject(error)
        }

    }

    then(onfullfilled, onrejected) {
        onfullfilled = typeof onfullfilled === 'function' ? onfullfilled : value => value
        onrejected = typeof onrejected === 'function' ? onrejected : reason => { throw reason }
        // then可以进行链式调用,所以需要返回promise
        return new MyPromise((resolve, reject) => {
            // 执行onfullfilled 和  onrejected 的过程中,需要对异常情况进行捕捉
             const onfulfilledCallback = () => {
                setTimeout(() => {
                    try {
                        const res = onfulfilled(this.value)
                        //  如果 res 是 Promise,需要特殊处理
                        if (res instanceof MyPromise) {
                            return res.then(resolve, reject)
                        } else {
                            resolve(res)
                        }
                    } catch (err) {
                        reject(err)
                    }
                })
            }
            const onrejectedCallback = () => {
                setTimeout(() => {
                    try {
                        const res = onrejected(this.reason)
                        if (res instanceof MyPromise) {
                            return res.then(resolve, reject)
                        } else {
                            resolve(res)
                        }
                    } catch (err) {
                        reject(err)
                    }
                })
            }

            // 判断当前状态,两种情况
            // 1:promise中是异步函数,那这里状态仍然是pedding,需要将当前处理函数保存在队列里,等状态改变了再执行
            // 2:promise中是同步函数,那这里状态已经改变了,为了不让它立即执行,需要使用settimeout模拟异步,保证then中内容在当前事件循环之后再执行
            if (this.state === FULLFILLED) {
                onfullfilledCallback()
            } else if (this.state === REJECTED) {
                onrejectedCallback()
            } else {
                // 保证then中内容在自己被调用的那一轮事件循环之后的新执行栈中执行
                // 如下例子,输出顺序应该为 1,3, 2
                // const p1 = new MyPromise((resolve, reject) => {
                //     setTimeout(() => {
                //         console.log(1)
                //         resolve(2)
                //         console.log(3)
                //     })
                // }).then(reg => {
                //     console.log('reg', reg)
                // })
                this.onfullfilledCallbackArr.push(onfullfilledCallback)
                this.onrejectedCallbackArr.push(onrejectedCallback)
            }
        })
    }
    //catch() 方法返回一个Promise,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected) 相同。
    catch(reject) {
        return this.then(null, reject)
    }

    // finally() 方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。
    // 这为在Promise是否成功完成后都需要执行的代码提供了一种方式。
    // 由于无法知道promise的最终状态,所以finally的回调函数中不接收任何参数,它仅用于无论最终结果如何都要执行的情况。
    finally(callback) {
        return this.then(callback, callback)
    }


    // Promise.resolve(value) 将给定的一个值转为Promise对象。
    // 静态方法,只能类本身调用,不能通过类实例调用
    static resolve(value) {
        // primise直接返回
        if (value instanceof MyPromise) {
            return value
        } else if (value instanceof Object && 'then' in value) {
            return new MyPromise((resolve, reject) => {
                value.then(resolve, reject)
            })
        }
        // 否则返回promise
        return new MyPromise(resolve => {
            resolve(value)
        })
    }
    // Promise.reject()方法返回一个带有拒绝原因的Promise对象。
    static reject(reason) {
        return new MyPromise((resolve, reject) => {
            reject(reason)
        })
    }
    // promise.all 等待所有成功或一个失败
    static all(arr) {
        return new MyPromise((resolve, reject) => {
            if (!Array.isArray(arr)) {
                reject('类型错误')
            }
            let res = []
            let count = 0;
            for (let i = 0; i < arr.length; i++) {
                MyPromise.resolve(arr[i]).then(curRes => {
                    res[i] = curRes;
                    count++;
                    if (count === arr.length) {
                        resolve(res)
                    }
                }, (err) => {
                    reject(err)
                })
            }
        })
    }
    // promise.race 等待第一个成功或一个失败
    static race(arr) {
        return new MyPromise((resolve, reject) => {
            if (!Array.isArray(arr)) {
                reject('类型错误')
            }
            for (let i = 0; i < arr.length; i++) {
                MyPromise.resolve(arr[i]).then(curRes => {
                    resolve(curRes)
                }, (err) => {
                    reject(err)
                })
            }
        })
    }
  // promise.allsettled 等待所有完成
   static allSettled(arr) {
        return new MyPromise((resolve, reject) => {
            if (!Array.isArray(arr)) {
                reject('类型错误')
            }
            let res = []
            let count = 0;
            for (let i = 0; i < arr.length; i++) {
                MyPromise.resolve(arr[i]).then(value => {
                    res[i] = {
                      staus: 'fulfilled',
                      value
                    };
                    count++;
                    if (count === arr.length) {
                        resolve(res)
                    }
                }, (reason) => {
                    res[i] = {
                      staus: 'rejected',
                      reason
                    };
                    count++;
                    if (count === arr.length) {
                        resolve(res)
                    }
                })
            }
        })
    }
  // promise.any 等待第一个成功,或者全部失败
  static any(arr) {
        return new MyPromise((resolve, reject) => {
            if (!Array.isArray(arr)) {
                reject('类型错误')
            }
            let errors = []
            let count = 0;
            for (let i = 0; i < arr.length; i++) {
                MyPromise.resolve(arr[i]).then(value => {
                    resolve(value) // 第一个成功
                }, (reason) => {
                    errors[i] = reason;
                    count++;
                    if (count === arr.length) {
                        reject(errors)
                    }
                })
            }
        })
    }

}

promise 打印顺序相关的题目

juejin.cn/post/684490…

10. promise 封装相关题目

  • promise封装ajax
// Promise封装Ajax请求
function ajax(method, url, data) {
    var xhr = new XMLHttpRequest();
    return new Promise(function (resolve, reject) {
        xhr.onreadystatechange = function () {
            if (xhr.readyState !== 4) return;
            if (xhr.status === 200) {
                resolve(xhr.responseText);
            } else {
                reject(xhr.statusText);
            }

        };
        xhr.open(method, url);
        xhr.send(data);
    });
}

ajax('get',url,data).then((res) => {
  const data = res;
}.catch(err => {
  console.log(err)
})
  • promise 封装图片加载,异步加载image
function loadImage = (url) => {
  return new Promise((resolve,reject) => {
    const img = new Image();
    img.onload = () => {
      resolve(img)
    }
    img.onerror = (err) => {
      reject(err)
    }
    img.src = url;
  }
}

loadImage(url).then(img => {  
    document.body.appendChild(img);  
    console.log('图片加载成功');  
})  
.catch(error => {  
    console.error('图片加载失败', error);  
});
  • promise实现交替打印红绿灯

红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?

function red() {
    console.log("red");
}
function green() {
    console.log("green");
}
function yellow() {
    console.log("yellow");
}

const task = (timer, light) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (light === 'red') {
                red()
            }
            else if (light === 'green') {
                green()
            }
            else if (light === 'yellow') {
                yellow()
            }
            resolve()
        }, timer)
    })
}
const step = () => {
    task(3000, 'red')
        .then(() => task(2000, 'green'))
        .then(() => task(1000, 'yellow'))
        .then(step)
}
step()

// 使用async await 
const arr = [1, 2, 3]
const delays = [1000, 2000, 3000]
function sleep(item, delay) {
    return new Promise((res) => {
        setTimeout(() => {
            console.log(item)
            res()
        }, delay)
    })
}
async function handler(arr, delays) {
    for (let i = 0; i < arr.length; i++) {
        await sleep(arr[i], delays[i])
    }
    handler(arr, delays)
}
handler(arr, delays)
  • promise 实现每隔一秒输出1,2,3
const arr = [1, 2, 3];
arr.reduce((pre, cur) => {
    return pre.then(() => {
        return new Promise(res => {
            setTimeout(() => {
                console.log(cur);
                res();
            }, 1000)
        })
    })
}, Promise.resolve())


// 使用 async await实现]
function print(item) {
    return new Promise((resove) => {
        setTimeout(() => {
            console.log(item)
            resove()
        }, 1000)
    })
}
async function handle() {
    for (let item of arr) {
        await print(item)
    }
}
handle()
  • 实现mergePromise函数

实现mergePromise函数,把传进去的数组按顺序先后执行,并且把返回的数据先后放到数组data中。

这道题有点类似于Promise.all(),不过.all()不需要管执行顺序,只需要并发执行就行了。但是这里需要等上一个执行完毕之后才能执行下一个。

解题思路:

  • 定义一个数组data用于保存所有异步操作的结果
  • 初始化一个const promise = Promise.resolve(),然后循环遍历数组,在promise后面添加执行ajax任务,同时要将添加的结果重新赋值到promise上。
// 生成的promise链
Promise.resolve()
  .then(ajax1).then(res => { data.push(res); return data; })
  .then(ajax2).then(res => { data.push(res); return data; })
  .then(ajax3).then(res => { data.push(res); return data; });
// 不能直接定义为promise,那样直接就执行了
const p1 = () => new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log(1)
        resolve(1)
    }, 2000)
})
const p2 = () => new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log(2)
        resolve(2)
    }, 1000)
})
const p3 = () => new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log(3)
        resolve(3)
    }, 500)
})

function mergePromise () {
  // 存放每个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;
}

// 使用reduce实现
 function mergePromise(arr) {
    let res = [];
    return arr.reduce((pre, cur) => {
        return pre.then(cur).then(c => {
            res.push(c)
            return res;
        })
    }, Promise.resolve())
}

mergePromise([p1, p2, p3]).then(data => {
  console.log("done");
  console.log(data); // data 为 [1, 2, 3]
});

// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]
限制异步操作的并发个数并尽可能快的完成全部

既然题目的要求是保证每次并发请求的数量为3,那么我们可以先请求urls中的前面三个(下标为0,1,2),并且请求的时候使用Promise.race()来同时请求,三个中有一个先完成了(例如下标为1的图片),我们就把这个当前数组中已经完成的那一项(第1项)换成还没有请求的那一项(urls中下标为3)。

直到urls已经遍历完了,然后将最后三个没有完成的请求(也就是状态没有改变的Promise)用Promise.all()来加载它们。

function limitLoad(urls, handler, limit) {
  let sequence = [].concat(urls); // 复制urls
  // 这一步是为了初始化 promises 这个"容器"
  let promises = sequence.splice(0, limit).map((url, index) => {
    return handler(url).then(() => {
      // 返回下标是为了知道数组中是哪一项最先完成
      return index;
    });
  });
  // 注意这里要将整个变量过程返回,这样得到的就是一个Promise,可以在外面链式调用
  return sequence
    .reduce((pCollect, url) => {
      return pCollect
        .then(() => {
          return Promise.race(promises); // 返回已经完成的下标
        })
        .then(fastestIndex => { // 获取到已经完成的下标
        	// 将"容器"内已经完成的那一项替换
          promises[fastestIndex] = handler(url).then(
            () => {
              return fastestIndex; // 要继续将这个下标返回,以便下一次变量
            }
          );
        })
        .catch(err => {
          console.error(err);
        });
    }, Promise.resolve()) // 初始化传入
    .then(() => { // 最后三个用.all来调用
      return Promise.all(promises);
    });
}
const urls = []
for (let i = 0; i < 10; i++) {
    urls.push(i);
}
function loadImg(url) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(url)
            resolve()
        }, Math.random() * 1000)
    })
}
limitLoad(urls, loadImg, 3)
  .then(res => {
    console.log("图片全部加载完毕");
    console.log(res);
  })
  .catch(err => {
    console.error(err);
  });

并发请求控制

  class Scheduler {
    constructor(limit = 6) {
        this.limit = limit;
        this.tasks = [];     // 任务队列:保存待执行任务
        this.pool = new Set(); // 执行池:当前正在执行的任务
        this.taskIndex = 0;  // 任务唯一索引标识
    }

    // 添加任务到队列
    addTask(task, ...args) {
        const index = this.taskIndex++;
        this.tasks.push({ task, args, index });
    }

    // 启动任务调度
    async start() {
        const results = new Array(this.taskIndex); // 按索引存储结果
        this.taskIndex = 0; // 重置索引(确保多次调用start()的正确性)

        while (this.tasks.length > 0) {
            const { task, args, index } = this.tasks.shift();

            // 包装任务,确保错误被捕获并按索引存储结果
            const wrapped = Promise.resolve()
                .then(() => task(...args))
                .then(value => {
                    results[index] = { status: 'fulfilled', value };
                })
                .catch(reason => {
                    results[index] = { status: 'rejected', reason };
                })
                .finally(() => {
                    this.pool.delete(wrapped);
                });

            this.pool.add(wrapped);

            // 并发控制:当执行池满时,等待最快完成的任务
            // await 的时候,会阻塞循环,等待当前执行池中的一个任务完成
            if (this.pool.size >= this.limit) {
                await Promise.race(this.pool);
            }
        }

        // 等待所有剩余任务完成
        await Promise.allSettled(this.pool);
        return results;
    }
}
// 使用示例
const scheduler = new Scheduler()

// 模拟异步任务
const request = (id) => new Promise(resolve =>
    setTimeout(() => {
        console.log(`Task ${id} completed`)
        resolve(id)
    }, id * 2000)
)

// 添加100个任务
for (let i = 0; i < 20; i++) {
    scheduler.addTask(request, i)
}

// 启动调度
scheduler.start().then((res) => console.log('All tasks completed', res))

核心机制对比

特性Scheduler类方案limitLoad函数方案
任务队列结构先进先出队列(FIFO)固定长度数组(动态替换)
初始化加载动态逐个添加预先加载前N个任务
并发维持方式Promise.race + 自动清理Promise.race + 索引替换
任务执行顺序严格按添加顺序执行完成顺序可能影响后续任务分配
内存管理Set自动清理数组索引替换
结果结果按照添加顺序输出

终止promise

Promise 一旦实例化之后,状态就只能由 Pending 转变为 Rejected 或者 Fulfilled, 本身是不可以取消已经实例化之后的 Promise 了。

但是我们可以通过一些其他的手段来实现终止 Promise 的继续执行来模拟 Promise 取消的效果。

标志位取消

在 Promise 内部检查外部变量状态,若需终止则主动调用 reject

 let isCancel = false
  function newPromise() {
      return new Promise((resolve, reject) => {
          setTimeout(() => {
              if (isCancel) {
                  reject('cancel')
              } else {
                  resolve('resolve')
              }
          }, 2000)
      })
  }
  const mypromise = newPromise();
  isCancel = true;
  mypromise.then(res => console.log(res)).catch(err => console.log(err))

promise.race

Promise.race 方法接收多个 Promise ,一旦这些 Promise中任意一个 Promise 存在 resove 或者 reject 其他的 Promise 就不会执行了,基于这个特点,我们可以构造代码实现终止 Promise 的执行

let abort = null;
const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log(1)
        resolve('resolve')
    }, 2000)
})
const p2 = new Promise((resolve, reject) => {
    abort = () => reject('err')
})
Promise.race([p1, p2]).then(res => console.log(res)).catch(err => console.log(err))
abort();

abortcontroller

const controller = new AbortController();
const { signal } = controller;

fetch("https://api.example.com", { signal })
  .then(response => response.json())
  .catch(err => {
    if (err.name === "AbortError") console.log("Request aborted");
  });

controller.abort(); // 终止请求

手动触发reject

function cancellablePromise() {
  let abort
  const p1 = new Promise((resolve, reject) => {
      abort = reject;
      setTimeout(resolve, 3000)
  })
  return {
      p1,
      abort
  }
}
const { p1, abort } = cancellablePromise();
p1.then(res => console.log(res))
abort('cancel')

第三方库

11. 函数柯里化

函数柯里化(Currying)可以将一个接受多个参数的函数转换成一系列使用一个参数的函数。这个转换后的函数链中的每一个函数都返回下一个函数,直到最后一个函数返回最终的结果。

const curry = (fn, ...args) => 
    // 函数的参数个数可以直接通过函数数的.length属性来访问
    args.length >= fn.length // 这个判断很关键!!!
    // 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
    ? fn(...args)
    /**
     * 传入的参数小于原始函数fn的参数个数时
     * 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数,递归
    */
    : (..._args) => curry(fn, ...args, ..._args);

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

function add1(x, y, z) {
    return x + y + z;
}
const add = curry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));

// 实现add(1)(2)(3)
function add(num) {
    sum = num || 0;
    if (!num) {
        return sum
    }
    function innerAdd(num2) {
        if (num2) {
            sum += num2;
            return innerAdd;
        } else {
            return sum;
        }

    }
    return innerAdd;
}
console.log(add(1)(2)(3)())

12. 数组的flat

// 第一种方法:递归
 const arr = [1, 2, [3, [4, 5], 6], [6, 7]]
const flat = (arr) => {
    let newArr = [];
    arr.forEach(item => {
        if (Array.isArray(item)) {
            newArr = newArr.concat(flat(item));
        } else {
            newArr.push(item);
        }
    });
    console.log('new', newArr)
    return newArr;
}
// 第二种方法: reduce
 const flat = (arr) => {
    return arr.reduce((pre, cur) => {
        if (Array.isArray(cur)) {
            pre = pre.concat(flat(cur))
        } else {
            pre.push(cur);
        }
        return pre;
    }, [])
}

// 第三种方式,使用扩展运算符
const flat = (arr) => {
  while(arr.some(item => Array.isArray(item))) {
    arr = [].concat(...arr);
  }
  return arr;
}

// 第四种方法,使用es10的flat方法,可能会有兼容问题。参数表示要展开多少层
console.log(arr.flat(Infinity)); 

13. 对象的flat

function flattenObject(obj) {
  const result = {};

  function traverse(currentObj, path = '') {
    for (const key in currentObj) {
      if (currentObj.hasOwnProperty(key)) {
        const currentPath = path ? `${path}.${key}` : key;
        const value = currentObj[key];
        // 是对象就继续处理
        if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
          traverse(value, currentPath);
        } else {
          result[currentPath] = value;
        }
      }
    }
  }

  traverse(obj);
  return result;
}
 const obj = {
    a: {
        b: {
            c: '1'
        },
        b2: {
            c: 3
        }
    },
    d: 0
}

14. sleep 函数

sleep函数使js开始休眠一段时间

第一种方法:promise
async function handleSleep(delay) {
  console.log('start')
  await sleep(delay);
  console.log('end')
  
  function sleep(dealy) {
    return new Promise((res,rej) => {
      setTimeout(res,delay);
    })
  }
}

// 第二种方法,完全堵塞进程来达到sleep
function handleSleep(delay) {
    console.log('start');
    sleep(delay);
    console.log('end');
    function sleep(delay) {
        const start = Date.now();
        while (Date.now() - start < delay) {
            continue;
        }
    }
}
handleSleep(1000);

15. 类数组转换为数组

  • 类数组:具有数字索引和length属性的对象
let arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
let arrayLike2 = document.getElementsByTagName('div');

// (1)通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
// (2)通过 call 调用数组的 splice 方法来实现转换,不适用于arrayLike2类型的,dom不能删除属性
Array.prototype.splice.call(arrayLike,0);
// (2)通过 call 调用数组的 concat 方法来实现转换, 不适用于arrayLike类型的,是对象,不能用于apply参数
Array.prototype.concat.apply([], arrayLike);
// (4)通过 Array.from 方法来实现转换
Array.from(arrayLike);
// (5) 使用扩展运算符,不适用于arrayLike类型的,是对象,需要用{...arrayLike}
[...arrayLike]

(6)使用for infor of迭代然后逐个加入新数组中

16. 将对象数组转换为树形结构

递归

const items = [  
  { id: 1, name: 'Item 1', parentId: null },  
  { id: 2, name: 'Item 1.1', parentId: 1 },  
  { id: 3, name: 'Item 1.2', parentId: 1 },  
  { id: 4, name: 'Item 2', parentId: null },  
  { id: 5, name: 'Item 2.1', parentId: 4 },  
  // ... 更多的项目  
];
const arrToTree = (arr, parentId = null) => {
  // 判断是否是顶层,是的话直接返回否则过滤出对应的子元素
    const filterArr = arr.filter(item => {
        return item.parentId === parentId;
    })
  // 递归返回对应的子
    filterArr.forEach(item => {
        item.childNode = arrToTree(arr, item.id);
    })
    return filterArr;
}
console.log(arrToTree(items))

迭代

function arrayToTreeIterative(data) {
  const map = {};
  const tree = [];

  // 创建哈希映射并保留引用
  data.forEach(node => {
    map[node.id] = { ...node, children: [] };
  });

  // 构建树结构
  data.forEach(node => {
    if (node.parentId) {
      // 把当前元素插入其父元素的children中,使用map获取,这样里面才有children属性
      map[node.parentId].children.push(map[node.id]);
    } else {
      tree.push(map[node.id]);
    }
  });

  return tree;
}

17. 发布-订阅模式

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态生改变时,所有依赖于它的对象都将得到状态改变的通知。

  • 订阅者(Subscriber)把自己想订阅的事件 注册(Subscribe)到调度中心(Event Channel);
  • 发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由 调度中心 统一调度(Fire Event)订阅者注册到调度中心的处理代码。

发布/订阅模式的优点是对象之间解耦,异步编程中,可以更松耦合的代码编写;缺点是创建订阅者本身要消耗一定的时间和内存,虽然可以弱化对象之间的联系,多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护

juejin.cn/post/705263…

class Observer {
    constructor() {
        this.message = {};
    }
    // 订阅者注册订阅事件到调度中心
    // 向消息队列中添加内容
    $on(type, callback) {
        if (!this.message[type]) {
            this.message[type] = [];
        }
        this.message[type].push(callback);
    }
    // 订阅者取消订阅事件
    // 删除消息队列中的内容
    $off(type, callback) {
        if (!this.message[type]) {
            return;
        }
        if (!callback) {
            this.message[type] = undefined;
        }
        this.message[type] = this.message.filter(item => item !== callback);
    }
    // 发布者发布事件到调度中心,调度中心处理代码
    // 触发消息队列中的内容
    $emit(type, data) {
        if (!this.message[type]) {
            return;
        }
        this.message[type].forEach(item => {
            item(data);
        })
    }
  // 使用once注册,然后emit执行一次之后就off掉
    $once(event, fn) {
        const _fn = (...args) => {
            fn.apply(this, args)
            this.$off(event, _fn);
        }
        this.$on(event, _fn)
    }
}

const person1 = new Observer();
person1.$on('onclick', () => { console.log('ddd') });
person1.$emir('onclick');

person1.$off('onclick', () => { console.log('ddd') });

18. 斐波那契数

斐波那契数列是一个经典的数列,其中每个数都是前两个数的和。例如,从 0 和 1 开始,后续的数依次为 0、1、1、2、3、5、8、13、21 等等

// 递归方法
function fi(n) {
  if (n <= 1){
    return n;
  }
  return fi(n - 1) + fi(n - 2);
}
console.log(fi(10))

// 迭代方法
function fi2(n) {
    let a = 0, b = 1, temp;
    if (n === 0) return a;
    if (n === 1) return b;
    for (let i = 2; i <= n; i++) {
        temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

// 动态规划方法
function fi3(n) {
    let dp = [0, 1];
    for (let i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

19. 使用setTimeout 模拟 setInterval

function myInterval(callback, delay) {
    let timer = null;
    function execute() {
        callback();
        timer = setTimeout(execute, delay);
    }
    timer = setTimeout(execute, delay);
    return () => clearTimeout(timer);
}
const id = myInterval(() => console.log('a'), 1000);
// 停止定时器
setTimeout(() => {
    id();
}, 6000)

20. 大数相加

js中数字的最大值不能超过2^53,但是某些情况下可能不得不去进行一些操作导致数字大于这个值,请问在这种情况下该怎么将他们相加(不允许使用bigInt类型)

  • padStart()方法是一种能够方便地填充字符串的方法,可以在字符串左侧添加指定数量的字符以达到规定的长度
给的数必须是字符串类型,否则直接报错
let a = '875223123123123123123123123123';
let b = '234234235879';
function addBig(a,b) {
  // 首先进行补位,给短的数前面加上0
  const maxLength = Math.max(num1.length, num2.length);
  a = a.padStart(maxLength,'0') // 扩充字符串达到指定长度
  b = b.padStart(maxLength,'0') // 扩充字符串达到指定长度

   let res = [];
  let cnt = 0;
  // 从后往前一个个加,进位
  for (let j = maxLength - 1; j >= 0; j--) {
      const ans = cnt + Number(a[j]) + Number(b[j]) // a[j]是字符串,不能直接相加
      res[j] = ans % 10;
      cnt = ans >= 10 ? 1 : 0;
  }
  // 如果最后还有进位。则加到前面去
  if (cnt === 1) {
      res.unshift(1);
  }
  console.log(res.join(""));
}

21. 手写reduce/push/map/filter

Array.prototype.myReduce = function(callback, initialValue) {
  // 1. 检查回调函数是否为函数类型
  if (typeof callback !== 'function') {
    throw new TypeError('Callback must be a function');
  }

  const array = this; // 原始数组
  const length = array.length;

  // 2. 处理空数组且无初始值的情况
  if (length === 0 && initialValue === undefined) {
    throw new TypeError('Reduce of empty array with no initial value');
  }

  let accumulator;
  let startIndex;

  // 3. 初始化累加器
  if (initialValue !== undefined) {
    accumulator = initialValue;
    startIndex = 0; // 从第0个元素开始遍历
  } else {
    // 查找第一个存在的元素作为初始值
    let found = false;
    for (let i = 0; i < length; i++) {
      if (i in array) {
        accumulator = array[i];
        startIndex = i + 1;
        found = true;
        break;
      }
    }
    if (!found) {
      throw new TypeError('Reduce of empty array with no initial value');
    }
  }

  // 4. 遍历处理每个元素
  for (let i = startIndex; i < length; i++) {
    // 跳过稀疏数组中的空元素
    if (!(i in array)) continue;

    const current = array[i];
    // 5. 执行回调并更新累加器
    accumulator = callback(accumulator, current, i, array);
  }

  return accumulator;
};

 Array.prototype.mypush = function (...args) {
    const arr = this;
    for (let i = 0; i < args.length; i++) {
        arr[arr.length] = args[i];
    }
    return arr.length;
}

Array.prototype.myfilter = function (fn) {
    const arr = this;
    const res = []
    for (let i = 0; i < arr.length; i++) {
        if (fn(arr[i], i, arr)) {
            res.push(arr[i])
        }
    }
    return res;
}

Array.prototype.myMap = function (fn) {
    const arr = this;
    const res = []
    for (let i = 0; i < arr.length; i++) {
        res.push(fn(arr[i], i, arr))
    }
    return res;
}

22. 数字千分位分割

function format(num) {
    num = String(num)
    let dim = num.indexOf(".") > -1 ? num.split(".")[1] : '';
    let res = [];
    num = dim ? num.split(".")[0].split("") : num.split("")
    while (num.length >= 3) {
        res.push(num.splice(-3))
    }
    if (num.length) {
        res.push(num);
    }
    res.reverse()
    res = res.map(item => item.join(""))
    return dim ? res.join(",") + '.' + dim : res.join(",")
}
console.log(format(12345678.334)); // 包含处理小数

23. isEqual

function isEqual(obj1, obj2) {
   //不是对象,直接返回比较结果
   if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
      return obj1 === obj2
   }
   //都是对象,且地址相同,返回true
    if (obj2 === obj1) return true
  
    //是对象或数组
    let keys1 = Object.keys(obj1)
    let keys2 = Object.keys(obj2)
  
    //比较keys的个数,若不同,肯定不相等
    if (keys1.length !== keys2.length) return false
      for (let k of keys1) {

        //递归比较键值对
        let res = isEqual(obj1[k], obj2[k])
        if (!res) return false
      }
      return true
    }

    const obj1 = {
      a: 100,
      b: {
        x: 100,
        y: 200,
      },
    }
    const obj2 = {
      a: 200,
      b: {
        x: 100,
        y: 200,
      },
}
console.log(isEqual(obj1, obj2)) //false