手写 call、apply、bind:彻底理解函数调用与 this 绑定

callapplybind 是 JavaScript 中高频出现却又容易被“会用但不理解”的三个方法。它们看起来只是改变 this 指向的小工具,实际上牵涉到函数调用机制、this 绑定规则、原型链、闭包、构造函数模拟以及 new 的优先级。本文将从原理出发,一步步手写实现,帮助你真正吃透函数调用与 this 绑定。

一、为什么需要 call、apply、bind?

在 JavaScript 中,this 的指向不是由函数定义的位置决定的,而是由函数调用方式决定的。常见规则可以概括为:

  • 普通函数调用:this 指向全局对象(非严格模式)或 undefined(严格模式)
  • 方法调用:this 指向调用该方法的对象
  • 构造函数调用:this 指向新创建的实例
  • 显式绑定:通过 callapplybind 指定 this

前三种是隐式规则,而 callapplybind 提供了显式控制 this 的能力。它们的核心区别是:

  • call:立即调用,参数逐个传入
  • apply:立即调用,参数以数组或类数组传入
  • bind:不立即调用,返回一个绑定了 this 和部分参数的新函数
function greet(city, country) {
  console.log(`${this.name} 来自 ${city}, ${country}`);
}

const person = { name: 'Alice' };

greet.call(person, '北京', '中国');
greet.apply(person, ['上海', '中国']);
const boundGreet = greet.bind(person, '广州');
boundGreet('中国');

二、手写 call

1. 基本思路

call 的本质是:让目标函数成为某个对象的临时方法,然后通过对象调用它。这样函数内部的 this 自然就指向了这个对象。

Function.prototype.myCall = function (context, ...args) {
  context = context == null ? globalThis : Object(context);

  const fnKey = Symbol('fn');
  context[fnKey] = this;

  const result = context[fnKey](...args);

  delete context[fnKey];

  return result;
};

2. 关键点解析

  • context == null 处理 nullundefined,非严格模式下应指向全局对象,这里用 globalThis 兼容不同环境
  • Object(context) 处理原始值,比如 myCall(1) 时,需要把数字包装成对象
  • 使用 Symbol 作为临时属性名,避免覆盖对象原有属性
  • 执行完成后必须 delete,保持对象干净
  • 返回函数执行结果,保持与原生 call 一致

3. 测试

function foo(a, b) {
  console.log(this.value, a, b);
}

const obj = { value: 42 };
foo.myCall(obj, 1, 2); // 42 1 2

三、手写 apply

applycall 唯一区别是参数形式。实现时只需把剩余参数改为接收一个数组。

Function.prototype.myApply = function (context, args) {
  context = context == null ? globalThis : Object(context);

  const fnKey = Symbol('fn');
  context[fnKey] = this;

  const result = args ? context[fnKey](...args) : context[fnKey]();

  delete context[fnKey];

  return result;
};

注意 args 可能为 nullundefined,此时相当于无参数调用。另外,apply 的第二个参数也可以是类数组,...args 展开时要求其可迭代,若需兼容类数组,可使用 Array.from(args)

四、手写 bind

bind 是最复杂的一个,因为它需要返回一个新函数,并且要处理多种调用场景。

1. 核心需求

  • 返回一个新函数
  • 新函数调用时,this 指向绑定的对象
  • 支持柯里化:绑定时的参数与调用时的参数合并
  • 支持 new 调用:当绑定后的函数被 new 调用时,绑定的 this 应被忽略,实例原型应指向原函数
Function.prototype.myBind = function (context, ...bindArgs) {
  const originalFn = this;

  function boundFn(...callArgs) {
    const isNew = this instanceof boundFn;
    const finalContext = isNew ? this : context;
    return originalFn.apply(finalContext, [...bindArgs, ...callArgs]);
  }

  if (originalFn.prototype) {
    boundFn.prototype = Object.create(originalFn.prototype);
  }

  return boundFn;
};

2. 为什么用 this instanceof boundFn 判断 new?

当使用 new boundFn() 时,boundFn 内部的 this 是新创建的实例,它继承自 boundFn.prototype,因此 this instanceof boundFntrue。此时应忽略绑定的 context,让 this 继续指向新实例,从而正确模拟构造函数行为。

3. 原型链处理

boundFn.prototype = Object.create(originalFn.prototype) 保证通过 new boundFn() 创建的实例,既能访问原函数原型上的方法,也能让 instanceof 判断保持合理。

4. 测试

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

Person.prototype.say = function () {
  console.log(this.name, this.age);
};

const BoundPerson = Person.myBind(null, 'Bob');
const p = new BoundPerson(18);
p.say(); // Bob 18
console.log(p instanceof Person); // true

五、常见误区与边界情况

1. 箭头函数无法绑定 this

箭头函数没有自己的 this,它的 this 来自外层作用域。因此对箭头函数使用 callapplybind 不会改变 this,手写实现也无法绕过这一点。

2. 严格模式下的差异

在严格模式中,call(null) 会让 this 保持为 null,而不是全局对象。手写时如果追求与原生完全一致,需要判断函数是否处于严格模式,这通常通过 Function.prototype.toString'use strict' 检测,实际项目中不必过度纠结。

3. bind 返回的函数作为构造函数时,bind 传入的参数仍然有效

注意上面的实现中,bindArgsnew 调用时依然会被传入,这与原生行为一致。

4. 性能与可读性

手写实现主要用于理解原理。生产环境应优先使用原生方法,它们经过引擎优化,性能更好,边界处理也更完善。

六、总结

通过手写 callapplybind,我们可以把零散的 this 知识串联成一条完整的链路:

  • callapply 的本质是“临时方法借用”,利用对象调用改变 this
  • bind 的本质是“闭包 + apply + 构造函数模拟”,需要额外处理 new 优先级和原型链
  • this 绑定规则中,new 的优先级高于显式绑定,这也是 bind 实现中必须判断 instanceof 的原因

理解这些之后,再回头看 React 事件绑定、防抖节流、函数柯里化、继承实现等场景,你会发现它们背后都藏着 callapplybind 的影子。真正掌握它们,不只是会手写代码,更是对 JavaScript 函数调用机制的一次系统梳理。

未经允许不得转载:任鹏个人博客 » 手写 call、apply、bind:彻底理解函数调用与 this 绑定

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏