深入解析 JavaScript 中的 call、apply、bind:用法、差异与面试题
在 JavaScript 中,call
、apply
和bind
是 Function.prototype 上的三个重要方法,它们的核心作用都是改变函数执行时的 this 指向,但在用法和场景上又存在明显差异。无论是日常开发还是面试,这三个方法都是高频考点。本文将从用法、差异对比、面试题解析三个维度,帮你彻底搞懂它们。
一、逐个击破:call、apply、bind 的详细用法
要掌握三者的区别,首先得明确每个方法的基本语法和使用场景。它们的第一个参数都是 “要绑定给 this 的值”,但后续参数的传递方式、函数是否立即执行等特性各不相同。
1. call 方法:逐个传递参数,立即执行
call
的作用是立即调用函数,并将函数内的this
绑定到第一个参数指定的对象上,后续参数则逐个传入函数。
语法
function.call(thisArg, arg1, arg2, ..., argN)
-
thisArg:函数执行时绑定的
this
值。如果为null
或undefined
,在非严格模式下this
会指向全局对象(浏览器中是window
,Node.js 中是global
),严格模式下仍为null/undefined
。 -
arg1, arg2,…:传递给函数的参数,需逐个列出。
-
返回值:函数执行后的返回值。
示例:改变 this 指向并传参
// 定义一个函数,打印this和参数function printInfo(age, job) {console.log(`姓名:${this.name},年龄:${age},职业:${job}`);}// 定义一个对象,作为this的绑定目标const person = { name: "张三" };// 使用call调用函数:this绑定到person,参数逐个传入printInfo.call(person, 25, "前端工程师");// 输出:姓名:张三,年龄:25,职业:前端工程师
2. apply 方法:数组传递参数,立即执行
apply
与call
的核心逻辑完全一致 ——立即调用函数并改变 this 指向,唯一的区别是:apply
的第二个参数必须是数组(或类数组对象,如 arguments),数组中的元素会作为参数传递给函数。
语法
function.apply(thisArg, [argsArray])
-
thisArg:同
call
,绑定给this
的值。 -
argsArray:数组或类数组对象,内部元素会作为参数传递给函数;若无需传参,可传
null
或空数组。 -
返回值:函数执行后的返回值。
示例:数组参数的场景
当我们需要传递的参数本身就是数组时,apply
会比call
更简洁(无需手动拆解数组):
// 复用上面的printInfo函数和person对象const info = [28, "产品经理"]; // 参数数组// 使用apply调用函数:第二个参数直接传数组printInfo.apply(person, info);// 输出:姓名:张三,年龄:28,职业:产品经理
经典场景:求数组最大值
Math.max()
方法不支持直接传入数组,但用apply
可以轻松解决:
const numbers = [10, 5, 20, 8];const max = Math.max.apply(Math, numbers); // 等同于Math.max(10,5,20,8)console.log(max); // 输出:20
3. bind 方法:绑定 this,返回新函数(不立即执行)
bind
与前两者的最大区别是:不立即执行函数,而是返回一个新的函数,新函数的this
被永久绑定到bind
的第一个参数上,后续参数会作为 “预设参数” 传递给新函数。
语法
const newFunction = function.bind(thisArg, arg1, arg2, ..., argN)
-
thisArg:绑定给新函数的
this
值(一旦绑定,后续无法通过call/apply
修改)。 -
arg1, arg2,…:预设参数,会在新函数调用时,排在实际传入参数的前面。
-
返回值:绑定了
this
和预设参数的新函数。
示例 1:绑定 this,后续调用
// 复用printInfo函数和person对象const boundPrint = printInfo.bind(person); // 返回新函数,this已绑定person// 后续需要时调用新函数,传入参数boundPrint(30, "后端工程师");// 输出:姓名:张三,年龄:30,职业:后端工程师
示例 2:预设参数(函数柯里化)
bind
的预设参数特性可用于 “函数柯里化”(将多参数函数拆分为单参数函数):
// 定义一个加法函数function add(a, b) {return a + b;}// 用bind预设第一个参数a=10,返回新函数const add10 = add.bind(null, 10);// 调用新函数时,只需传第二个参数bconsole.log(add10(5)); // 10+5=15console.log(add10(8)); // 10+8=18
二、横向对比:call、apply、bind 的核心差异
掌握了单个方法的用法后,我们通过表格直观对比三者的核心差异,避免混淆:
对比维度 | call | apply | bind |
---|---|---|---|
执行时机 | 立即执行函数 | 立即执行函数 | 不立即执行,返回新函数 |
参数传递方式 | 第二个参数开始,逐个传入 | 第二个参数是数组(或类数组) | 第二个参数开始,逐个预设参数 |
返回值 | 函数执行后的返回值 | 函数执行后的返回值 | 绑定了 this 的新函数 |
this 修改 | 临时修改一次执行的 this | 临时修改一次执行的 this | 永久绑定 this(新函数无法修改) |
适用场景 | 参数数量固定时 | 参数已存在于数组中时 | 需延迟执行(如定时器、事件) |
三、面试题实战:从理论到应用
理解了基础后,我们结合高频面试题,看看实际场景中如何运用这些知识。
面试题 1:用 call/apply 实现 bind 方法
题目要求:手动实现一个myBind
方法,功能与原生bind
一致。
思路分析:
-
myBind
是函数的方法,需挂载到Function.prototype
上; -
接收
thisArg
和预设参数,返回一个新函数; -
新函数调用时,需将
this
绑定到thisArg
,并合并预设参数和实际参数; -
需处理新函数被
new
关键字调用的场景(此时this
应指向实例,而非thisArg
)。
实现代码:
Function.prototype.myBind = function (thisArg, ...presetArgs) {const self = this; // 保存原函数(调用myBind的函数)// 返回新函数const boundFunction = function (...actualArgs) {// 合并参数:预设参数 + 实际参数const args = [...presetArgs, ...actualArgs];// 判断是否用new调用:this是否为boundFunction的实例if (this instanceof boundFunction) {// 用new调用时,this指向实例,原函数作为构造函数执行return new self(...args);} else {// 普通调用,用call绑定thisArgreturn self.call(thisArg, ...args);}};// 让新函数的原型继承原函数的原型(保证new实例的原型链正确)boundFunction.prototype = Object.create(self.prototype);return boundFunction;};// 测试function Person(name, age) {this.name = name;this.age = age;}const BoundPerson = Person.myBind(null, "李四");const p = new BoundPerson(26);console.log(p.name); // 李四(预设参数)console.log(p.age); // 26(实际参数)
面试题 2:解释下面代码的输出结果
题目代码:
const obj = {name: "A",fn: function () {console.log(this.name);}};const fn = obj.fn;setTimeout(fn.bind(obj), 1000); // 1setTimeout(obj.fn.call(obj), 1000); // 2setTimeout(() => obj.fn.apply(obj), 1000); // 3
问题:1 秒后,三个setTimeout
分别输出什么?
分析与答案:
-
1 处(bind):
fn.bind(obj)
返回新函数,1 秒后执行新函数,this
绑定obj
,输出A
; -
2 处(call):
obj.fn.call(obj)
会立即执行(call 是立即执行),此时会先输出A
,1 秒后setTimeout
执行的是call
的返回值(undefined
),无额外输出; -
3 处(apply):箭头函数延迟 1 秒执行,执行时调用
obj.fn.apply(obj)
,this
绑定obj
,输出A
;
最终输出:先立即输出A
(来自 2 处),1 秒后输出A
(1 处)和A
(3 处)。
面试题 3:如何将类数组对象转为真正的数组?
题目要求:列举至少两种将类数组对象(如arguments
、DOM 元素集合)转为数组的方法,其中一种需用到call/apply
。
答案:
- 用
Array.prototype.slice.call()
:
function fn() {const args = Array.prototype.slice.call(arguments); // 类数组转数组console.log(Array.isArray(args)); // true}fn(1,2,3);
- 用
Array.from()
(ES6):
const doms = document.querySelectorAll("div"); // DOM类数组const domArray = Array.from(doms);
- 用扩展运算符(ES6):
function fn() {const args = [...arguments];}
四、总结
call
、apply
、bind
的核心是 “改变 this 指向”,但因执行时机和参数传递方式的差异,适用场景各不相同:
-
需立即执行且参数固定 → 用
call
; -
需立即执行且参数在数组中 → 用
apply
; -
需延迟执行(如定时器、事件回调) → 用
bind
。
掌握它们不仅能提升代码灵活性(如复用函数、处理 this 丢失问题),更是面试中的 “加分项”。建议结合本文的示例和面试题多动手实践,才能真正融会贯通。