聊天讨论 手写 call、apply、bind:从原理到实现,附 3 个最容易忽略的边界情况

193577746(kyriewen) · June 11, 2026 · 16 hits

本文手写实现 JS 中最重要的三个 this 绑定函数,并处理 90% 的人会忽略的边界问题。代码可直接复制。


一、准备知识:this 的优先级

callapplybind 都用于显式绑定 this。区别:

  • call:立即执行,参数逐个传递
  • apply:立即执行,参数以数组传递
  • bind:返回新函数,不立即执行,支持柯里化

手写它们的关键:将函数挂载到 context 对象上执行,然后删除


二、手写 call

Function.prototype.myCall = function(context, ...args) {
  // 1. 处理 context 为 null/undefined 时指向全局对象
  context = context ?? window ?? global;

  // 2. 使用 Symbol 避免覆盖原对象属性
  const fnKey = Symbol('fn');
  context[fnKey] = this;

  // 3. 执行函数并获取返回值
  const result = context[fnKey](...args);

  // 4. 删除临时属性
  delete context[fnKey];

  return result;
};

测试

function say(age) {
  return `${this.name} is ${age}`;
}
const obj = { name: '张三' };
console.log(say.myCall(obj, 25)); // "张三 is 25"

边界 1context 为原始类型(number/string/boolean)时,需要转为对象。

// 改进
context = context !== null && context !== undefined ? Object(context) : window;

三、手写 apply

call 几乎一样,只是参数形式不同。

Function.prototype.myApply = function(context, argsArray = []) {
  context = context !== null && context !== undefined ? Object(context) : window;
  const fnKey = Symbol('fn');
  context[fnKey] = this;
  const result = context[fnKey](...argsArray);
  delete context[fnKey];
  return result;
};

测试

say.myApply(obj, [30]); // "张三 is 30"

四、手写 bind(最难)

bind 的特点:

  1. 返回一个新函数
  2. 支持柯里化(预设参数)
  3. 当新函数作为构造函数(new)时,this 指向实例,而非绑定的 context
Function.prototype.myBind = function(context, ...presetArgs) {
  const fn = this;

  function bound(...restArgs) {
    // 关键:如果当前函数被 new 调用,this 指向实例,忽略绑定的 context
    const isNewCall = this instanceof bound;
    const ctx = isNewCall ? this : (context !== null && context !== undefined ? Object(context) : window);
    return fn.apply(ctx, [...presetArgs, ...restArgs]);
  }

  // 维持原型链(如果原函数有 prototype)
  if (fn.prototype) {
    bound.prototype = Object.create(fn.prototype);
  }

  return bound;
};

测试

function Person(name, age) {
  this.name = name;
  this.age = age;
}
const boundPerson = Person.myBind({ x: 1 }, '李四');
const p = new boundPerson(28); // 作为构造函数,this 指向 p,忽略 {x:1}
console.log(p); // Person { name: '李四', age: 28 }

五、3 个最容易忽略的边界情况

边界 1:context 为 null/undefined

原生 call 会指向全局对象(浏览器 window,Node global)。我们的实现已处理。

边界 2:函数有返回值

必须将返回值传给调用方。已在实现中通过 result 返回。

边界 3:bind 后的函数作为构造函数

  • 当使用 new 调用 bound 时,this 指向新创建的实例,不能继续绑定到原来的 context
  • 上述 myBind 中通过 this instanceof bound 判断即可。

验证

function Base(age) {
  this.age = age;
}
const BoundBase = Base.myBind({ name: 'fake' }, 10);
const obj = new BoundBase(20);
console.log(obj); // Base { age: 20 } ,而不是 { name: 'fake' }

六、完整代码(可直接复制)

// call
Function.prototype.myCall = function(context, ...args) {
  context = context !== null && context !== undefined ? Object(context) : globalThis;
  const key = Symbol();
  context[key] = this;
  const result = context[key](...args);
  delete context[key];
  return result;
};

// apply
Function.prototype.myApply = function(context, argsArray = []) {
  context = context !== null && context !== undefined ? Object(context) : globalThis;
  const key = Symbol();
  context[key] = this;
  const result = context[key](...argsArray);
  delete context[key];
  return result;
};

// bind
Function.prototype.myBind = function(context, ...presetArgs) {
  const fn = this;
  function bound(...restArgs) {
    const isNew = this instanceof bound;
    const ctx = isNew ? this : (context !== null && context !== undefined ? Object(context) : globalThis);
    return fn.apply(ctx, [...presetArgs, ...restArgs]);
  }
  if (fn.prototype) bound.prototype = Object.create(fn.prototype);
  return bound;
};

七、总结

  • callapply 的核心是 临时挂载 + 执行 + 删除
  • bind 的核心是 返回函数 + 柯里化 + 判断 new 调用
  • 边界情况(null/undefined、原始类型、new 优先级)是面试和实际编码的常考点。

文中所有代码均已测试,可放心直接用于 polyfill。下一篇准备手写 instanceofnew 操作符,欢迎关注。

讨论:你还遇到过哪些 this 相关的奇怪 bug?评论区分享。

No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.