JavaScript中的Call和Apply看这篇文章就够了

4 阅读6分钟

今天我们来聊聊JavaScript里的两个重要方法:callapply

它们是什么?为什么需要它们?

我们先看一个简单的问题。在JavaScript里,函数怎么执行?

function sayHello(name) {
  console.log(`Hello, ${name}!`);
}

sayHello('小明'); // 输出:Hello, 小明!

这很直接。我们调用了函数sayHello,并传入了参数'小明'。函数里的this指向谁呢?在这个例子里,this指向全局对象(在浏览器里是window)。这没什么特别的。

但是,如果我想让函数里的this指向一个我指定的对象呢?这就是callapply出场的时候了。 面试必挂?JavaScript中this指向的6种场景全破解

简单来说,callapply能让你调用一个函数,并且指定这个函数内部this的指向。

先看一个例子

假设我们有一个对象person

const person = {
  name: '小红',
  age: 25
};

我们还有一个函数,这个函数想使用person对象里的数据:

function introduce(company) {
  console.log(`我叫${this.name},今年${this.age}岁,在${company}工作。`);
}

如果直接调用introduce('某公司')this.namethis.age会是undefined。因为this指向全局对象,全局对象上没有这些属性。

这时候,call就派上用场了。

introduce.call(person, '某公司');
// 输出:我叫小红,今年25岁,在某公司工作。

看,我们通过.call(person, ...),做了两件事:

  1. 调用了introduce函数。

  2. 把函数内部的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更方便。

举例说明

场景一:借用方法

这是callapply一个非常常见的用途。JavaScript里有一些内置方法很好用,但不是所有对象都能直接调用。

比如,数组有push方法。我们有一个类数组对象arrayLike,它想用数组的push方法给自己加一个元素。

const arrayLike = {
  0'a',
  1'b',
  length2
};

// 类数组对象没有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 = [518310];

// 这样不行
// 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,传什么都可以,通常传nullMath本身。

深入理解:this 的绑定

要真正用好callapply,你必须理解JavaScript中this的绑定规则。callapply显式绑定的典型代表。

JavaScript中this的绑定有几种情况:

1. 默认绑定:独立函数调用,this指向全局对象(严格模式下是undefined)。

2. 隐式绑定:函数作为对象的方法调用,this指向那个对象。

3. 显式绑定:通过callapplybind指定this

4. new绑定:使用new调用构造函数,this指向新创建的对象。

callapply让我们可以强行改变一个函数的执行上下文,不受它如何被定义的限制。这给了我们极大的灵活性。

实现继承

在ES6的class语法普及之前,开发者经常用“构造函数+原型”的方式实现继承。callapply在这里扮演了关键角色。

// 父类构造函数
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方法。它和callapply是“一家人”,但行为不同。

• callapply立即调用函数,并指定this和参数。

• bind创建一个新的函数,这个新函数的this被永久绑定为你指定的对象,但它不会立即执行。

function multiply(a, b) {
  return a * b;
}

// call 立即执行,得到结果 20
const result1 = multiply.call(null45);

// apply 立即执行,得到结果 20
const result2 = multiply.apply(null, [45]);

// bind 不执行,它返回一个新函数 double
const double = multiply.bind(null2); // 把第一个参数a固定为2
// 之后调用这个新函数
const result3 = double(10); // 相当于 multiply(2, 10),得到结果 20

简单记:想马上调用函数,用callapply;想创建一个未来再调用的、固定了this的新函数,用bind

需要注意的细节

1. 第一个参数(thisArg) :如果传入nullundefined,在非严格模式下,函数内的this会指向全局对象;在严格模式下,就真的是nullundefined。如果传入一个原始值(数字、字符串),它会被转换成对应的包装对象。

2. 性能:在现代JavaScript引擎中,三者的性能差异极小,不需要过度考虑。选择哪个主要取决于代码的清晰度和便利性。 3. 3. 箭头函数:箭头函数没有自己的this,它的this在定义时就确定了,指向外层作用域的this。因此,对箭头函数使用callapplybind来改变this是无效的。

const obj = { value: 42 };
const arrowFunc = () => console.log(this.value);

arrowFunc.call(obj); // 输出:undefined(或全局对象的value)
// 因为箭头函数的this在定义时就被绑定了,call无法改变它