call、apply、bind 是 JavaScript 中高频出现却又容易被“会用但不理解”的三个方法。它们看起来只是改变 this 指向的小工具,实际上牵涉到函数调用机制、this 绑定规则、原型链、闭包、构造函数模拟以及 new 的优先级。本文将从原理出发,一步步手写实现,帮助你真正吃透函数调用与 this 绑定。
一、为什么需要 call、apply、bind?
在 JavaScript 中,this 的指向不是由函数定义的位置决定的,而是由函数调用方式决定的。常见规则可以概括为:
- 普通函数调用:
this指向全局对象(非严格模式)或undefined(严格模式) - 方法调用:
this指向调用该方法的对象 - 构造函数调用:
this指向新创建的实例 - 显式绑定:通过
call、apply、bind指定this
前三种是隐式规则,而 call、apply、bind 提供了显式控制 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处理null和undefined,非严格模式下应指向全局对象,这里用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
apply 与 call 唯一区别是参数形式。实现时只需把剩余参数改为接收一个数组。
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 可能为 null 或 undefined,此时相当于无参数调用。另外,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 boundFn 为 true。此时应忽略绑定的 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 来自外层作用域。因此对箭头函数使用 call、apply、bind 不会改变 this,手写实现也无法绕过这一点。
2. 严格模式下的差异
在严格模式中,call(null) 会让 this 保持为 null,而不是全局对象。手写时如果追求与原生完全一致,需要判断函数是否处于严格模式,这通常通过 Function.prototype.toString 或 'use strict' 检测,实际项目中不必过度纠结。
3. bind 返回的函数作为构造函数时,bind 传入的参数仍然有效
注意上面的实现中,bindArgs 在 new 调用时依然会被传入,这与原生行为一致。
4. 性能与可读性
手写实现主要用于理解原理。生产环境应优先使用原生方法,它们经过引擎优化,性能更好,边界处理也更完善。
六、总结
通过手写 call、apply、bind,我们可以把零散的 this 知识串联成一条完整的链路:
call和apply的本质是“临时方法借用”,利用对象调用改变thisbind的本质是“闭包 + apply + 构造函数模拟”,需要额外处理new优先级和原型链this绑定规则中,new的优先级高于显式绑定,这也是bind实现中必须判断instanceof的原因
理解这些之后,再回头看 React 事件绑定、防抖节流、函数柯里化、继承实现等场景,你会发现它们背后都藏着 call、apply、bind 的影子。真正掌握它们,不只是会手写代码,更是对 JavaScript 函数调用机制的一次系统梳理。
未经允许不得转载:任鹏个人博客 » 手写 call、apply、bind:彻底理解函数调用与 this 绑定

