浅谈bind、call和apply的异同及其原理
在 JavaScript 中,修改一个函数的 this 指向,一般有两种方法,一种是通过参数传输,将需要指向的对象传参进去,另一种则是使用 Function 的原型函数,如 bind
、call
和 apply
。
本文将着重介绍 Function 的原型的三个重要函数,即 bind、call 和 apply。并且会尝试手动实现这三个函数,以便探究其原理。
Function 对象
Function
对象是 JavaScript 中非常常用的一个对象,我们定义的任何一个函数,都是 Function 对象的实例。
平时在函数中使用的 arguments 对象也是 Function 原因的一个熟悉。
我们通常调用一个函数:
function a() {
console.log(this, 'a')
};
function b(b) {
console.log(b)
}
a.call(b, 'b params');
// 输出 [Function: b] 'a'
以上都很好理解。
bind
bind 函数绑定在 Function.prototype 上,所以是函数的函数。
bind 返回的是一个修改过的函数的引用,所以还需要在调用引用函数才能得到真正的函数返回值。
试着实现一下bind:
function bind(fun, context) {
if (typeof fun !== 'function') {
throw "fun must be a function";
}
context = context ? Object(context) : window;
let args = Array.prototype.slice.call(arguments).slice(2);
// 或者用下面的展开运算符
// let args = [...arguments].slice(2);
return function() {
return fun.apply(context, args.concat([...arguments]))
}
}
上面实现了一个简单的 bind 方法,这个是一个方法,而不是原型方法,所以这个方法也拥有 bind 方法。调用增方法可是这样的:
function a() {
console.log(this, 'a')
};
function b(b) {
console.log(b)
}
let resbind = bind(a, b, 'b params');
resbind();
// 输出 [Function: b] 'a'
原生的 bind 只需要绑定到 prototype 即可。
Function.prototype.bind(context) {
context = context ? Object(context) : window;
let self = this;
let args = Array.prototype.slice.call(arguments).slice(2);
// 或者用下面的展开运算符
// let args = [...arguments].slice(2);
return function() {
return self.apply(context, args.concat([...arguments]))
}
}
上面的代码引出了 apply,下面看 apply 。
apply
与 bind 不同的是,apply 方法不是返回引用函数,而是直接调用函数,得到返回值。
我们如果要实现 apply 函数,就不能通过 bind 来修改 this 指向了,这样只会产生循环递归,不会得到结果。所以这里就引出了另一个问题,即 JavaScript 中的 this 指向问题。
默认情况下,全局函数的 this 指向了 window,所以在 bind 中才会有这一句:
context = context ? Object(context) : window;
context 是引用上下文,我们让其默认值为 window,这是符合定义的。
初次之外,如果需要修改一个函数的 this 指向,就只能通过其他上下文的形式来实现了,举个例子:
let person = {
name: "Tome",
getName: function () {
return this.name;
}
}
persion.getName();
// Tom
请看这个例子中的 this ,person 对象的 this 其所在的函数是 getName,而 getName 所在的上下文是 person 对象,所以 this.name 才是可以访问到的。
基于上面的原理来实现 apply 。
function apply(fun, context) {
if (typeof fun !== "function") {
throw "fun must be a function";
}
context = context ? Object(context) : window;
// 先将函数绑定到上下文环境中
context.fn = fun;
let result = context.fn(Array.prototype.slice.call(arguments).slice(2));
// 使用完后删除函数
delete context.fn;
return result;
}
测试一下:
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
let slice = apply(Array.prototype.slice, arr, [0, 4]);
console.log(slice);
// 输出 [1, 2, 3, 4]
可以看到输出结果是正确的。
在 JavaScript 中,this 的指向其实就是其所在的上下文环境。在不使用 bind、apply 和 call 方法的情况下,若想修改 this 指向,只能修改其上下文。
call
call 方法与 apply 本质是一样的,只是调用时的传参不同,apply 的参数必须是数组,而 call 的传参是需要直接写入的,可以使用展开运算符。
function call(fun, context) {
if (typeof fun !== "function") {
throw "fun must be a function";
}
context = context ? Object(context) : window;
// 先将函数绑定到上下文环境中
context.fn = fun;
// call 方法这里传参与 apply 有所不同
let result = context.fn(...arguments[2]);
// 使用完后删除函数
delete context.fn;
return result;
}
总结
bind
、apply
和 call
都是为了改变函数的 this 指向,它们都是函数的函数,bind 返回一个函数引用,apply 和 call 返回函数调用结果。apply 和 call 只是传参形式不同,本质并无区别。
最后需要了解,JavaScript 中的 this 指向的本质还是跟运行时的上下文有关,所以修改 this 指向的本质就是修改其上下文。