在 JavaScript 的世界中,call、apply 和 bind 是三个非常重要的方法,它们都属于 Function.prototype 上的方法。通过这三个方法,我们可以控制函数内部的 this 指向,也可以实现一些非常实用的功能。
但对很多前端新手来说,这三个方法看起来很像,却又难以区分。今天我们就来从头讲起,用最通俗易懂的语言和实际例子,让你彻底搞清楚它们之间的区别与用途。
一、先理解一个核心概念:函数中的 this
在讲解 call、apply 和 bind 之前,我们首先要明白一个关键点:
在 JavaScript 中,函数内部的
this关键字指向谁,取决于函数如何被调用,而不是它在哪里定义。
例如:
javascript
const person = {
name: "小明",
sayHello: function () {
console.log("你好,我是" + this.name);
}
};
person.sayHello(); // 输出:你好,我是小明
这里的 this 指向的是 person 对象,因为函数是作为对象的方法被调用的。
但如果这样写:
javascript
const sayHello = person.sayHello;
sayHello(); // 输出:你好,我是undefined(非严格模式下可能为 window)
这时候 this 就不再指向 person,而是指向全局对象(如浏览器中的 window),所以输出了 undefined。
这就引出了我们今天要讲的内容:如何手动控制函数内部的 this?
答案就是:使用 call、apply 或 bind!
二、call 方法详解
定义:
call() 方法用于立即调用一个函数,并指定该函数内部的 this 值,还可以传入参数。
语法:
javascript
function.call(thisArg, arg1, arg2, ...)
thisArg:你希望函数内部的this指向的对象。arg1, arg2,...:传给函数的参数列表。
实际案例:
场景:借用其他对象的方法
假设我们有两个对象:
javascript
const dog = {
name: '旺财',
speak: function (emotion) {
console.log(this.name + '说:汪汪!我感到' + emotion);
}
};
const cat = {
name: '咪咪'
};
现在我们想让 cat 也"说话",但又不想重复写一个 speak 函数,就可以用 call 来"借"一下:
javascript
dog.speak.call(cat, '开心');
// 输出:咪咪说:汪汪!我感到开心
这里,我们把 dog.speak 这个函数拿出来,用 call 把它的 this 改成 cat,并传了一个参数 "开心"。
三、apply 方法详解
定义:
apply() 的作用和 call() 类似,也是用来改变函数内部的 this 指向 ,不同之处在于参数传递方式不同。
语法:
javascript
function.apply(thisArg, [argsArray])
thisArg:同上。argsArray:一个数组或类数组,表示函数的参数。
实际案例:
场景:求数组中的最大值
我们知道,JavaScript 的 Math.max() 可以接收多个数字参数,但不能直接传一个数组进去:
javascript
const numbers = [3, 7, 2, 9, 5];
console.log(Math.max(numbers)); // 输出 NaN
这时候可以用 apply 来解决这个问题:
javascript
console.log(Math.max.apply(null, numbers)); // 输出:9
解释:我们把 numbers 数组作为参数传给了 Math.max,虽然第一个参数 null 没有用到(因为 Math.max 不依赖 this),但这正是 apply 的标准用法。
四、bind 方法详解
定义:
bind() 方法不会立即执行函数,而是返回一个新的函数 ,这个新函数内部的 this 已经绑定为你指定的对象。
语法:
javascript
function.bind(thisArg, arg1, arg2, ...)
- 返回的是一个绑定好
this的新函数,需要后续调用才会执行。
实际案例:
场景:事件监听器中保持 this 指向
在开发中,我们经常会遇到这样的问题:
javascript
const button = document.getElementById('myButton');
const user = {
name: '张三',
clickHandler: function () {
console.log(this.name + '点击了按钮');
}
};
button.addEventListener('click', user.clickHandler);
这时候你会发现,点击按钮后输出的是 undefined 点击了按钮,因为 this 指向的是 button 元素,而不是 user。
怎么解决?用 bind!
javascript
button.addEventListener('click', user.clickHandler.bind(user));
这样,无论点击多少次,都会正确输出 "张三点击了按钮"。
五、call、apply 和 bind 的区别总结
| 特性 | call | apply | bind |
|---|---|---|---|
| 是否立即执行 | 是 | 是 | 否(返回一个新函数) |
| 参数形式 | 逐个传参 | 数组传参 | 逐个传参 |
| 用途 | 强制绑定 this 并立即调用 | 调用函数并传入数组参数 | 创建一个绑定好 this 的新函数供以后调用 |
六、综合对比图解
ini
call => func.call(obj, arg1, arg2)
apply => func.apply(obj, [arg1, arg2])
bind => const newFunc = func.bind(obj, arg1, arg2); newFunc()
七、常见应用场景对比表
| 应用场景 | 推荐使用 | 说明 |
|---|---|---|
| 借用方法(如猫说话) | call / apply | 控制 this 并立即执行 |
| 求最大值/最小值 | apply | 参数是数组时更方便 |
| 绑定事件处理函数 | bind | 避免 this 指向错误 |
| 提前绑定部分参数 | bind | 可以预设一部分参数 |
| 替换 this 执行函数 | call / apply | 根据参数形式选择其中一个 |
八、进阶技巧:偏函数应用(Partial Application)
利用 bind,我们还可以实现一种叫做偏函数的技术,即提前绑定一部分参数,生成一个新函数。
javascript
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
console.log(double(5)); // 输出:10
在这个例子中,我们把 multiply 函数的第一个参数固定为 2,创建了一个新的 double 函数,专门用来计算某个数乘以 2。
九、面试常问题:call、apply 和 bind 的底层原理是什么?
虽然这超出了本文的重点,但简单提一下可以帮助你更好地理解:
call和apply的本质是:临时将函数赋值给某个对象,然后调用这个对象的方法 ,从而改变this。bind的本质是:返回一个包装函数,里面封装了原函数和 this 的绑定逻辑。
如果你感兴趣,可以进一步学习这些方法的 polyfill 实现。
十、结语:掌握 call、apply、bind 是成为高级前端的必经之路
这三个方法看似简单,却在 JavaScript 开发中无处不在。它们帮助我们解决了:
- 函数上下文混乱的问题
- 参数传递不灵活的问题
- 函数复用性差的问题
理解它们的区别和适用场景,不仅能提升你的代码质量,也能让你在面试中脱颖而出。
如果你能熟练运用
call、apply和bind,那么恭喜你,你已经迈出了通往高级前端工程师的重要一步!
如果你还想了解它们在 ES6/ESNext 中的新特性,或者想看看在 Vue、React 等框架中是如何使用它们的,欢迎继续关注我的博客更新
建议练习题目:
- 写一个函数,使用
call让两个不同的对象都能调用同一个方法。 - 使用
apply求数组中的最大值和最小值。 - 使用
bind绑定事件处理器,并测试 this 是否正确指向。 - 用
bind实现一个加法函数的偏函数版本。