MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript中的call、apply与bind方法

2024-11-233.9k 阅读

一、JavaScript 中的 this 指向

在深入了解 callapplybind 方法之前,我们先来回顾一下 JavaScript 中 this 的指向问题。this 的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定 this 到底指向谁,实际上 this 的最终指向的是那个调用它的对象。

1.1 全局作用域中的 this

在全局作用域中,this 指向全局对象。在浏览器环境中,全局对象是 window;在 Node.js 环境中,全局对象是 global

console.log(this === window); // true
function logThis() {
    console.log(this === window); // true
}
logThis();

1.2 函数作为对象方法调用时的 this

当函数作为对象的方法被调用时,this 指向该对象。

const person = {
    name: 'John',
    sayHello: function () {
        console.log(`Hello, I'm ${this.name}`);
    }
};
person.sayHello(); // Hello, I'm John

1.3 普通函数调用时的 this

当函数不是作为对象的方法调用时,也就是普通函数调用,在非严格模式下,this 指向全局对象(浏览器中是 window);在严格模式下,thisundefined

// 非严格模式
function sayHi() {
    console.log(this === window); // true
}
sayHi();

// 严格模式
function sayGoodbye() {
    'use strict';
    console.log(this); // undefined
}
sayGoodbye();

1.4 构造函数调用时的 this

当使用 new 关键字调用构造函数时,this 指向新创建的对象实例。

function Person(name) {
    this.name = name;
    this.sayName = function () {
        console.log(`My name is ${this.name}`);
    };
}
const tom = new Person('Tom');
tom.sayName(); // My name is Tom

二、call 方法

call 方法是 JavaScript 中所有函数都可以使用的方法,它的作用是改变函数内部 this 的指向,并立即执行该函数。

2.1 call 方法的语法

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

  • thisArg:在 function 函数运行时指定的 this 值。如果这个参数为空,在非严格模式下,则默认指向全局对象(浏览器中是 window),在严格模式下为 undefined
  • arg1, arg2, ...:传递给 function 函数的参数列表。

2.2 call 方法的示例

  1. 改变函数内部 this 指向
const person1 = {
    name: 'Alice',
    sayHello: function () {
        console.log(`Hello, I'm ${this.name}`);
    }
};

const person2 = {
    name: 'Bob'
};

person1.sayHello.call(person2); // Hello, I'm Bob

在这个例子中,原本 person1.sayHello 函数内部的 this 指向 person1,通过 call 方法,将 this 指向了 person2,所以输出的是 Bob

  1. 实现继承
function Animal(name) {
    this.name = name;
    this.speak = function () {
        console.log(`${this.name} makes a sound.`);
    };
}

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

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // Buddy makes a sound.
console.log(myDog.breed); // Golden Retriever

这里通过 call 方法,在 Dog 构造函数内部调用 Animal 构造函数,从而实现了继承。Animal 构造函数中的 this 被指向了新创建的 Dog 实例,这样 myDog 就拥有了 Animal 的属性和方法。

2.3 call 方法的原理

我们可以手动模拟实现一个 call 方法来深入理解它的原理。

Function.prototype.myCall = function (thisArg, ...args) {
    if (typeof this!== 'function') {
        throw new TypeError('this is not a function');
    }
    thisArg = thisArg || window;
    const fnSymbol = Symbol('fn');
    thisArg[fnSymbol] = this;
    const result = thisArg[fnSymbol](...args);
    delete thisArg[fnSymbol];
    return result;
};
  1. 首先检查调用 myCall 的是否是一个函数,如果不是则抛出错误。
  2. 确定 thisArg,如果没有传入则默认指向全局对象。
  3. 使用一个唯一的 Symbol 作为属性名,将调用 myCall 的函数挂载到 thisArg 上。
  4. 通过 thisArg[fnSymbol](...args) 执行函数,并传递参数,获取执行结果。
  5. 删除挂载在 thisArg 上的函数,避免污染 thisArg
  6. 返回函数执行结果。

三、apply 方法

apply 方法和 call 方法类似,也是用来改变函数内部 this 的指向并立即执行函数,不同之处在于 apply 方法接收的第二个参数是一个数组(或者类数组对象)。

3.1 apply 方法的语法

function.apply(thisArg, [argsArray])

  • thisArg:在 function 函数运行时指定的 this 值。如果这个参数为空,在非严格模式下,则默认指向全局对象(浏览器中是 window),在严格模式下为 undefined
  • argsArray:一个数组或者类数组对象,其中的元素将作为单独的参数传给 function 函数。

3.2 apply 方法的示例

  1. 改变函数内部 this 指向并传递数组参数
function sum(a, b) {
    return a + b;
}

const numbers = [2, 3];
const result = sum.apply(null, numbers);
console.log(result); // 5

这里 sum 函数原本期望两个参数,通过 apply 方法,将数组 numbers 中的元素作为参数传递给 sum 函数,并且由于第一个参数为 null,在非严格模式下,this 指向全局对象 window

  1. 使用 apply 方法求数组中的最大值
const numbersArray = [1, 5, 3, 9, 4];
const max = Math.max.apply(null, numbersArray);
console.log(max); // 9

Math.max 方法接受多个参数,通过 apply 方法,我们可以将数组展开作为参数传递给它,从而求出数组中的最大值。

3.3 apply 方法的原理

同样,我们可以模拟实现一个 apply 方法。

Function.prototype.myApply = function (thisArg, argsArray) {
    if (typeof this!== 'function') {
        throw new TypeError('this is not a function');
    }
    thisArg = thisArg || window;
    const fnSymbol = Symbol('fn');
    thisArg[fnSymbol] = this;
    let result;
    if (argsArray) {
        result = thisArg[fnSymbol](...argsArray);
    } else {
        result = thisArg[fnSymbol]();
    }
    delete thisArg[fnSymbol];
    return result;
};
  1. 首先检查调用 myApply 的是否是一个函数,如果不是则抛出错误。
  2. 确定 thisArg,如果没有传入则默认指向全局对象。
  3. 使用一个唯一的 Symbol 作为属性名,将调用 myApply 的函数挂载到 thisArg 上。
  4. 判断是否有 argsArray,如果有则展开数组作为参数执行函数,否则直接执行函数,获取执行结果。
  5. 删除挂载在 thisArg 上的函数,避免污染 thisArg
  6. 返回函数执行结果。

四、bind 方法

bind 方法也是用来改变函数内部 this 的指向,但与 callapply 不同的是,bind 方法不会立即执行函数,而是返回一个新的函数,这个新函数内部的 this 被绑定到指定的值。

4.1 bind 方法的语法

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

  • thisArg:在返回的新函数中,将 this 绑定到该值。如果这个参数为空,在非严格模式下,则默认指向全局对象(浏览器中是 window),在严格模式下为 undefined
  • arg1, arg2, ...:(可选)预定义的参数,这些参数将被前置到新函数被调用时传入的参数之前。

4.2 bind 方法的示例

  1. 绑定 this 指向并返回新函数
const person = {
    name: 'Charlie',
    sayHello: function () {
        console.log(`Hello, I'm ${this.name}`);
    }
};

const boundSayHello = person.sayHello.bind({ name: 'David' });
boundSayHello(); // Hello, I'm David

这里通过 bind 方法,将 person.sayHello 函数内部的 this 绑定到一个新的对象 { name: 'David' },并返回一个新函数 boundSayHello,调用新函数时输出的是 David

  1. 使用 bind 方法预定义参数
function multiply(a, b) {
    return a * b;
}

const double = multiply.bind(null, 2);
console.log(double(5)); // 10

这里通过 bind 方法,将 multiply 函数的第一个参数预定义为 2,返回一个新函数 double,调用 double 时只需要传入第二个参数即可。

4.3 bind 方法的原理

下面是模拟实现 bind 方法的代码。

Function.prototype.myBind = function (thisArg, ...bindArgs) {
    if (typeof this!== 'function') {
        throw new TypeError('this is not a function');
    }
    const self = this;
    return function (...args) {
        if (this instanceof self) {
            return new self(...bindArgs, ...args);
        }
        return self.apply(thisArg, bindArgs.concat(args));
    };
};
  1. 首先检查调用 myBind 的是否是一个函数,如果不是则抛出错误。
  2. 保存调用 myBind 的函数 self
  3. 返回一个新函数,在新函数内部:
    • 如果新函数是通过 new 关键字调用的(即 this instanceof selftrue),说明是作为构造函数调用,那么使用 new 关键字创建新实例,并将 bindArgs 和新函数调用时传入的 args 作为参数传递给原函数 self
    • 如果不是通过 new 关键字调用的,则使用 apply 方法改变 self 内部 this 的指向为 thisArg,并将 bindArgs 和新函数调用时传入的 args 作为参数传递给 self

五、call、apply 与 bind 方法的区别

  1. 执行时机

    • callapply 方法会立即执行函数。
    • bind 方法不会立即执行函数,而是返回一个新的函数,需要调用这个新函数才会执行。
  2. 参数传递方式

    • call 方法接受多个参数,第一个参数是 this 的指向,后面的参数依次传递给函数。
    • apply 方法接受两个参数,第一个参数是 this 的指向,第二个参数是一个数组(或类数组对象),数组中的元素作为函数的参数。
    • bind 方法第一个参数是 this 的指向,后面可以跟任意数量的参数,这些参数会被前置到新函数调用时传入的参数之前。
  3. 返回值

    • callapply 方法返回的是函数执行的结果。
    • bind 方法返回的是一个新的函数。
  4. 对原函数的影响

    • callapply 方法不会改变原函数本身,只是改变函数执行时 this 的指向并执行函数。
    • bind 方法同样不会改变原函数本身,而是返回一个新函数,新函数内部的 this 被绑定到指定的值。

六、实际应用场景

  1. 继承 在实现继承时,callapply 方法常用于在子类构造函数中调用父类构造函数,以实现属性和方法的继承。例如前面提到的 AnimalDog 的例子。

  2. 函数借用 当一个对象没有某个方法,但其他对象有这个方法时,可以使用 callapply 方法借用其他对象的方法。

const arrayLike = { 0: 1, 1: 2, length: 2 };
const sum = Array.prototype.reduce.call(arrayLike, (acc, cur) => acc + cur, 0);
console.log(sum); // 3

这里 arrayLike 并不是一个真正的数组,但通过 call 方法借用了 Array.prototype.reduce 方法来计算其元素的总和。

  1. 事件处理函数中的 this 绑定 在事件处理函数中,this 的指向可能不符合我们的预期,这时可以使用 bind 方法来确保 this 指向我们期望的对象。
const button = document.getElementById('myButton');
const person = {
    name: 'Eve',
    clickHandler: function () {
        console.log(`${this.name} clicked the button.`);
    }
};
button.addEventListener('click', person.clickHandler.bind(person));

这样在按钮点击时,clickHandler 函数内部的 this 就会指向 person 对象。

  1. 柯里化 bind 方法可以用于实现柯里化。柯里化是一种将多参数函数转换为一系列单参数函数的技术。
function add(a, b, c) {
    return a + b + c;
}

const curriedAdd = add.bind(null, 1);
const result1 = curriedAdd(2, 3);
const curriedAdd2 = curriedAdd.bind(null, 2);
const result2 = curriedAdd2(3);
console.log(result1); // 6
console.log(result2); // 6

这里通过 bind 方法逐步固定 add 函数的参数,实现了柯里化。

七、总结与注意事项

  1. 总结

    • callapplybind 方法是 JavaScript 中非常强大的工具,它们允许我们灵活地控制函数内部 this 的指向,从而实现继承、函数借用、事件处理等多种功能。
    • callapply 方法用于立即执行函数并改变 this 指向,区别在于参数传递方式;bind 方法用于返回一个新函数,新函数内部的 this 被绑定到指定的值,且可以预定义部分参数。
  2. 注意事项

    • 在使用 callapplybind 方法时,要注意 thisArg 参数的取值,特别是在严格模式和非严格模式下的不同表现。
    • 当使用 bind 方法返回的新函数作为构造函数时,this 的绑定会失效,新函数内部的 this 会指向新创建的实例,这是需要特别注意的地方。
    • 在传递参数时,要根据 callapplybind 方法不同的参数要求进行正确的传递,避免出现参数错误的情况。

通过深入理解和熟练运用 callapplybind 方法,我们能够更加灵活地编写 JavaScript 代码,提高代码的复用性和可维护性。希望本文的介绍和示例能帮助你更好地掌握这些重要的方法。