《前端面试题:call、apply、bind 区别》
JavaScript 函数三剑客:call、apply、bind 深度解析与实战指南
引言:为什么需要改变 this 指向?
在 JavaScript 中,函数执行时的 this
值是由调用上下文决定的。但在实际开发中,我们经常需要控制函数的执行环境,例如:
- 在对象方法中借用其他对象的功能
- 实现函数复用和代码共享
- 创建预设参数的函数版本
- 实现函数式编程中的柯里化
call
、apply
和 bind
正是为此而生的三个强大工具。本文将深入剖析它们的原理、区别和实际应用场景。
一、核心概念与基本用法
1. call 方法
功能:立即调用函数,并指定函数内部的 this
值和参数列表
语法:
func.call(thisArg, arg1, arg2, ...)
示例:
const person = {name: 'Alice'
};function greet(greeting, punctuation) {console.log(`${greeting}, ${this.name}${punctuation}`);
}greet.call(person, 'Hello', '!');
// 输出: "Hello, Alice!"
2. apply 方法
功能:与 call 类似,但参数以数组形式传递
语法:
func.apply(thisArg, [argsArray])
示例:
greet.apply(person, ['Hi', '!!']);
// 输出: "Hi, Alice!!!"
3. bind 方法
功能:创建一个新函数,该函数被调用时 this
值固定为指定值
语法:
const boundFunc = func.bind(thisArg, arg1, arg2, ...)
示例:
const greetAlice = greet.bind(person, 'Hey');
greetAlice('...');
// 输出: "Hey, Alice..."
二、核心区别对比
特性 | call | apply | bind |
---|---|---|---|
调用方式 | 立即调用 | 立即调用 | 返回绑定函数 |
参数形式 | 逗号分隔列表 | 单个数组 | 逗号分隔列表 |
执行时机 | 立即执行 | 立即执行 | 延迟执行(需手动调用) |
参数预设 | 不支持 | 不支持 | 支持部分参数 |
使用场景 | 明确参数个数 | 参数个数不确定 | 需要固定 this 或预设参数 |
三、底层原理与手动实现
1. 手写 call 方法
Function.prototype.myCall = function(context, ...args) {// 处理 context 为 null 或 undefined 的情况context = context || window; // 创建唯一属性避免覆盖const fnKey = Symbol('fn');// 将函数设置为 context 的方法context[fnKey] = this;// 执行函数const result = context[fnKey](...args);// 删除临时属性delete context[fnKey];return result;
};// 测试
greet.myCall(person, 'Hello', '!');
2. 手写 apply 方法
Function.prototype.myApply = function(context, argsArray) {context = context || window;const fnKey = Symbol('fn');context[fnKey] = this;// 处理未传递 argsArray 的情况const result = argsArray ? context[fnKey](...argsArray) : context[fnKey]();delete context[fnKey];return result;
};// 测试
greet.myApply(person, ['Hi', '!!']);
3. 手写 bind 方法
Function.prototype.myBind = function(context, ...bindArgs) {const originalFunc = this;return function boundFunc(...callArgs) {// 判断是否作为构造函数使用if (new.target) {return new originalFunc(...bindArgs, ...callArgs);}return originalFunc.call(context, ...bindArgs, ...callArgs);};
};// 测试
const greetAlice = greet.myBind(person, 'Hey');
greetAlice('...');
四、高级应用场景
1. 类数组转为真实数组
function listToArray() {return Array.prototype.slice.call(arguments);
}const array = listToArray(1, 2, 3); // [1, 2, 3]
2. 继承中调用父类构造函数
function Parent(name) {this.name = name;
}function Child(name, age) {Parent.call(this, name); // 继承属性this.age = age;
}
3. 函数柯里化
function add(a, b, c) {return a + b + c;
}const addFive = add.bind(null, 2, 3);
console.log(addFive(5)); // 10 (2+3+5)
4. 数组求最大值
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers); // 7
5. 事件处理函数中的 this 绑定
class Button {constructor() {this.text = 'Click me';// 使用 bind 固定 thisthis.handleClick = this.handleClick.bind(this);}handleClick() {console.log(`Button text: ${this.text}`);}
}const btn = new Button();
document.querySelector('button').addEventListener('click', btn.handleClick);
五、常见面试题解析
1. 基础题:以下代码输出什么?
const obj = {value: 42,getValue: function() {return this.value;}
};const unboundGet = obj.getValue;
console.log(unboundGet()); // ?const boundGet = obj.getValue.bind(obj);
console.log(boundGet()); // ?
答案:
undefined
42
解析:直接调用函数时 this 指向全局对象(浏览器中为 window),全局对象无 value 属性。bind 方法将 this 固定为 obj。
2. 陷阱题:多次 bind 的结果
function foo() {console.log(this.name);
}const obj1 = { name: 'Alice' };
const obj2 = { name: 'Bob' };const bound = foo.bind(obj1).bind(obj2);
bound(); // 输出什么?
答案:"Alice"
解析:bind 返回的函数已经是固定 this 的函数,再次 bind 不会改变其 this 值。
3. 实现题:实现 call 方法
// 参考上文的手写实现
4. 综合题:bind 后使用 new 关键字
function Person(name) {this.name = name;
}const BoundPerson = Person.bind({}, 'Alice');
const alice = new BoundPerson();
const bob = new BoundPerson('Bob');console.log(alice.name); // ?
console.log(bob.name); // ?
答案:
"Alice"
"Alice"
解析:使用 new 操作符时,bind 预设的参数优先,传入的参数被忽略。
六、最佳实践与注意事项
-
性能考虑:
- call/apply 性能优于 bind(bind 需要创建新函数)
- 在循环中避免使用 bind
-
箭头函数:
- 箭头函数没有自己的 this,无法使用 call/apply/bind 改变
const arrowFunc = () => this; console.log(arrowFunc.call(person)); // 无效,仍指向外层 this
-
现代替代方案:
- 使用箭头函数避免 this 问题
class Button {text = 'Click me';handleClick = () => {console.log(this.text);} }
- 使用展开运算符替代 apply
Math.max(...numbers); // 替代 Math.max.apply(null, numbers)
七、总结:三剑客的核心差异
-
call 与 apply:
- 都是立即调用函数
- 区别仅在于参数传递形式
- 优先使用 call(引擎优化更好)
-
bind:
- 创建新函数,延迟执行
- 支持部分参数预设
- 在需要固定 this 的场景非常有用
“call 和 apply 是立即执行的命令,而 bind 是预设战场后等待出击的伏兵。”
理解并灵活运用 call、apply 和 bind,是掌握 JavaScript 函数执行上下文的关键。它们在以下场景中发挥着不可替代的作用:
- 对象方法借用:不同对象间共享方法
- 函数柯里化:创建预设参数的函数版本
- 构造函数继承:子类调用父类构造函数
- 数组操作:操作类数组对象
- 事件处理:绑定组件实例上下文
随着现代 JavaScript 的发展,虽然某些场景有了新的解决方案,但这三个方法仍然是 JavaScript 核心能力的重要组成部分,值得每个开发者深入掌握。