← 返回题目列表

如何手写 call、apply 和 bind?

高频 中等 第 4 / 27 题 更新于 2026/07/28
手写代码thiscallapplybind

简化版

callapply 都会立即调用函数并指定 this,区别是 call 接收参数列表,apply 接收数组或类数组;bind 不立即调用,而是返回绑定了 this 和前置参数的新函数。面试手写的重点是参数转发、返回值、异常清理,以及 bindnew 调用时忽略绑定的 this

详细版

临时挂载函数能演示普通对象方法调用时的 this 绑定,但无法完整复刻严格模式、原始值接收者和引擎内部槽。更准确的教学实现可用 Reflect.apply 转发调用:

function myCall(fn, thisArg, ...args) {
  return Reflect.apply(fn, thisArg, args)
}

function myApply(fn, thisArg, args) {
  return Reflect.apply(fn, thisArg, args == null ? [] : Array.from(args))
}

bind 还要区分普通调用和构造调用。构造调用时应忽略绑定对象,并把前置参数放在运行时参数之前。手写版必须明确:函数的 namelength、内部槽和原生错误细节通常不能完全复刻。

完整版教学

一、先比较三者的调用合同

API是否立即执行参数形式返回值
fn.call(ctx, a, b)逐个传入原函数返回值
fn.apply(ctx, [a, b])数组或类数组原函数返回值
fn.bind(ctx, a)可预置部分参数新的绑定函数
function greet(prefix, suffix) {
  return `${prefix}${this.name}${suffix}`
}

const user = { name: 'Ada' }
greet.call(user, 'Hi ', '!')
greet.apply(user, ['Hi ', '!'])
greet.bind(user, 'Hi ')('!')

三次结果都是 Hi Ada!,但第三次创建了一个可复用函数。

“把函数挂到对象上再调用”只适合解释方法调用规则,不是完整 polyfill。原生调用能保留严格模式下精确的 thisArg,用户态临时挂载会先把原始值装箱,也无法挂到 null 上。

二、call 的教学版与异常安全

经典写法用唯一 Symbol 避免覆盖同名属性,并用 try/finally 保证被调函数抛错时也清理临时属性:

function educationalCall(fn, thisArg, ...args) {
  if (typeof fn !== 'function') throw new TypeError('fn must be callable')

  const receiver = thisArg == null ? globalThis : Object(thisArg)
  const key = Symbol('temporary method')
  receiver[key] = fn

  try {
    return receiver[key](...args)
  } finally {
    delete receiver[key]
  }
}

这个版本能教学,但有明确差异:严格函数被原生 call(null) 调用时收到 null,这里却收到 globalThis;严格函数收到数字 1 时,原生保留数字,这里会收到包装对象。

若允许使用标准底层转发工具,Reflect.apply(fn, thisArg, args) 才能正确保留这些调用语义。

三、apply 不只是参数写成数组

规范上的 apply 第二个参数可以是数组或类数组;nullundefined 表示没有参数。可写成:

function educationalApply(fn, thisArg, args) {
  if (typeof fn !== 'function') throw new TypeError('fn must be callable')
  if (args == null) return Reflect.apply(fn, thisArg, [])
  if (typeof args !== 'object' && typeof args !== 'function') {
    throw new TypeError('args must be array-like')
  }
  return Reflect.apply(fn, thisArg, Array.from(args))
}

例如 {0: 'a', 1: 'b', length: 2} 能被 Array.from 转成两个参数。教学实现使用 Array.from 会先创建数组,原生内部过程及报错时机未必完全相同,因此仍不应称为字节级等价 polyfill。

四、bind 的普通调用与参数合并

普通调用时,后来怎样调用绑定函数都不能替换已绑定的 this;再次 bind 也只能继续追加参数:

function educationalBind(fn, thisArg, ...boundArgs) {
  if (typeof fn !== 'function') throw new TypeError('fn must be callable')

  function bound(...callArgs) {
    const args = boundArgs.concat(callArgs)
    if (new.target) {
      return Reflect.construct(fn, args, new.target)
    }
    return Reflect.apply(fn, thisArg, args)
  }

  if (fn.prototype && typeof fn.prototype === 'object') {
    bound.prototype = Object.create(fn.prototype, {
      constructor: { value: bound, writable: true, configurable: true },
    })
  }

  return bound
}

boundArgs 永远在 callArgs 前面。比如 add.bind(null, 1)(2) 最终调用 add(1, 2)

五、为什么 new bound() 必须单独处理

原生绑定函数作为构造器调用时会忽略绑定的 this,但仍使用预置参数:

function Person(name, age) {
  this.name = name
  this.age = age
}

const BoundPerson = Person.bind({ ignored: true }, 'Ada')
const person = new BoundPerson(18)

person.name              // 'Ada'
person.age               // 18
person instanceof Person // true

Reflect.construct(fn, args, new.target) 使用真正的构造语义,能处理构造函数显式返回对象、类构造器和 new.target。若目标不可构造,例如箭头函数,构造时自然抛出 TypeError

但原生绑定函数没有自己的 prototype 属性,本文为教学和 instanceof 直觉设置了原型,因此它与原生仍有差异,尤其不能据此模拟 class X extends boundFn 的全部行为。

六、严格模式和原生函数的不可模拟部分

场景原生行为临时挂载近似版
严格函数 + nullthis === null被替换为全局对象
严格函数 + 数字保留原始数字数字被装箱
bindname通常带 bound 前缀不自动一致
bindlength扣除预置参数并截断到 0不自动一致

引擎通过 [[Call]][[Construct]][[BoundTargetFunction]] 等内部机制实现这些能力,普通 JavaScript 无法创建完全相同的内部槽。面试时能准确指出不可模拟之处,比堆砌一个看似完整的 polyfill 更专业。

七、常见误区与追问

  • 误区:给目标对象挂一个普通属性就安全。 可能覆盖已有属性,而且函数抛错后若没有 finally 就会留下脏数据。
  • 误区:bind 返回箭头函数最省事。 箭头函数不可构造,会直接丢失 new bound() 这一关键语义。
  • 误区:new bound() 仍应使用绑定的对象。 构造调用会忽略绑定的 this,只保留预置参数。
  • 追问:callapply 的性能谁更好? 现代引擎中不应凭接口形式下结论,应按参数来源和可读性选择并用基准测试验证热点。
  • 追问:为什么要用 try/finally 被调用函数可能抛异常,finally 能保证临时 Symbol 属性仍被删除。
  • 追问:对已经 bind 的函数再次 bind 会怎样? 新的 thisArg 不生效,但新的预置参数会追加到已有参数之后。
  • 追问:手写版为什么推荐 Reflect.apply 它直接表达“以指定接收者和参数列表调用”,避免临时挂载造成的严格模式和装箱偏差。

八、加强记忆

  1. call:立即调用,参数逐个传。
  2. apply:立即调用,参数来自数组或类数组。
  3. bind:返回新函数,绑定接收者并预置参数。
  4. 构造new bound() 忽略绑定对象,但保留预置参数。
  5. 清理:临时挂载必须用 Symbol 和 try/finally,且仍只是近似教学版。
  6. 边界:原生内部槽、严格 thisnamelength 等无法靠几行用户态代码完整复制。