JS 手写基础

面试精简版 · 记住:输入输出 → 最小模板 → 常见坑

1 实现 bind

最简面试手写版(支持 new)
  • bind 返回一个新函数,绑定 this + 预设参数
  • 三个要点:① 合并两次参数 ② new 调用时 this 指向实例 ③ 继承原函数 prototype
Function.prototype.myBind = function (context, ...presetArgs) {
  const fn = this

  function bound(...callArgs) {
    // new 调用时 this 是 bound 的实例,普通调用时 this 固定为 context
    const thisArg = this instanceof bound ? this : context
    return fn.apply(thisArg, presetArgs.concat(callArgs))
  }

  // 让 new bound() 的实例能继承原函数 fn 的原型方法
  bound.prototype = Object.create(fn.prototype)
  return bound
}

2 深拷贝

基础版 + WeakMap 处理循环引用
  • 只考虑三种:简单类型 + Array + Object
  • WeakMap 解决循环引用(a.self = a
// 仅考虑三种:简单类型 + Array + Object
function deepClone(origin, mp = new WeakMap()) {
  if (typeof origin !== 'object' || origin === null) {
    return origin
  }
  // WeakMap 处理循环引用,已拷贝过的直接返回
  if (mp.has(origin)) return mp.get(origin)

  const target = Array.isArray(origin) ? [] : {}
  mp.set(origin, target) // 先存再递归,防止死循环

  for (let key in origin) {
    if (origin.hasOwnProperty(key)) {
      target[key] = deepClone(origin[key], mp)
    }
  }
  return target
}
完善版:支持 Symbol 属性
function deepClone(origin, map = new WeakMap()) {
  if (origin === null || typeof origin !== 'object') return origin
  if (map.get(origin)) return map.get(origin)

  let target = Array.isArray(origin) ? [] : {}
  map.set(origin, target)

  const keys = [
    ...Object.keys(origin),
    ...Object.getOwnPropertySymbols(origin)
  ]
  for (let key of keys) {
    if (origin.hasOwnProperty(key)) {
      target[key] = deepClone(origin[key], map)
    }
  }
  return target
}

3 发布订阅 EventBus

数据结构:subscribers = { event: [cb1, cb2] }
  • :subscribers 是对象,不要写成数组
class PubSub {
  constructor() {
    this.subscribers = {}
  }

  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = []
    }
    this.subscribers[event].push(callback)
  }

  unsubscribe(event, callback) {
    if (!this.subscribers[event]) return
    this.subscribers[event] = this.subscribers[event]
      .filter(sub => sub !== callback)
    if (this.subscribers[event].length === 0) {
      delete this.subscribers[event]
    }
  }

  publish(event, data) {
    if (!this.subscribers[event]) return
    this.subscribers[event].forEach(cb => cb(data))
  }
}

4 数组扁平化 flat

全部拍平 + 按层数拍平
// 全部拍平
// 子问题:元素就推进去,数组就 concat 递归自己
function myFlat(arr) {
  let ans = []
  for (let i = 0; i < arr.length; i++) {
    let item = arr[i]
    if (!Array.isArray(item)) {
      ans.push(item)
    } else {
      ans = ans.concat(myFlat(item)) // 直接递归自己就好了
    }
  }
  return ans
}

// 按层数拍平(注意默认值 layer=1)
function myFlat(arr, layer = 1) {
  if (layer === 0) return arr // layer 为 0 不再拍平
  let ans = []
  for (let i = 0; i < arr.length; i++) {
    let item = arr[i]
    if (!Array.isArray(item)) {
      ans.push(item)
    } else {
      ans = ans.concat(myFlat(item, layer - 1))
    }
  }
  return ans
}

5 函数柯里化

定长参数版:参数够了就执行,不够就继续收集
  • 核心:把 args 累积,args.length >= fn.length 时执行
  • 支持 add(1,2)(3) / add(1)(2)(3) 等任意组合
// 总体逻辑:把传入的参数全部保存进 args,一旦数量够了就执行
// curry(fn, ...args, ...args2) 会把两次参数合并作为新的 args 传给下一个 curry
function curry(fn, ...args) {
  if (args.length >= fn.length) {
    return fn(...args) // 参数够了,直接执行
  }
  // 参数不够,返回新函数继续收集
  return (...args2) => curry(fn, ...args, ...args2)
}

function add1(x, y, z) { return x + y + z }
const add = curry(add1)

add(1, 2, 3)  // 6
add(1)(2)(3)  // 6
add(1, 2)(3)  // 6
不定长参数版(用 () 收口)
function add(...args) {
  let sum = args.reduce((a, b) => a + b, 0)

  function fn(...more) {
    if (more.length === 0) return sum
    sum += more.reduce((a, b) => a + b, 0)
    return fn
  }
  return fn
}

add(1)(2)(3)()         // 6
add(1, 2, 3)(4)()      // 10
add(1)(2)(3)(4)(5)()   // 15

6 防抖 debounce

触发后 n 秒执行,重复触发重新计时
  • 场景:搜索输入、resize、邮箱校验
  • :外层用 function 保留 this,内层用 fn.call(this, ...args)
const debounce = (fn, delay) => {
  let timer
  return function (...args) { // 这里要用 function,箭头函数没有自己的 this
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.call(this, ...args) // 用 call 保证 this 不丢失
    }, delay)
  }
}

7 节流 throttle

固定间隔内只执行一次(技能 CD)
  • 场景:滚动加载、高频点击、表单重复提交
const throttle = (fn, delay) => {
  let flag = true
  return function (...args) {
    if (!flag) return
    fn.call(this, ...args)
    flag = false
    setTimeout(() => {
      flag = true
    }, delay)
  }
}

8 实现 call / apply

原理:把函数挂到目标对象上调用,用 Symbol 防覆盖
// 原理:把函数挂到目标对象上调用,this 就指向它了
// greet.myCall(person, 'Hello') → 在 person 作用域下调用 greet

// call
Function.prototype.myCall = function (thisArg, ...args) {
  const fn = this // this 指向调用 myCall 的函数,即要改变 this 的那个函数
  if (typeof fn !== 'function') throw new TypeError(fn + ' is not a function')
  thisArg = thisArg || globalThis // 没传 this 就绑定全局
  const sym = Symbol('temp') // 用 Symbol 防止覆盖 thisArg 上已有的属性
  thisArg[sym] = fn // 把函数挂到目标对象上
  let ans = thisArg[sym](...args) // 通过目标对象调用,this 就指向它了
  delete thisArg[sym] // 用完删掉临时属性
  return ans
}

// apply(和 call 唯一区别:参数是数组)
Function.prototype.myApply = function (context, args) {
  if (typeof this !== 'function') throw new TypeError(this + ' is not a function')
  context = context || globalThis
  if (!Array.isArray(args) && args !== null && args !== undefined) {
    throw new TypeError('CreateListFromArrayLike called on non-object')
  }
  const sym = Symbol()
  context[sym] = this
  const res = args ? context[sym](...args) : context[sym]()
  delete context[sym]
  return res
}