一文彻底搞懂 call/apply/bind:从原理到手写实现,面试再也不怕
call、apply、bind是 JavaScript 中最核心的三个方法,也是面试的必考题。它们的唯一作用就是改变函数执行时的this指向,但很多人只知道怎么用,却不知道它们的底层原理和区别。本文将从this 绑定规则→核心用法→底层原理→手写实现→真实应用五个维度,带你彻底搞懂这三个方法,让你不仅会用,更能在面试中从容应对所有相关问题。
一、为什么我们需要改变 this 指向?
在讲这三个方法之前,我们必须先搞懂一个前提:JavaScript 中的 this 是动态绑定的,不是在函数定义时确定,而是在函数调用时确定。
这是 JavaScript 最灵活也最容易踩坑的特性。很多时候,函数执行时的 this 并不是我们预期的那个值:
const person = {
name: '张三',
sayHello() {
console.log(`你好,我是${this.name}`);
}
};
// 正常调用:this 指向 person
person.sayHello(); // "你好,我是张三"
// 把方法赋值给变量后调用:this 丢失了
const say = person.sayHello;
say(); // "你好,我是undefined" ❌
上面的例子中,当我们把 person.sayHello 赋值给变量 say 后再调用,this 就不再指向 person 了,而是指向了全局对象(浏览器中是 window)。
call、apply、bind 就是为了解决这个问题而生的:它们允许我们手动指定函数执行时的 this 指向。
二、先回顾:this 的四种绑定规则
call、apply、bind 属于显式绑定,是四种 this 绑定规则中优先级最高的(仅次于 new 绑定)。
| 绑定规则 | 说明 | this 指向 |
|---|---|---|
| 默认绑定 | 函数独立调用 | 全局对象(严格模式下是 undefined) |
| 隐式绑定 | 作为对象的方法调用 | 调用方法的对象 |
| 显式绑定 | 使用 call/apply/bind 调用 | 手动指定的对象 |
| new 绑定 | 使用 new 关键字调用 | 新创建的实例对象 |
优先级:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
三、call:立即执行函数并绑定 this
1. 核心定义
call 方法会立即执行函数,并将函数的 this 指向第一个参数,后续参数作为函数的参数逐个传递。
2. 基本语法
function.call(thisArg, arg1, arg2, ...);
thisArg:函数执行时的this指向arg1, arg2, ...:传递给函数的参数,逐个传递- 返回值:函数执行后的返回值
3. 基本用法
function sayHello(greeting, punctuation) {
console.log(`${greeting},我是${this.name}${punctuation}`);
}
const person = { name: '张三' };
// 调用 sayHello,将 this 绑定到 person
sayHello.call(person, '你好', '!'); // "你好,我是张三!"
4. 特殊情况
- 如果
thisArg是null或undefined,在非严格模式下会自动替换为全局对象(window) - 如果
thisArg是基本类型(数字、字符串、布尔),会自动转换为对应的包装对象
// 非严格模式下,this 指向 window
sayHello.call(null, '你好', '!'); // "你好,我是undefined"
// 基本类型会被包装成对象
function showThis() {
console.log(this);
}
showThis.call(123); // Number {123}
四、apply:立即执行函数并绑定 this(数组传参)
1. 核心定义
apply 方法和 call 方法几乎完全相同,唯一的区别是:
call是逐个传递参数apply是将参数放在一个数组或类数组中传递
类数组对象 = 有数字下标 + 有 length属性 + 不是数组 + 不能用数组方法
- 必须拥有非负整数的数字索引属性:属性名必须是 0、1、2、3... 这样的非负整数(字符串形式的数字也可以,如 "0"、"1")
- 必须拥有 length 属性:值为非负整数,表示元素的个数
- 不能是数组本身:obj instanceof Array 必须返回 false
- 不继承数组原型方法:没有 push、pop、forEach、map 等数组专属方法
2. 基本语法
function.apply(thisArg, [argsArray]);
thisArg:函数执行时的this指向argsArray:传递给函数的参数数组或类数组- 返回值:函数执行后的返回值
3. 基本用法
function sayHello(greeting, punctuation) {
console.log(`${greeting},我是${this.name}${punctuation}`);
}
const person = { name: '张三' };
// 用数组传递参数
sayHello.apply(person, ['你好', '!']); // "你好,我是张三!"
4. 经典应用场景:数组方法借用
这是 apply 最常用的场景,因为它可以将数组展开成逐个参数:
// 求数组的最大值
const arr = [1, 3, 5, 2, 4];
// Math.max 不接受数组参数,只接受逐个参数
console.log(Math.max.apply(null, arr)); // 5 ✅
// 等价于
console.log(Math.max(1, 3, 5, 2, 4)); // 5
五、bind:返回新函数并永久绑定 this
1. 核心定义
bind 方法和前两者最大的区别是:它不会立即执行函数,而是返回一个新的函数,这个新函数的 this 会被永久绑定到第一个参数,后续参数会作为新函数的预置参数。
2. 基本语法
const newFunction = function.bind(thisArg, arg1, arg2, ...);
thisArg:新函数执行时的this指向arg1, arg2, ...:预置参数,会在调用新函数时先传递- 返回值:一个新的绑定了 this 的函数
3. 基本用法
function sayHello(greeting, punctuation) {
console.log(`${greeting},我是${this.name}${punctuation}`);
}
const person = { name: '张三' };
// 返回一个新函数,this 永久绑定到 person
const boundSayHello = sayHello.bind(person);
// 调用新函数
boundSayHello('你好', '!'); // "你好,我是张三!"
// 也可以预置参数
const boundSayHi = sayHello.bind(person, '嗨');
boundSayHi('~'); // "嗨,我是张三~"
4. 重要特性:永久绑定
bind 绑定的 this 是永久的,一旦绑定就无法再被 call 或 apply 改变:
const person1 = { name: '张三' };
const person2 = { name: '李四' };
const boundSayHello = sayHello.bind(person1);
boundSayHello('你好', '!'); // "你好,我是张三!"
// 尝试用 call 改变 this,无效
boundSayHello.call(person2, '你好', '!'); // "你好,我是张三!" ❌
六、三者核心区别对比表
| 对比维度 | call | apply | bind |
|---|---|---|---|
| 执行时机 | 立即执行 | 立即执行 | 不立即执行,返回新函数 |
| 参数传递 | 逐个传递 | 数组/类数组传递 | 逐个传递,支持预置参数 |
| this 绑定 | 临时绑定 | 临时绑定 | 永久绑定,无法改变 |
| 返回值 | 函数执行结果 | 函数执行结果 | 新的绑定函数 |
| 适用场景 | 大多数场景 | 需要数组传参的场景 | 回调函数、事件处理、预置参数 |
七、最常见的 5 个应用场景
1. 借用其他对象的方法(最经典)
这是 call 和 apply 最常用的场景,可以让一个对象借用另一个对象的方法:
// 类数组对象借用数组的 slice 方法
function sum() {
// arguments 是类数组,没有 slice 方法
const args = Array.prototype.slice.call(arguments);
return args.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
2. 改变回调函数的 this 指向
在定时器、事件监听器等回调函数中,this 经常会丢失,用 bind 可以解决:
const person = {
name: '张三',
sayHello() {
console.log(`你好,我是${this.name}`);
}
};
// ❌ 错误:this 指向 window
setTimeout(person.sayHello, 1000); // "你好,我是undefined"
// ✅ 正确:用 bind 绑定 this
setTimeout(person.sayHello.bind(person), 1000); // "你好,我是张三"
3. 实现继承
在 ES6 之前,我们可以用 call 来实现构造函数的继承:
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(name, age, grade) {
// 调用父类构造函数,绑定 this 到子类实例
Person.call(this, name, age);
this.grade = grade;
}
const student = new Student('张三', 18, '高三');
console.log(student.name); // "张三"
console.log(student.age); // 18
console.log(student.grade); // "高三"
4. 函数柯里化
利用 bind 的预置参数特性,可以实现函数柯里化:
function add(a, b) {
return a + b;
}
// 预置第一个参数为 5,得到一个新函数
const add5 = add.bind(null, 5);
console.log(add5(3)); // 8
console.log(add5(7)); // 12
5. 绑定事件处理函数的 this
在 React 类组件中,我们经常需要用 bind 来绑定事件处理函数的 this:
class Button extends React.Component {
constructor(props) {
super(props);
// 在构造函数中绑定 this
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this); // 指向组件实例
}
render() {
return <button onClick={this.handleClick}>点击我</button>;
}
}
八、手写实现 call/apply/bind(面试必考题)
手写实现这三个方法是前端面试的高频考点,下面我们一步步实现它们的完善版本。
1. 手写 call
Function.prototype.myCall = function(thisArg, ...args) {
// 1. 处理 thisArg 为 null/undefined 的情况
thisArg = thisArg || window;
// 2. 创建一个唯一的 Symbol 属性,避免覆盖原有属性
const key = Symbol();
// 3. 将当前函数(this)挂载到 thisArg 上
thisArg[key] = this;
// 4. 执行函数并获取结果
const result = thisArg[key](...args);
// 5. 删除临时属性
delete thisArg[key];
// 6. 返回结果
return result;
};
// 测试
const person = { name: '张三' };
function sayHello(greeting) {
return `${greeting},我是${this.name}`;
}
console.log(sayHello.myCall(person, '你好')); // "你好,我是张三"
2. 手写 apply
apply 和 call 几乎一样,只是参数处理不同:
Function.prototype.myApply = function(thisArg, argsArray) {
thisArg = thisArg || window;
const key = Symbol();
thisArg[key] = this;
// 处理参数数组
const result = argsArray ? thisArg[key](...argsArray) : thisArg[key]();
delete thisArg[key];
return result;
};
// 测试
console.log(sayHello.myApply(person, ['你好'])); // "你好,我是张三"
3. 手写 bind
bind 是最复杂的,需要处理永久绑定、预置参数和 new 绑定的情况:
Function.prototype.myBind = function(thisArg, ...boundArgs) {
const fn = this;
// 创建一个中间函数,用来处理原型链
//隔离原型:修改 boundFn.prototype 不会影响原函数的原型
const Fn = function() {};
const boundFn = function(...args) {
// 如果是 new 调用,this 指向新创建的实例
// 否则指向绑定的 thisArg
const context = this instanceof Fn ? this : thisArg;
// 合并预置参数和调用时的参数
return fn.apply(context, [...boundArgs, ...args]);
};
// 维护原型链
Fn.prototype = fn.prototype;
boundFn.prototype = new Fn();
//new boundFn() → boundFn.prototype → new Fn() → Fn.prototype → 原函数.prototype
return boundFn;
};
// 测试
const boundSayHello = sayHello.myBind(person);
console.log(boundSayHello('你好')); // "你好,我是张三"
九、常见误区与注意事项
1. 误区1:bind 可以多次绑定
错! bind 绑定的 this 是永久的,多次绑定只有第一次有效:
const fn = function() { console.log(this.name); };
const bound1 = fn.bind({ name: '张三' });
const bound2 = bound1.bind({ name: '李四' });
bound2(); // "张三" ❌ 不是李四
2. 误区2:箭头函数可以被 call/apply/bind 改变 this
错! 箭头函数没有自己的 this,它的 this 继承自外层作用域,无法被 call/apply/bind 改变:
const arrowFn = () => console.log(this);
arrowFn.call({ name: '张三' }); // window ❌
3. 误区3:apply 的第二个参数可以是任何值
错! apply 的第二个参数必须是数组或类数组对象,否则会抛出 TypeError:
sayHello.apply(person, '你好'); // ❌ TypeError: CreateListFromArrayLike called on non-object
4. 误区4:call/apply 会改变原函数的 this
错! call 和 apply 只是在本次调用时临时改变 this,不会影响原函数本身:
const fn = function() { console.log(this); };
fn.call({ name: '张三' }); // { name: '张三' }
fn(); // window ✅ 原函数的 this 没有改变
十、面试高频考点总结
-
call、apply、bind 的区别是什么?
call和apply立即执行函数,bind返回新函数call逐个传参,apply数组传参bind永久绑定 this,call和apply临时绑定
-
如何实现 call/apply/bind? 参考上面的手写实现,重点掌握
call和bind的实现。 -
bind 多次绑定会怎么样? 只有第一次绑定有效,后续绑定会被忽略。
-
箭头函数可以被 call/apply/bind 改变 this 吗? 不可以,箭头函数的 this 继承自外层作用域,无法被改变。
-
apply 的第二个参数有什么要求? 必须是数组或类数组对象。
-
new 绑定和 bind 绑定哪个优先级更高?
new绑定优先级更高,当用new调用 bind 绑定的函数时,this会指向新创建的实例。
写在最后
call、apply、bind 是 JavaScript 函数式编程的基础,也是理解 this 指向的关键。掌握它们不仅能让你写出更优雅、更健壮的代码,更是面试中脱颖而出的必备技能。
希望这篇文章能帮你彻底搞懂这三个方法,让你在实际开发和面试中都能游刃有余。
如果觉得这篇文章对你有帮助,欢迎点赞、收藏、关注,有任何问题可以在评论区留言讨论!