聊聊 call/apply/bind 的原理与妙用
2023-09-19 19:34:45
# 序
在前端开发中,`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.call
的 this
值将被赋值为 obj
,this.a
就指向了 'objA'
。
使用 bind
创建一个新方法
bind
方法允许我们创建一个新方法,该新方法在调用时将使用特定的 this
值和一组特定的实参。
let boundFoo = foo.bind(obj);
boundFoo();
输出:
objA
与 foo.call(obj)
的行为类似,boundFoo
也将 this
值显式地指定为 obj
。
总结
总而言之,call
、apply
、bind
的原理如下:
call
:直接用提供的值替换方法的this
值,并使用按顺序传递的参数来调用方法。apply
:也用提供的值替换方法的this
值,但使用一个数组来传递方法的参数。bind
:创建一个新方法,该方法在调用时将使用指定的this
值和一组特定实参,而无需显式调用该方法。
妙用场景
掌握了 call
、apply
、bind
的原理后,我们来看看它们如何派上用场。
场景一:改变 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
支持的浏览器中工作。
常见问题解答
-
call
与apply
的区别是什么?call
使用逗号分隔的参数,而apply
使用数组进行实参传递。 -
bind
与call
、apply
的区别是什么?bind
允许你创建一个新方法,该方法在调用时将使用特定this
值和实参,而无需显式调用该方法。 -
使用
bind
的最佳方法是什么?使用
bind
的最佳方法是创建一个新方法,该方法在调用时将使用特定this
值和实参,而无需显式调用该方法。
总结
call
、apply
、bind
这些方法在灵活地操纵和使用 this
引用中表现出非凡的实用性。掌握它们可以让我们在编码过程中更从容地应对 this
值的变化,在复杂的业务场景中也游刃有余。
灵活运用这三个方法,你就能更有效率、更专业地写出易于扩展和调试的 JavaScript
代码。让它们在指尖下绽放异彩,助你创造更出色、更具竞争力的前端应用程序!