深拷贝

function deepClone(obj, cache = new WeakMap()) {
    // 检查是否已经克隆过该对象,避免循环引用
    if (cache.has(obj)) return cache.get(obj);
 
    if (typeof obj !== "object" || obj === null) {
        return obj;
    }
 
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
 
    let res = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
 
    cache.set(obj, res);
 
    Reflect.ownKeys(obj).forEach((key) => {
        res[key] = deepClone(obj[key], cache);
    });
 
    return res;
}

计算嵌套对象里各个 value 值的总和

function sum(obj) {
  let res = 0
 
  function f(val) {
    if (typeof val === "number") {
      res += val
      return
    }
 
    Reflect.ownKeys(val).forEach((key) => {
      f(val[key])
    })
  }
  f(obj)
 
  return res
}

数组去重

// 1.indexOf 或 includes
function unique(arr) {
  let res = []
  for (const ele of arr) {
    if (res.indexOf(ele) === -1) res.push(ele)
    // if (!res.includes(ele)) res.push(ele)
  }
  return res
}
 
// 2.Set
function unique(arr) {
  return [...new Set(arr)]
}
 
// 3.filter
function unique(arr) {
  return arr.filter((ele, index, arr) => arr.indexOf(ele) === index)
}

数组扁平化

// 1.递归
function flat(arr, depth = 1) {
  let res = []
  for (const ele of arr) {
    if (Array.isArray(ele) && depth > 0) {
      res.push(...flat(ele, depth - 1))
    } else {
      res.push(ele)
    }
  }
  return res
}
 
// 2.reduce
function flat(arr, depth = 1) {
  return depth > 0
    ? arr.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flat(cur, depth - 1) : cur), [])
    : arr
}

实现 call/apply

const obj = {a: 1}
const func = function (...args) {
  console.log(this, args)
}
 
// 本质就是要构造一个这样的对象:
// obj = {
//   a: 1,
//   Symbol(): func
// }
 
Function.prototype.call = function (context, ...args) {
  context = Object(context) // 原始值变成包装类型
  
  const fnSymbol = Symbol() // 避免属性冲突
  context[fnSymbol] = this // this 此时是调用 call 的原函数
  
  const res = context[fnSymbol](...args) // 执行对象下的原函数,此时原函数的 this 执行的就是这个对象
 
  delete context[fnSymbol]
 
  return res
}
 
// apply 同上,只是参数有点区别
Function.prototype.apply = function (context, arr = []) {
  context = Object(context) // 原始值变成包装类型
 
  const fnSymbol = Symbol() // 避免属性冲突
  context[fnSymbol] = this // this 此时是调用 call 的原函数
 
  const res = context[fnSymbol](...arr) // 执行对象下的原函数,此时原函数的 this 执行的就是这个对象
 
  delete context[fnSymbol]
 
  return res
}

实现 bind

Function.prototype.bind = function (context, ...args) {
  const fn = this // this 就是调用 bind 的源函数
  
  // const args = [...arguments].slice(1) // 获取调用 bind 时传入的参数
  
  return function(...moreArgs) {
    return fn.apply(context, args.concat(...moreArgs))
  }
}

实现 new 操作符

function myNew (Func, ...args) {
  let obj = {}
  obj.__proto__ = Func.prototype
  let res = Func.apply(obj, args)
  return res instanceof Object ? res : obj
}

实现 instanceof 操作符

function myInstanceof (left, right) {
  if (typeof left !== 'object' || left === null) return false
  if (typeof right !== 'function') return false
  
  while (left) {
    const leftProto = Object.getPrototypeOf(left)
    if (leftProto === right.prototype) {
      return true
    }
    left = leftProto
  }
  
  return false
}

Promise.all

全部成功则成功,有一个失败则失败

function promiseAll(promises) {
  // 判断输入是否为 Iterable
  const isIterable = typeof promises[Symbol.iterator] === "function"
 
  // 不是的话就回传错误讯息
  if (!isIterable) {
    return new TypeError("Arguments must be iterable")
  }
 
  // 把 Iterable 转成 Array
  promises = Array.from(promises)
 
  const outputs = []; // 先定义一个最终要 resolve 的 outputs,之后每个 promise 被 fulfilled 时,就放到 outputs 里面
  let fulfilledCount = 0; // 记录有多少个 promise 已经 fulfilled
 
  return new Promise((resolve, reject) => {
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then((value) => {
          outputs[index] = value
          fulfilledCount += 1
          if (fulfilledCount === promises.length) {
            resolve(outputs)
          }
        })
        .catch((err) => {
          reject(err)
        })
    })
  })
}

Promise.any

取第一个成功的

function promiseAny(promises) {
  // 省略参数校验...
  
  const errors = []        // 存储所有 Promise 的拒绝原因
  let rejectedCount = 0    // 记录已拒绝的 Promise 数量
  
  return new Promise((resolve, reject) => {
    // 迭代 promises
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then((value) => {
          // 只要有 fulfill 的,就马上 resolve
          resolve(val)
        })
        .catch((err) => {
          errors[index] = err // 记录 err 信息
          rejectedCount++
          if (rejectedCount === promises.length) reject(new AggregateError(errors, 'All promises were rejected')) // 全部 rejected 则返回错误
        })
    })
  })
}

Promise.race

取第一个完成的

function promiseRace(promises) {
  // 省略参数校验...
 
  return new Promise((resolve, reject) => {
    // 迭代 promises
    for (const p of promises) {
      p.then((val) => {
        // 只要有 fulfill 的,就马上 resolve
        resolve(val)
      }).catch((e) => {
        // 或是只要有 reject 的,就马上 reject
        reject(e)
      })
    }
  })
}

Promise.allSettled

全部完成后返回对应的结果

function promiseAllSettled(promises) {
  // 省略参数校验...
 
  const outputs = []      // 记录结果
  let settledCount = 0    // 记录已完成的 Promise 数量
  
  return new Promise((resolve, reject) => {
    // 迭代过 promises
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then((value) => {
          // fulfill 的放入结果中
          outputs[index] = { status: 'fulfilled', value: value }
          settledCount++
          if (settledCount === promises.length) resolve(outputs) // 全部完成了,返回结果
        })
        .catch((err) => {
          // rejected 的放入结果中
          outputs[index] = { status: 'rejected', reason: err }
          settledCount++
          if (settledCount === promises.length) resolve(outputs) // 全部完成了,返回结果
        })
    })
  })
}

Promise 限制并发数量

请实现 PLimit 类,该类可以控制异步函数的并发数量

const limit = new PLimit(2); // 设置并发数为 2
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
 
const task = async () => {
  console.log('Start task');
  await delay(1000); // 模拟一个耗时的任务
  console.log('End task');
};
 
Promise.all([
  limit.add(task),
  limit.add(task),
  limit.add(task),
  limit.add(task),
  limit.add(task),
  limit.add(task),
  limit.add(task),
  limit.add(task),
  limit.add(task),
  limit.add(task),
]).then(() => console.log("done"));
 
// 请补充下面的代码片段
class PLimit {
  constructor(limit) {
    this.limit = limit;
    // 请补充
  }
 
  add(fn) {
    // 请补充
  }
}

答案如下:

class PLimit {
  constructor(limit) {
    this.limit = limit;
    this.queue = []; // 任务队列
    this.runningCount = 0; // 当前运行任务数
  }
 
  add(fn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ fn, resolve, reject }); // 每个任务要跟它自身的 resolve 和 reject 相对应
      this.run();
    });
  }
 
  run() {
    // 当前运行任务数量超过限制 || 任务队列为空
    if (this.runningCount >= this.limit || !this.queue.length) return;
 
    // 开始一个新任务
    this.runningCount += 1;
    let { fn, resolve, reject } = this.queue.shift();
 
    // 执行它
    fn()
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      })
      .finally(() => {
        // 任务结束,进行下一个任务
        this.runningCount -= 1; // 必须先减 1 再 run
        this.run();
        // 如果要每次执行完 limit 个再执行下 limit 个,则把上面的 this.run() 替换成:
        // if (this.runningCount === 0) {
        //   for (let i = 0; i < this.limit; i++) {
        //     this.run();
        //   }
        // }
      });
  }
}

实现一个 LazyMan,可以按照以下方式调用:

LazyMan("Hank")
输出:
Hi! This is Hank!
 
LazyMan("Hank").sleep(10).eat("dinner")
输出:
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner
 
LazyMan("Hank").eat("dinner").eat("supper")
输出:
Hi! This is Hank!
Eat dinner
Eat supper
 
LazyMan("Hank").sleepFirst(5).eat("supper")
输出:
//等待5秒
Wake up after 5
Hi! This is Hank!
Eat supper
function LazyMan(name) {
    const queue = [] // 任务队列
 
    // 执行函数,依次取任务队列中的任务进行执行
    function run() {
        if (!queue.length) return
        const task = queue.shift()
        task()
    }
 
    // 保证在执行完同步任务后再执行 run
    setTimeout(() => {
        run()
    })
 
    // 函数调用时的初始化任务
    const sayHi = () => {
        console.log(`Hi! This is ${name}!`)
        run()
    }
    queue.push(sayHi)
 
    // 要实现链式调用,需要定义一个操作映射对象,并返回这个 obj
    const obj = {
        sleep: (time) => {
            queue.push(() => {
                setTimeout(() => {
                    console.log(`Wake up after ${time}`)
                    run()
                }, time * 1000)
            })
 
            return obj
        },
        eat: (something) => {
            queue.push(() => {
                console.log(`Eat ${something}`)
                run()
            })
 
            return obj
        },
        sleepFirst: (time) => {
            queue.unshift(() => {
                setTimeout(() => {
                    console.log(`Wake up after ${time}`)
                    run()
                }, time * 1000)
            })
 
            return obj
        },
    }
 
    return obj
}

实现一个 LRU 缓存

class LRUCache {
  constructor(capacity) {
    this.cache = new Map() // 使用 Map 保证插入顺序
    this.capacity = capacity // 初始化缓存的容量
  }
 
  get(key) {
    if (!this.cache.has(key)) {
      return -1
    }
    const value = this.cache.get(key)
    this.cache.delete(key) // 访问时先删除再重新插入,表示最近使用
    this.cache.set(key, value)
    return value
  }
  
  put(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key)
    } else if (this.cache.size >= this.capacity) {
      this.cache.delete(this.cache.keys().next().value) // 容量满了,优先删除最久没有访问过的(第一个)
    }
    this.cache.set(key, value)
  }
}

实现一个 Vue 自定义指令:v-debounce

Vue2:

// 组件上使用类似:<input v-debounce:input="handleFn" delay="1000" />,绑定 input 事件,调用 handleFn 函数
 
Vue.directive('debounce', {
  inserted(el, binding) {
    const eventName = binding.arg || 'input' // 获取要设置防抖的事件,默认绑定 input 事件
    const fn = binding.value // 获取绑定的要执行的函数
    const wait = el.getAttribute('delay') || 500 // 可以读取 dom 上的自定义特性 delay 的值
    
    let timer
    el._debounceFn = function(...args) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        fn.apply(this, args)
      }, wait)
    }
    
    el.addEventListener(eventName, el._debounceFn)
  },
  unbind() {
    const eventName = binding.arg || 'input'
    el.removeEventListener(eventName, el._debounceFn)
    el._debounceFn = null
  }
})

Vue3:

  • Vue.directive 换成 app.directive
  • inserted 换成 mounted
  • unbind 换成 unmounted

版本号排序算法

// 按照版本号由小到大排序,如:
// 输入:['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']
// 输出:['0.1.1', '0.302.1', '2.3.3', '4.2', '4.3.4.5', '4.3.5']
 
function sortVersions(versions) {
    // 复制原数组以避免修改输入
    return [...versions].sort((a, b) => {
        // 将版本号拆分为数字数组
        const aParts = a.split('.').map(Number);
        const bParts = b.split('.').map(Number);
        
        // 取两个版本号中较长的长度进行比较
        const maxLength = Math.max(aParts.length, bParts.length);
        
        // 逐个比较版本号的每个部分
        for (let i = 0; i < maxLength; i++) {
            // 对于较短的版本号,缺失的部分视为0
            const aVal = aParts[i] || 0;
            const bVal = bParts[i] || 0;
            
            // 如果找到差异,返回比较结果
            if (aVal !== bVal) {
                return aVal - bVal;
            }
        }
        
        // 所有部分都相同
        return 0;
    });
}

扁平数据结构 转 树形结构

// const flatData = [
//   { id: 1, parentId: null, name: 'Item 1' },
//   { id: 2, parentId: 1, name: 'Item 2' },
//   { id: 3, parentId: 1, name: 'Item 3' },
//   { id: 4, parentId: 2, name: 'Item 4' },
//   { id: 5, parentId: null, name: 'Item 5' },
//   { id: 6, parentId: 5, name: 'Item 6' },
// ]
 
 
function flatToTree(flatData) {
  // 创建一个映射表,用于快速查找每个节点
  const map = {};
  flatData.forEach((item) => {
    map[item.id] = { ...item, children: [] };
  });
 
  // 创建一个数组,用于存储根节点
  const tree = [];
 
  flatData.forEach((item) => {
    if (item.parentId === null) {
      // 如果是根节点,直接加入树数组
      tree.push(map[item.id]);
    } else {
      // 如果不是根节点,找到其父节点并加入父节点的 children 数组
      const parent = map[item.parentId];
      if (parent) {
        parent.children.push(map[item.id]);
      }
    }
  });
 
  return tree;
}

树形结构 转 扁平数据结构

// const treeData = [
//   {
//     id: 1,
//     name: 'node1',
//   },
//   {
//     id: 2,
//     name: 'node2',
//     children: [
//       {id: 3, name: 'node3'},
//       {id: 4, name: 'node4'},
//     ]
//   },
//   {
//     id: 5,
//     name: 'node5',
//   }
// ]
 
function treeToFlat(treeData) {
  let res = []
  
  function dfs(obj) {
    res.push({id: obj.id, name: obj.name})
    
    if (obj.children) {
      for (let i of obj.children) {
        dfs(i)
      }
    }
  }
  
  for (let i of treeData) {
    dfs(i)
  }
  
  return res
}
 
// [
//   {id: 1, name: 'node1'},
//   {id: 2, name: 'node2'},
//   {id: 3, name: 'node3'},
//   {id: 4, name: 'node4'},
//   {id: 5, name: 'node5'},
// ]

实现一个 EventBus(发布/订阅模式)

class EventBus {
  constructor() {
    this.value = new Map(); // 存放事件名称和对应的回调列表
  }
 
  // 订阅事件
  on(eventName, cb) {
    if (!this.value.has(eventName)) {
      this.value.set(eventName, []);
    }
    this.value.get(eventName).push(cb); // 每次有订阅就放入事件名称对应的回调列表
  }
 
  // 一次性订阅事件
  once(eventName, cb) {
    const onceCb = (...args) => {
      cb(...args);
      this.off(eventName, onceCb); // 调用完成后取消订阅
    };
    this.on(eventName, onceCb);
  }
 
  // 发布事件
  emit(eventName, ...args) {
    if (this.value.has(eventName)) {
      // 拿到事件名称对应的回调列表,并依次执行
      const cbs = this.value.get(eventName);
      cbs.forEach((cb) => {
        cb(...args);
      });
    }
  }
 
  // 取消订阅
  off(eventName, cb) {
    if (this.value.has(eventName)) {
      if (cb) {
        // 如果有传入回调,则只取消这个回调
        this.value.set(
          eventName,
          this.value.get(eventName).filter((i) => i !== cb)
        );
      } else {
        // 否则取消所有回调
        this.value.delete(eventName);
      }
    }
  }
}
 
let e = new EventBus();
 
let f = (...args) => console.log(...args);
e.on("a", f);
e.once("b", f);
 
e.on("c", f);
e.off("c", f);
 
e.emit("a", 111);
e.emit("b", 222);
e.emit("b", 333);
e.emit("c", 444);
 
// 最终输出:
// 111
// 222

实现异步任务限制时间

如一个异步任务如果 10s 没有完成则返回超时

const task = () => { ... }
 
const timeout = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('timeout')
  }, 10000)
})
 
Promise.race([task(), timeout]).then(res => {
  console.log(res)
})