面试题之手写new apply call bind

4 分钟阅读

手写 new / call / apply / bind

手写 new、call、apply、bind 是面试中出现频率最高的手写题之一。这四个 API 考察的是同一个核心知识点:JS 中 this 的绑定规则。理解了原理,实现只是水到渠成的事。本文按"原理 → 实现 → 边界情况"的顺序逐个实现。

1. call:显式绑定 this

call 的作用是:以指定的 this 和参数列表调用函数。

实现思路很直接——把函数临时挂载到目标对象上,以对象方法的形式调用,this 自然就指向了该对象,调用完再删掉:

javascript
Function.prototype.myCall = function(context, ...arg) {
  if (typeof this !== 'function') {
    // 排除 Function.prototype.myCall.call(sthNotAFunction) 这种情况
    throw new TypeError('Error')
  }
  // 使用 Symbol 避免覆盖 context 上已有的同名属性
  const symbolFn = Symbol('fn')
  context[symbolFn] = this
  const result = arg.length ? context[symbolFn](...arg) : context[symbolFn]()
  delete context[symbolFn]
  return result
}

两个设计细节:

  • 为什么用 Symbol 做 key:如果直接用字符串 key(比如 context.fn = this),当 context 上恰好已有同名属性时会被覆盖,delete 时还会误删对象原本的属性和方法。Symbol 的唯一性保证不会污染目标对象。
  • 为什么要 delete:挂载是手段不是目的,调用完成后必须清理,否则会在目标对象上留下垃圾属性。

这里简化处理了 context 为基础类型或 null/undefined 的边界情况(原生 call 会将其包装为对象或替换为全局对象),理解原理即可。

2. apply:数组形式的 call

apply 与 call 的唯一区别是参数以数组形式传入。有了 myCall,实现只需一行:

javascript
Function.prototype.myApply = function(context, arg) {
  // 把数组展开交给 myCall,不需要重复实现
  return this.myCall(context, ...arg)
}

3. bind:返回一个绑定了 this 的新函数

bind 不会立即执行,而是返回一个"预绑了 this 和部分参数"的新函数。它有一个容易被忽略的边界:返回的函数可能被当作构造函数使用。

javascript
Function.prototype.myBind = function(context, ...arg) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  const _this = this
  return function F(...args) {
    if (this instanceof F) {
      // 被 new 调用时,bind 指定的 this 失效,
      // 此时 this 指向新创建的实例,直接按构造函数处理
      return new _this(...arg, ...args)
    }
    // 普通调用:拼接 bind 时的预设参数和调用时的新参数
    return _this.call(context, ...arg, ...args)
  }
}

两个关键点:

  • this instanceof F 判断:原生 bind 返回的函数被 new 调用时,预设的 this 会被忽略(这正是"new 绑定优先级高于显式绑定"的 this 绑定优先级规则)。实现里必须检测并处理这种情况,否则预设参数会丢。
  • 参数拼接用展开而不是 concat:网上很多实现用 arg.concat(args) 再交给 apply,当参数本身是数组时会被意外展开——[].concat([1, 2], [2, 3]) 得到 [1, 2, 2, 3]。用展开运算符逐个传递参数不会有这个问题。

4. new:四步创建一个实例

new 运算符背后做了四件事:

  1. 创建一个新的空对象
  2. 将新对象的 __proto__ 链接到构造函数的 prototype
  3. 以新对象为 this 执行构造函数
  4. 如果构造函数返回了一个对象,则返回它;否则返回新创建的对象

翻译成代码:

javascript
function myNew(constructor, ...arg) {
  let obj = {}
  // 链接原型,让实例能访问构造函数 prototype 上的方法
  obj.__proto__ = constructor.prototype
  // 绑定 this 并执行
  let result = constructor.apply(obj, arg)
  // 构造函数显式返回对象时以它为准,否则返回新对象
  return result instanceof Object ? result : obj
}

第 4 步是最容易被遗漏的:如果构造函数 return 了一个引用类型,new 表达式的结果会是那个对象而不是新实例——这也是"构造函数里一般不写 return"这条经验的原因。

5. 小结

四个 API 的实现都围绕 this 绑定展开:

API 核心机制 关键边界
call 挂载为对象方法再调用 不污染目标对象(Symbol + delete)
apply 参数为数组的 call 直接复用 call 展开即可
bind 闭包保存 this 和预设参数 被 new 调用时 this 失效、参数正确拼接
new 建对象、链原型、绑 this、处理返回值 构造函数显式返回对象的情况

掌握这几道题的价值不在于背出实现,而在于把 this 绑定规则、原型链、闭包这些基础知识串成一条线——面试官真正想考察的正是这个。

相关文章
前后篇
← NEWER浅谈LHS和RHSOLDER →用PM2做自动部署
评论