好的,我们来深入地探讨和实现 JavaScript 中的 call, apply, 和 bind。
这三者是 JavaScript 中非常重要的函数方法,核心作用都是改变函数执行时的 this 上下文。理解它们是掌握 JavaScript 语言精髓的关键一步。
核心前提:理解 this
在解释这三兄弟之前,必须先明白 this 是什么。JavaScript 中的 this 是一个动态的关键字,它在函数被调用时确定,其值取决于函数是如何被调用的。
-
全局调用:
func(),在非严格模式下this指向全局对象 (window或global),严格模式下是undefined。 -
作为对象方法调用:
obj.func(),this指向obj。 -
作为构造函数调用:
new Func(),this指向新创建的实例。 -
箭头函数: 箭头函数没有自己的
this,它会捕获其所在词法作用域的this值。
call, apply, bind 的出现,就是为了让我们能够手动、精确地控制这个动态的 this,让它指向我们想要的对象。
一、讲解:call, apply, bind 的区别与联系
1. Function.prototype.call()
call 方法会立即调用一个函数,并将该函数的 this 值设置为给定的第一个参数。后续的参数会作为独立参数传递给该函数。
-
语法:
function.call(thisArg, arg1, arg2, ...) -
特点:
-
立即执行函数。
-
第一个参数是
this的指向。 -
后续参数以逗号分隔的形式,逐一传入函数。
-
你可以这样记:Call 的 C 代表 Comma (逗号)。
示例:
JavaScript
const person = {
name: '张三'
};
function greet(greeting, punctuation) {
console.log(`${greeting}, my name is ${this.name}${punctuation}`);
}
greet('Hello', '!'); // 在浏览器非严格模式下,this.name 是 undefined,输出 "Hello, my name is !"
greet.call(person, 'Hi', '.'); // 输出 "Hi, my name is 张三."
2. Function.prototype.apply()
apply 方法与 call 非常相似,它也会立即调用函数并指定 this 值。唯一的区别在于它接收参数的方式。
-
语法:
function.apply(thisArg, [argsArray]) -
特点:
-
立即执行函数。
-
第一个参数是
this的指向。 -
第二个参数是一个数组(或类数组对象),数组中的元素会作为参数传递给函数。
-
你可以这样记:Apply 的 A 代表 Array (数组)。
示例:
JavaScript
const person = {
name: '李四'
};
function greet(greeting, punctuation) {
console.log(`${greeting}, my name is ${this.name}${punctuation}`);
}
greet.apply(person, ['Hey', '!!!']); // 输出 "Hey, my name is 李四!!!"
// 一个经典应用:使用 Math.max 找出数组最大值
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers); // 等同于 Math.max(5, 6, 2, 3, 7)
console.log(max); // 7
// 使用ES6的扩展运算符更简单: Math.max(...numbers)
3. Function.prototype.bind()
bind 是三者中最特别的一个。它不会立即执行函数,而是会创建一个新的函数。这个新函数的 this 被永久地绑定到了 bind 的第一个参数。
-
语法:
function.bind(thisArg[, arg1[, arg2[, ...]]]) -
特点:
-
不立即执行,而是返回一个新的、绑定好
this的函数。 -
可以预先设置部分参数(这个特性被称为柯里化 (Currying))。
-
示例:
bind 最常见的应用场景是在异步操作(如 setTimeout)或事件处理中,this 指向会发生改变。
JavaScript
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // undefined,因为此时 this 指向全局对象
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 42,因为 this 被永久绑定到了 module
// 柯里化示例
function add(a, b) {
return a + b;
}
const add5 = add.bind(null, 5); // this 无所谓,所以是 null。预先设置第一个参数 a 为 5
console.log(add5(10)); // 15, 相当于调用 add(5, 10)
总结对比
| 特性 | call |
apply |
bind |
|---|---|---|---|
| 执行时机 | 立即执行 | 立即执行 | 不执行,返回新函数 |
this 指向 |
第一个参数指定 | 第一个参数指定 | 第一个参数指定 |
| 参数传递 | 逐个传递 (逗号分隔) | 数组形式传递 | 部分预设,剩余后传 |
| 返回值 | 原函数的返回值 | 原函数的返回值 | 绑定后的新函数 |
二、实现:手写 call, apply, bind
要实现这三个方法,核心思路是:将要执行的函数,挂载到目标 this 对象上作为一个临时方法来调用,调用结束后再删除它。
1. 实现 myCall
JavaScript
Function.prototype.myCall = function(context, ...args) {
// 1. 处理 context,如果为 null 或 undefined,则指向全局对象
// 使用 Object() 包装,确保 context 是一个对象,从而可以挂载属性
context = context === null || context === undefined ? window : Object(context);
// 2. 创建一个唯一的 key,避免与 context 上的原有属性冲突
const uniqueKey = Symbol('fn');
// 3. 将当前函数 (this) 挂载到 context 上
// 这里的 this 就是调用 myCall 的函数,例如前面的 greet 函数
context[uniqueKey] = this;
// 4. 调用函数,并通过 ...args 展开参数
const result = context[uniqueKey](...args);
// 5. 删除临时挂载的函数,保持 context 干净
delete context[uniqueKey];
// 6. 返回函数的执行结果
return result;
}
// ---- 测试 ----
const person = { name: '王五' };
function introduce(job, age) {
console.log(`我叫 ${this.name}, 我的工作是 ${job}, 今年 ${age} 岁`);
return this.name;
}
const name = introduce.myCall(person, '工程师', 28);
// 输出: 我叫 王五, 我的工作是 工程师, 今年 28 岁
console.log('返回值:', name); // 输出: 返回值: 王五
2. 实现 myApply
myApply 的实现与 myCall 几乎完全一样,只是参数处理方式不同。
JavaScript
Function.prototype.myApply = function(context, argsArray) {
// 1. 处理 context
context = context === null || context === undefined ? window : Object(context);
// 2. 创建唯一 key
const uniqueKey = Symbol('fn');
// 3. 挂载函数
context[uniqueKey] = this;
let result;
// 4. 调用函数,处理参数数组
if (Array.isArray(argsArray)) {
result = context[uniqueKey](...argsArray);
} else {
// 如果不传 argsArray 或者传的不是数组,则不带参数执行
result = context[uniqueKey]();
}
// 5. 删除临时函数
delete context[uniqueKey];
// 6. 返回结果
return result;
}
// ---- 测试 ----
const person2 = { name: '赵六' };
introduce.myApply(person2, ['产品经理', 30]);
// 输出: 我叫 赵六, 我的工作是 产品经理, 今年 30 岁
3. 实现 myBind
myBind 的实现要复杂一些,因为它返回一个新函数,并且需要处理 new 调用的情况(这是一个高级实现点,但很重要)。
JavaScript
Function.prototype.myBind = function(context, ...bindArgs) {
// 1. 保存原始函数 (调用 myBind 的函数)
const self = this;
// 2. 返回一个新函数
const boundFunction = function(...callArgs) {
// 3. 合并两次传递的参数
const finalArgs = [...bindArgs, ...callArgs];
// 4. 处理 new 操作符调用的情况
// 当 boundFunction 被 new 调用时, `this` 是 new 创建的新实例,
// 并且 this.__proto__ === boundFunction.prototype
// 即 this instanceof boundFunction 为 true
// 此时,原始函数的 this 应该指向这个新实例,而不是我们绑定的 context
if (this instanceof boundFunction) {
// this 指向 new 创建的实例
return new self(...finalArgs);
} else {
// 普通调用,this 指向绑定的 context
return self.apply(context, finalArgs);
}
};
// 5. 维护原型链:让 boundFunction 的实例能够继承原始函数的原型
// 这样 new boundFunction() 出来的实例才能访问到原始构造函数原型上的属性
if (self.prototype) {
boundFunction.prototype = Object.create(self.prototype);
}
return boundFunction;
}
// ---- 测试 ----
const car = {
brand: 'Tesla',
showBrand() {
console.log(`This is a ${this.brand}`);
}
};
const myCarShow = car.showBrand;
// myCarShow(); // 报错或 undefined,因为 this 不指向 car
const boundShowBrand = myCarShow.myBind(car);
boundShowBrand(); // 输出: This is a Tesla
// ---- 测试 new ----
function Dog(name, age) {
this.name = name;
this.age = age;
console.log('Dog constructor called');
}
Dog.prototype.bark = function() {
console.log(`${this.name} says woof!`);
}
// 预设 this 和 name 参数
const aDogConstructor = Dog.myBind(null, '旺财');
// 使用 new 调用 bind 返回的函数
const wangcai = new aDogConstructor(3); // age = 3
// 输出: Dog constructor called
console.log(wangcai.name, wangcai.age); // 输出: 旺财 3
wangcai.bark(); // 输出: 旺财 says woof!
三、如何选择?
在现代 JavaScript (ES6+) 开发中,选择哪一个通常很清晰:
-
如果你需要改变 this 指向并立即执行函数,同时参数是分散的:用 call。
doSomething.call(context, 'arg1', 'arg2');
-
如果你需要改变 this 指向并立即执行函数,同时参数已经在一个数组里:用 apply。
doSomething.apply(context, ['arg1', 'arg2']);
(在 ES6 扩展运算符普及后,apply 的这个优势不再明显,因为你可以写 doSomething.call(context, ...argsArray),所以 call 的使用频率相对更高了)
-
如果你需要创建一个新函数,这个函数的
this永久绑定到某个对象,并可能在未来某个时候执行(例如作为回调函数或事件监听器):用bind。这是三者中唯一不立即执行的,用途也最独特。
最终建议:
-
日常开发中,当你需要手动绑定
this时,首先思考bind,因为它在处理回调和事件时最直接、最安全。 -
call和apply更多用于需要立即执行的场景,或者在一些需要巧妙调用函数的库代码和“炫技”场合。在 ES6 之后,call因为扩展运算符的存在,比apply更常用。 -
在 React 类组件中,为了保证方法中的
this指向组件实例,在构造函数中使用this.handleClick = this.handleClick.bind(this)曾是标准做法。
然而,箭头函数的出现极大地简化了 this 的处理。 在很多原本需要 bind 的场景,现在一个箭头函数就能搞定,因为它不创建自己的 this 上下文。
JavaScript
// 旧方法: bind
setTimeout(function() {
console.log(this.x);
}.bind(module), 1000);
// 新方法: 箭头函数
setTimeout(() => {
console.log(this.x); // this 继承自外层作用域
}, 1000);
尽管如此,深入理解 call, apply, bind 的原理和实现,对于理解 JavaScript 的核心运行机制、作用域以及 this 的本质,仍然具有不可替代的价值。