【js面试题】一文搞懂JavaScript 中 call、apply、bind 的深度解析与区别

732 阅读7分钟

在 JavaScript 的函数世界里,call、apply 和 bind 是三个极为重要且功能强大的方法,它们都与函数的上下文(this)绑定以及参数传递有着紧密的联系。深入理解它们之间的区别与用法,对于编写灵活、高效且易于维护的 JavaScript 代码至关重要。本文将对这三个方法进行全面而深入的剖析。

一、基本概念与语法

(一)call 方法

call 方法允许一个函数以指定的 this 值和单独给出的参数来调用。其语法如下:

function.call(thisArg[, arg1[, arg2[,...]]])

其中,thisArg 是在函数执行时被绑定为 this 的对象,而 arg1、arg2 等则是传递给函数的参数。例如:

function greet(name) {
    console.log('Hello,'+ name + ', I am'+ this.role);
}
var person = {
    role: 'developer'
};

greet.call(person, 'John');
// 输出:Hello, John, I am developer

在这个例子中,greet 函数原本的 this 指向全局对象(在非严格模式下),但通过 call 方法,将 this 绑定为 person 对象,并传递了 'John' 作为参数,从而使得函数内部能够访问到 person 对象的 role 属性。

(二)apply 方法

apply 方法与 call 方法类似,它也用于改变函数的 this 指向并调用函数,但它接收参数的方式略有不同。apply 的语法如下:

function.apply(thisArg, [argsArray])

这里,thisArg 的含义与 call 方法中的相同,而 argsArray 是一个包含函数参数的数组。例如:

function sum(a, b) {
    return a + b;
}

var numbers = [3, 5];

var result = sum.apply(null, numbers);
console.log('result:',result)
// 输出:8

在这个例子中,sum 函数需要两个参数,通过 apply 方法,将 null 作为 this 值(因为 sum 函数内部并不依赖 this),并将 numbers 数组作为参数传递给 sum 函数,实现了数组元素作为函数参数的传递。

(三)bind 方法

bind 方法创建一个新的函数,在这个新函数被调用时,this 值会被绑定到指定的对象,并且可以预先传递部分参数。其语法如下:

function.bind(thisArg[, arg1[, arg2[,...]]])

例如:

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

var double = multiply.bind(null, 2);
// 创建了一个新函数 double,它将第一个参数固定为 2

var result = double(3);
// 输出:6

在这个例子中,bind 方法创建了一个新的函数 double,它将 multiply 函数的第一个参数绑定为 2。当调用 double 函数时,只需要传递第二个参数即可。

二、this 绑定的区别

(一)call 和 apply 的 this 绑定

call 和 apply 方法在调用函数时立即执行函数,并将指定的对象绑定为 this。它们的区别主要在于参数传递方式,call 是逐个传递参数,而 apply 是将参数封装在一个数组中传递。例如:

function showFullName(firstName, lastName) {
    console.log(this.name +'' + firstName +'' + lastName);
}

var person = {
    name: 'Alice'
};

// 使用 call 方法
showFullName.call(person, 'Bob', 'Smith');
// 输出:Alice Bob Smith

// 使用 apply 方法
showFullName.apply(person, ['Charlie', 'Brown']);
// 输出:Alice Charlie Brown

在这两个例子中,showFullName 函数的 this 都被绑定到了 person 对象,但参数传递方式不同。

(二)bind 的 this 绑定

bind 方法并不立即执行函数,而是返回一个新的函数,新函数在被调用时 this 才会被绑定到指定的对象。例如:

function greet() {
    console.log('Hello, I am'+ this.name);
}

var person1 = {
    name: 'David'
};

var person2 = {
    name: 'Emma'
};

var greetPerson1 = greet.bind(person1);
var greetPerson2 = greet.bind(person2);

greetPerson1();
// 输出:Hello, I am David
greetPerson2();
// 输出:Hello, I am Emma

在这个例子中,greet 函数通过 bind 方法分别绑定到了 person1 和 person2 对象,创建了两个新的函数 greetPerson1 和 greetPerson2,当调用这两个新函数时,this 分别指向对应的对象。

三、参数传递的区别

(一)call 的参数传递

call 方法按照参数的顺序逐个传递给被调用的函数。例如:

function addNumbers(a, b, c) {
    return a + b + c;
}

var result = addNumbers.call(null, 1, 2, 3);
// 输出:6

在这个例子中,1、2、3 分别作为 a、b、c 参数传递给 addNumbers 函数。

(二)apply 的参数传递

apply 方法将所有参数封装在一个数组中传递给函数。例如:

function addNumbers(a, b, c) {
    return a + b + c;
}

var numbers = [4, 5, 6];
var result = addNumbers.apply(null, numbers);
// 输出:15

在这个例子中,numbers 数组中的元素作为参数传递给 addNumbers 函数。

(三)bind 的参数传递

bind 方法可以预先传递部分参数,这些参数会在新函数创建时被固定,后续调用新函数时再传递剩余的参数。例如:

function subtract(a, b) {
    return a - b;
}

var subtract5 = subtract.bind(null, 5);
var result = subtract5(3);
// 输出:2

在这个例子中,bind 方法将 subtract 函数的第一个参数绑定为 5,创建了 subtract5 新函数,当调用 subtract5 函数时,只需要传递第二个参数 3 即可。

四、应用场景的区别

(一)call 和 apply 的应用场景

  1. 借用方法

当一个对象没有某个方法,但其他对象有该方法时,可以使用 call 或 apply 方法来借用。例如:

var arrayLike = {
    0: 'apple',
    1: 'banana',
    length: 2
};

// 借用数组的 push 方法
Array.prototype.push.call(arrayLike, 'cherry');
console.log(arrayLike);
// 输出:{ 0: 'apple', 1: 'banana', 2: 'cherry', length: 3 }

在这个例子中,arrayLike 不是一个真正的数组,但通过 call 方法借用了数组的 push 方法,实现了向 arrayLike 对象添加元素的功能。

  1. 继承

在模拟传统的基于原型的继承时,可以使用 call 或 apply 方法来调用父类的构造函数并传递参数。例如:

function Animal(name) {
    this.name = name;
}

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

var dog = new Dog('Max', 'Labrador');
console.log(dog);
// 输出:{ name: 'Max', breed: 'Labrador' }

在这个例子中,Dog 构造函数通过 call 方法调用了 Animal 构造函数,将 name 参数传递给父类构造函数,实现了继承的部分功能。

(二)bind 的应用场景

  1. 事件处理程序绑定

在事件处理程序中,可以使用 bind 方法来确保函数内部的 this 指向正确的对象。例如:

var button = document.getElementById('myButton');
var person = {
    name: 'John',
    handleClick: function() {
        console.log('Button clicked by'+ this.name);
    }
};

button.addEventListener('click', person.handleClick.bind(person));

在这个例子中,bind 方法将 person.handleClick 函数的 this 绑定为 person 对象,这样在点击按钮时,handleClick 函数内部的 this 就能正确地访问到 person 对象的属性。

  1. 函数柯里化

bind 方法可以用于实现函数柯里化,即预先固定部分参数,返回一个新的函数等待接收剩余参数。例如:

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

var doubleAndMultiplyBy3 = multiply.bind(null, 2, 3);
var result = doubleAndMultiplyBy3(4);
console.log('result:',result)
// 输出:24

在这个例子中,bind 方法创建了一个新函数 doubleAndMultiplyBy3,它预先将 multiply 函数的前两个参数固定为 2 和 3,当调用 doubleAndMultiplyBy3 函数时,只需要传递第三个参数 4 即可。

五、总结

在 JavaScript 中,call、apply 和 bind 这三个方法虽然都与函数的 this 绑定和参数传递有关,但它们在功能和使用方式上存在着明显的区别。call 和 apply 主要用于立即调用函数并改变 this 指向,它们的区别仅在于参数传递方式,call 逐个传递参数,apply 以数组形式传递参数。而 bind 方法则是创建一个新的函数,在新函数被调用时才进行 this 绑定,并且可以预先固定部分参数。在实际编程中,根据不同的需求可以灵活选择使用这三个方法。如果需要立即调用函数并改变 this 指向且参数明确,call 或 apply 可能更合适;如果需要创建一个新函数并在未来某个时候调用,且希望预先固定部分参数或者确保 this 指向特定对象,那么 bind 方法则是更好的选择。深入理解它们的区别和应用场景,能够让我们在 JavaScript 函数操作中更加游刃有余,编写出更加高效、灵活且易于维护的代码,提升我们的编程能力和代码质量。