返回

聊聊 call/apply/bind 的原理与妙用

前端





# 序

在前端开发中,`this` 的指向问题是一个非常常见且关键的问题。`call`、`apply`、`bind` 这些方法正是为我们提供灵活调控 `this` 指向的方案,借此我们可以更轻松地操纵和使用复杂的引用。在本文中,我们将详细探讨这些方法的原理和妙用,帮助你深挖它们的使用奥秘,在编码中如虎添翼。

## call、apply、bind 简介

这三个方法的共同点在于:

* 允许显式地为一个给定的 `this` 上下文调用一个目标方法。
* 允许为这个方法传入额额外自变量,就像直接调用这个方法时传递自变量的方式。
* 返回方法调用后的返回值。

但由于其调用时的细微差别,它们各有千秋。

### 相同点:

* 第一个都是为这个方法指定一个 this 值,第二个则允许你提供一个实参数组。

### 差异点:

* call: 传递实参使用逗号分割
* apply: 传递实参时需要传入一个 arguments 数组(实参)

另外,`bind` 并不像 `call` 与 `apply` 一样立即调用,而是会产生一个新方法,而新方法在调用时将使用你所提供的上下文和实参。

## 原理剖析

这三个方法的原理是相当直观的,我们可以从一个更底层的角度来一探究竟。

### 调用 `Function.prototype.call`

我们可以直接调用一个 `Function` 对象的 `call` 方法。

```js
function foo() {
  console.log(this.a);
}

let obj = {
  a: 'objA',
};

foo.call(obj);

输出:

objA

第一个给定 foo.callthis 值将被赋值为 objthis.a 就指向了 'objA'

使用 bind 创建一个新方法

bind 方法允许我们创建一个新方法,该新方法在调用时将使用特定的 this 值和一组特定的实参。

let boundFoo = foo.bind(obj);

boundFoo();

输出:

objA

foo.call(obj) 的行为类似,boundFoo 也将 this 值显式地指定为 obj

总结

总而言之,callapplybind 的原理如下:

  • call:直接用提供的值替换方法的 this 值,并使用按顺序传递的参数来调用方法。
  • apply:也用提供的值替换方法的 this 值,但使用一个数组来传递方法的参数。
  • bind:创建一个新方法,该方法在调用时将使用指定的 this 值和一组特定实参,而无需显式调用该方法。

妙用场景

掌握了 callapplybind 的原理后,我们来看看它们如何派上用场。

场景一:改变 this 指向

问题场景

我们有一个 person 对象,它有一个 speak 方法,该方法应打印出 this.name。但当我们从一个按钮的单击侦听器中调用 speak 方法时,this 引用了按钮,导致打印了 undefinded

const person = {
  name: 'John Doe',
  speak() {
    console.log(this.name);
  },
};

const button = document.getElementById('button');

button.onclick = function() {
  person.speak();
};

如何修复

我们可以使用 bind 方法将 person.speak 重新邦定到 person 上下文。

const person = {
  name: 'John Doe',
  speak() {
    console.log(this.name);
  },
};

const button = document.getElementById('button');

button.onclick = function() {
  person.speak.bind(person)();
};

这样,当我们单击按钮时,person.speak 将在 person 的上下文中被调用,正确地打印出 John Doe

场景二:传递额外实参

问题场景

我们有一个 ajax 函数,它接受一个用于发送 ajax 请求的 url。我们想创建一个包装器方法,该方法允许我们为每次调用指定一个不同的 url,而无需显式地向原始 ajax 函数传递 url

function ajax(url) {
  // 发送 ajax 请求
}

function wrapperAjax(params) {
  // 根据 params.url 发送 ajax 请求
}

如何修复

我们可以使用 bind 方法来创建一个新方法,该方法在调用时将使用特定的 url

function ajax(url) {
  // 发送 ajax 请求
}

function wrapperAjax(params) {
  const boundAjax = ajax.bind(null, params.url);
  boundAjax();
}

bind 方法允许我们创建一个新方法 boundAjax,该方法在调用时将使用 null 替换方法的 this 值,并将 params.url 替换为方法的第一个实参。这样,我们可以像下面这样在 params 对象中指定 url,而无需显式地将 url 传递给 ajax 函数。

const params = {
  url: 'example.com/api/v1/users',
};

wrappedAjax(params);

场景三:上下文中的 polyfill

问题场景

我们有一个 Array.from 方法,它将类数组转换为数组。然而,这个方法在早期版本的 JavaScript 中并不受原生 JavaScript 支持。

const arrayLike = {
  length: 5,
  0: 'foo',
  1: 'bar',
  2: 'baz',
  3: 'qux',
  4: 'quux',
};

const array = Array.from(arrayLike);

如何修复

我们可以使用 bind 方法来创建一个 Array.from 的 polyfill。

if (!Array.from) {
  Array.from = function(arrayLike) {
    return [].splice.call(arrayLike, 0);
  };
}

bind 方法允许我们创建一个新方法 from,该方法在调用时将使用 null 替换方法的 this 值,并将 arrayLike 替换为方法的第一个实参。这样,Array.from 的 polyfill 就可以在不受原生 JavaScript 支持的浏览器中工作。

常见问题解答

  • callapply 的区别是什么?

    call 使用逗号分隔的参数,而 apply 使用数组进行实参传递。

  • bindcallapply 的区别是什么?

    bind 允许你创建一个新方法,该方法在调用时将使用特定 this 值和实参,而无需显式调用该方法。

  • 使用 bind 的最佳方法是什么?

    使用 bind 的最佳方法是创建一个新方法,该方法在调用时将使用特定 this 值和实参,而无需显式调用该方法。

总结

callapplybind 这些方法在灵活地操纵和使用 this 引用中表现出非凡的实用性。掌握它们可以让我们在编码过程中更从容地应对 this 值的变化,在复杂的业务场景中也游刃有余。

灵活运用这三个方法,你就能更有效率、更专业地写出易于扩展和调试的 JavaScript 代码。让它们在指尖下绽放异彩,助你创造更出色、更具竞争力的前端应用程序!