今天我们来聊聊JavaScript里的两个重要方法:call和apply。
它们是什么?为什么需要它们?
我们先看一个简单的问题。在JavaScript里,函数怎么执行?
function sayHello(name) {
console.log(`Hello, ${name}!`);
}
sayHello('小明'); // 输出:Hello, 小明!
这很直接。我们调用了函数sayHello,并传入了参数'小明'。函数里的this指向谁呢?在这个例子里,this指向全局对象(在浏览器里是window)。这没什么特别的。
但是,如果我想让函数里的this指向一个我指定的对象呢?这就是call和apply出场的时候了。
面试必挂?JavaScript中this指向的6种场景全破解
简单来说,call和apply能让你调用一个函数,并且指定这个函数内部this的指向。
先看一个例子
假设我们有一个对象person:
const person = {
name: '小红',
age: 25
};
我们还有一个函数,这个函数想使用person对象里的数据:
function introduce(company) {
console.log(`我叫${this.name},今年${this.age}岁,在${company}工作。`);
}
如果直接调用introduce('某公司'),this.name和this.age会是undefined。因为this指向全局对象,全局对象上没有这些属性。
这时候,call就派上用场了。
introduce.call(person, '某公司');
// 输出:我叫小红,今年25岁,在某公司工作。
看,我们通过.call(person, ...),做了两件事:
-
调用了
introduce函数。 -
把函数内部的
this,绑定成了person这个对象。
所以,函数里访问this.name,就相当于访问person.name。
Call 和 Apply 的语法
它们的语法非常像,只有一点点区别。
call 方法
function.call(thisArg, arg1, arg2, ...)
• thisArg:你希望函数运行时,其内部的this指向哪个对象。
• arg1, arg2, ...:传递给函数的一系列参数,用逗号分开。
apply 方法
function.apply(thisArg, [argsArray])
• thisArg:和call一样,指定this的指向。
• [argsArray]:一个数组(或类数组对象),里面包含了要传递给函数的所有参数。
它们核心功能一模一样,唯一的区别就是传参的方式不同。 call是挨个传参数,apply是把所有参数放在一个数组里传进去。
我们再用apply来调用刚才的introduce函数:
introduce.apply(person, ['某公司']);
// 输出:我叫小红,今年25岁,在某公司工作。
结果完全一样。
什么时候用 Call?什么时候用 Apply?
既然功能一样,我们怎么选呢?规则很简单:
• 当你明确知道函数需要哪些参数,并且参数数量不多时,用call更直观。
• 当函数的参数数量不确定,或者你手里已经有一个数组正好就是参数时,用apply更方便。
举例说明
场景一:借用方法
这是call和apply一个非常常见的用途。JavaScript里有一些内置方法很好用,但不是所有对象都能直接调用。
比如,数组有push方法。我们有一个类数组对象arrayLike,它想用数组的push方法给自己加一个元素。
const arrayLike = {
0: 'a',
1: 'b',
length: 2
};
// 类数组对象没有push方法,直接调用会报错
// arrayLike.push('c'); // 错误!
// 我们可以“借用”数组的push方法
Array.prototype.push.call(arrayLike, 'c');
console.log(arrayLike); // 输出:{0: 'a', 1: 'b', 2: 'c', length: 3}
这里,我们通过call,让数组的push方法在arrayLike对象上执行了一次,并且正确地修改了它的length属性。arguments对象和NodeList对象也经常这样操作。
场景二:处理不确定数量的参数(用Apply)
假设我们要找一个数组里的最大值。JavaScript提供了Math.max()方法,但它不接受数组,只接受一串数字。
const numbers = [5, 1, 8, 3, 10];
// 这样不行
// Math.max(numbers); // 返回NaN
// 传统做法:用扩展运算符
Math.max(...numbers); // 返回10
// 在ES6之前,用apply是标准做法
Math.max.apply(null, numbers); // 返回10
这里,apply的第二个参数[5, 1, 8, 3, 10],会被“展开”成Math.max(5, 1, 8, 3, 10)。第一个参数thisArg我们传了null,因为Math.max方法内部不依赖this,传什么都可以,通常传null或Math本身。
深入理解:this 的绑定
要真正用好call和apply,你必须理解JavaScript中this的绑定规则。call和apply是显式绑定的典型代表。
JavaScript中this的绑定有几种情况:
1. 默认绑定:独立函数调用,this指向全局对象(严格模式下是undefined)。
2. 隐式绑定:函数作为对象的方法调用,this指向那个对象。
3. 显式绑定:通过call、apply、bind指定this。
4. new绑定:使用new调用构造函数,this指向新创建的对象。
call和apply让我们可以强行改变一个函数的执行上下文,不受它如何被定义的限制。这给了我们极大的灵活性。
实现继承
在ES6的class语法普及之前,开发者经常用“构造函数+原型”的方式实现继承。call或apply在这里扮演了关键角色。
// 父类构造函数
function Animal(name) {
this.name = name;
this.type = '动物';
}
Animal.prototype.say = function() {
console.log(`我是一只${this.type},我叫${this.name}`);
};
// 子类构造函数
function Dog(name, breed) {
// 关键一步:调用父类构造函数,初始化父类的属性
// 这样,Dog的实例也会有name和type属性
Animal.call(this, name); // 也可以用 Animal.apply(this, [name])
this.type = '狗';
this.breed = breed;
}
// 建立原型链,让Dog继承Animal的方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// 子类自己的方法
Dog.prototype.bark = function() {
console.log(`${this.name}在汪汪叫!`);
};
const myDog = new Dog('旺财', '金毛');
myDog.say(); // 输出:我是一只狗,我叫旺财
myDog.bark(); // 输出:旺财在汪汪叫!
在Dog构造函数里,Animal.call(this, name)这一行,相当于在Dog创建的新对象(this)上,执行了一遍Animal构造函数的代码,从而拥有了name属性。这是实现属性继承的核心。
和 Bind 的简单对比
你可能还听说过bind方法。它和call、apply是“一家人”,但行为不同。
• call和apply是立即调用函数,并指定this和参数。
• bind是创建一个新的函数,这个新函数的this被永久绑定为你指定的对象,但它不会立即执行。
function multiply(a, b) {
return a * b;
}
// call 立即执行,得到结果 20
const result1 = multiply.call(null, 4, 5);
// apply 立即执行,得到结果 20
const result2 = multiply.apply(null, [4, 5]);
// bind 不执行,它返回一个新函数 double
const double = multiply.bind(null, 2); // 把第一个参数a固定为2
// 之后调用这个新函数
const result3 = double(10); // 相当于 multiply(2, 10),得到结果 20
简单记:想马上调用函数,用call或apply;想创建一个未来再调用的、固定了this的新函数,用bind。
需要注意的细节
1. 第一个参数(thisArg) :如果传入null或undefined,在非严格模式下,函数内的this会指向全局对象;在严格模式下,就真的是null或undefined。如果传入一个原始值(数字、字符串),它会被转换成对应的包装对象。
2. 性能:在现代JavaScript引擎中,三者的性能差异极小,不需要过度考虑。选择哪个主要取决于代码的清晰度和便利性。
3. 3. 箭头函数:箭头函数没有自己的this,它的this在定义时就确定了,指向外层作用域的this。因此,对箭头函数使用call、apply、bind来改变this是无效的。
const obj = { value: 42 };
const arrowFunc = () => console.log(this.value);
arrowFunc.call(obj); // 输出:undefined(或全局对象的value)
// 因为箭头函数的this在定义时就被绑定了,call无法改变它