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

JavaScript中的bind、call与apply方法

2021-12-257.2k 阅读

JavaScript 中的函数上下文

在深入探讨 bindcallapply 方法之前,我们首先需要理解 JavaScript 中的函数上下文(也称为 this 关键字)这一概念。

在 JavaScript 中,this 关键字的值取决于函数的调用方式。它不像其他一些编程语言那样有固定的绑定规则,而是在运行时动态确定的。

全局上下文中的 this

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

console.log(this === window); // 在浏览器中为 true
console.log(this);

函数作为方法调用时的 this

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

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

person.sayHello(); // 输出: Hello, I'm John

普通函数调用时的 this

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

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

// 非严格模式
sayHello(); // 输出: Hello, undefined(因为全局对象中没有 name 属性)

// 严格模式
function strictSayHello() {
  'use strict';
  console.log(`Hello, ${this.name}`);
}

strictSayHello(); // 报错: Cannot read property 'name' of undefined

构造函数调用时的 this

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

function Person(name) {
  this.name = name;
  this.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
  };
}

const john = new Person('John');
john.sayHello(); // 输出: Hello, I'm John

理解了函数上下文的基本概念后,我们就可以更好地理解 bindcallapply 方法的作用了。它们的主要目的就是帮助我们更灵活地控制函数调用时 this 的指向。

call 方法

call 方法是 JavaScript 中函数对象的一个方法,它允许我们调用一个函数,并将指定的对象作为函数执行时的 this 值,同时可以传递一系列参数。

语法

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

  • thisArg:在 function 函数运行时指定的 this 值。如果该参数为 nullundefined,在非严格模式下,this 将指向全局对象,在严格模式下,this 将为 nullundefined
  • arg1, arg2, ..., argN:传递给 function 函数的参数列表。

示例

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

const anotherPerson = {
  name: 'Jane'
};

// 使用 call 方法调用 sayHello 方法,并将 anotherPerson 作为 this 值
person.sayHello.call(anotherPerson, 'Hi'); // 输出: Hi, I'm Jane

在上述示例中,person.sayHello 函数通常在 person 对象的上下文中调用,this 指向 person。但通过 call 方法,我们将 this 的指向改为了 anotherPerson,同时传递了一个参数 'Hi'

call 方法的实际应用

  1. 继承:在 JavaScript 中,我们可以使用 call 方法来实现对象间的继承。
function Animal(name) {
  this.name = name;
  this.speak = function() {
    console.log(`${this.name} makes a sound.`);
  };
}

function Dog(name, breed) {
  // 使用 call 方法调用 Animal 构造函数,并将 this 指向 Dog 的实例
  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

在这个例子中,Dog 构造函数通过 call 方法调用了 Animal 构造函数,并将 this 指向 Dog 的实例。这样,Dog 实例就继承了 Animal 构造函数定义的属性和方法。

  1. 借用其他对象的方法:假设我们有一个数组,想要使用 Object.prototype.toString 方法来获取数组的类型信息。
const arr = [1, 2, 3];
// 使用 call 方法借用 Object.prototype.toString 方法
const type = Object.prototype.toString.call(arr);
console.log(type); // 输出: [object Array]

这里,我们通过 call 方法将 Object.prototype.toString 方法应用到数组 arr 上,从而获取到数组的准确类型信息。

apply 方法

apply 方法与 call 方法类似,也是用于调用一个函数,并指定函数执行时的 this 值。不同之处在于,apply 方法接受的参数是一个数组(或类数组对象),而不是一个个单独的参数。

语法

function.apply(thisArg, [argsArray])

  • thisArg:在 function 函数运行时指定的 this 值,与 call 方法中的 thisArg 含义相同。
  • argsArray:一个数组或者类数组对象,其中的元素将作为参数传递给 function 函数。如果该参数为 nullundefined,则不传递任何参数。

示例

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

const anotherPerson = {
  name: 'Jane'
};

// 使用 apply 方法调用 sayHello 方法,并将 anotherPerson 作为 this 值
const greetings = ['Hello', 'Hi'];
person.sayHello.apply(anotherPerson, greetings); // 输出: Hello and Hi, I'm Jane

在这个例子中,我们定义了一个数组 greetings,然后使用 apply 方法将其作为参数传递给 sayHello 函数,同时将 this 指向 anotherPerson

apply 方法的实际应用

  1. 求数组中的最大值和最小值:JavaScript 中的 Math.maxMath.min 方法接受多个参数,但不直接支持数组。我们可以使用 apply 方法来解决这个问题。
const numbers = [5, 10, 3, 8, 15];

// 使用 apply 方法求数组中的最大值
const max = Math.max.apply(null, numbers);
console.log(max); // 输出: 15

// 使用 apply 方法求数组中的最小值
const min = Math.min.apply(null, numbers);
console.log(min); // 输出: 3

在上述代码中,我们通过 apply 方法将数组 numbers 展开作为参数传递给 Math.maxMath.min 方法,从而得到数组中的最大值和最小值。由于这两个方法不需要特定的 this 上下文,所以 thisArg 参数设置为 null

  1. 数组方法的借用:有时候,我们可能需要将类数组对象当作真正的数组来操作。例如,arguments 对象是一个类数组对象,它没有数组的一些方法,如 pushpop 等。我们可以借用数组的方法来操作它。
function addNumbers() {
  const args = Array.prototype.slice.apply(arguments);
  return args.reduce((sum, num) => sum + num, 0);
}

const result = addNumbers(1, 2, 3, 4);
console.log(result); // 输出: 10

在这个例子中,我们使用 Array.prototype.slice.apply(arguments)arguments 对象转换为真正的数组,然后就可以使用数组的 reduce 方法来计算所有参数的总和。

bind 方法

bind 方法也是函数对象的一个方法,它用于创建一个新的函数,这个新函数在调用时,this 的值会被绑定到 bind 方法传入的第一个参数。与 callapply 方法不同,bind 方法不会立即调用函数,而是返回一个新的函数,这个新函数在调用时会使用指定的 this 值。

语法

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

  • thisArg:在新函数中,将作为 this 值使用的对象。如果该参数为 nullundefined,在非严格模式下,新函数执行时 this 将指向全局对象,在严格模式下,this 将为 nullundefined
  • arg1, arg2, ..., argN:当目标函数被调用时,预先添加到参数列表开头的参数。

示例

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

const anotherPerson = {
  name: 'Jane'
};

// 使用 bind 方法创建一个新函数,并将 this 绑定到 anotherPerson
const boundSayHello = person.sayHello.bind(anotherPerson);
boundSayHello(); // 输出: Hello, I'm Jane

在上述示例中,bind 方法返回了一个新的函数 boundSayHello,这个新函数在调用时,this 被固定为 anotherPerson

bind 方法的实际应用

  1. 事件处理函数中的 this 绑定:在 HTML 事件处理中,this 的指向可能不是我们期望的对象。例如,在 DOM 元素的点击事件处理函数中,this 指向触发事件的 DOM 元素。如果我们想在事件处理函数中使用外部对象的 this,可以使用 bind 方法。
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>Bind in Event Handler</title>
</head>

<body>
  <button id="myButton">Click me</button>
  <script>
    const app = {
      message: 'Button was clicked!',
      handleClick: function() {
        console.log(this.message);
      }
    };

    const button = document.getElementById('myButton');
    button.addEventListener('click', app.handleClick.bind(app));
  </script>
</body>

</html>

在这个例子中,app.handleClick 函数中的 this 应该指向 app 对象。但是如果直接将 app.handleClick 作为事件处理函数添加到按钮上,this 会指向按钮元素。通过 bind 方法,我们将 app.handleClick 函数中的 this 绑定到 app 对象,确保在按钮点击时能够正确输出 app.message

  1. 偏函数应用bind 方法还可以用于实现偏函数应用(Partial Application)。偏函数应用是指固定一个函数的部分参数,从而产生一个新的函数,这个新函数只接受剩余的参数。
function add(a, b) {
  return a + b;
}

// 使用 bind 方法固定第一个参数为 5
const addFive = add.bind(null, 5);
const result = addFive(3);
console.log(result); // 输出: 8

在上述代码中,add.bind(null, 5) 创建了一个新的函数 addFive,这个新函数固定了 add 函数的第一个参数为 5,只需要再传入一个参数就可以完成加法运算。

bind、call 与 apply 方法的性能比较

在性能方面,callapply 方法相对 bind 方法来说,在直接调用函数时性能更好。因为 bind 方法返回一个新的函数,这涉及到额外的函数创建开销。

当我们需要多次调用同一个函数,并且每次调用都需要改变 this 指向时,callapply 方法会更高效。例如,在一个循环中调用函数:

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

const people = [
  { name: 'Jane' },
  { name: 'Bob' },
  { name: 'Alice' }
];

for (let i = 0; i < people.length; i++) {
  person.sayHello.call(people[i]);
}

在这个循环中,使用 call 方法直接调用 sayHello 函数,避免了 bind 方法创建新函数的开销,性能相对更好。

然而,如果我们需要创建一个新的函数,并且这个新函数的 this 指向是固定的,那么 bind 方法是更好的选择。例如,在事件处理函数绑定中,我们通常只需要创建一次绑定函数,然后在事件触发时多次调用,这种情况下 bind 方法的开销就可以忽略不计。

总结 bindcallapply 方法的区别

  1. 调用时机

    • callapply 方法会立即调用函数。
    • bind 方法不会立即调用函数,而是返回一个新的函数,这个新函数在调用时会使用指定的 this 值。
  2. 参数传递

    • call 方法接受一个 this 值和一系列参数列表。
    • apply 方法接受一个 this 值和一个数组(或类数组对象)作为参数。
    • bind 方法接受一个 this 值和一系列参数列表,并且可以预先设置部分参数,返回的新函数在调用时会将预先设置的参数与实际传入的参数合并。
  3. 返回值

    • callapply 方法返回函数调用的结果。
    • bind 方法返回一个新的函数。
  4. 性能

    • callapply 方法在直接调用函数时性能更好,因为避免了 bind 方法创建新函数的开销。
    • bind 方法适用于需要创建一个 this 指向固定的新函数的场景,虽然有额外的函数创建开销,但在某些情况下使用更方便。

通过深入理解 bindcallapply 方法的特性和区别,我们可以在 JavaScript 编程中更灵活、高效地控制函数的执行上下文和参数传递,从而编写出更健壮、可维护的代码。无论是在实现对象继承、处理事件、还是进行函数式编程等方面,这三个方法都发挥着重要的作用。在实际开发中,根据具体的需求选择合适的方法,能够让我们的代码更加简洁、清晰,同时也能提升程序的性能。