理解 JavaScript call()/apply()/bind()

时间:2022-04-25 21:38:18

理解 JavaScript this 文章中已经比较全面的分析了 this 在 JavaScript 中的指向问题,用一句话来总结就是:this 的指向一定是在执行时决定的,指向被调用函数的对象。当然,上篇文章也指出可以通过 call() / apply() / bind() 这些内置的函数方法来指定 this 的指向,以达到开发者的预期,而这篇文章将进一步来讨论这个问题。

先来回顾一下,举个简单的例子:


var leo = {
name: 'Leo',
sayHi: function() {
return "Hi! I'm " + this.name;
}
}; var neil = {
name: 'Neil'
}; leo.sayHi(); // "Hi! I'm Leo"
leo.sayHi.call(neil); // "Hi! I'm Neil"

基本用法

在 JavaScript 中,函数也是对象,所以 JS 的函数有一些内置的方法,就包括 call(), apply() 和 bind(),它们都定义在 Function 的原型上,所以每一个函数都可以调用这 3 个方法。

Function.prototype.call(thisArg [, arg1 [, arg2, ...]]),对于 call() 而言,它的第一个参数为需要绑定的对象,也就是 this 指向的对象,比如今天的引例中就是这样。

第一个参数也可以是 null 和 undefined,在严格模式下 this 将指向浏览器中的 window 对象或者是 Node.js 中的 global 对象。


var leo = {
name: 'Leo',
sayHi: function() {
return "Hi! I'm " + this.name;
}
}; leo.sayHi.call(null); // "Hi! I'm undefined"

▲ this 指向 window,window.name 没有定义

除了第一个参数,call() 还可以选择接收剩下任意多的参数,这些参数都将作为调用函数的参数,来看一下:


function add(a, b) {
return a + b;
} add.call(null, 2, 3); // 5

▲ 等同于 add(2, 3)

apply() 的用法和 call() 类似,唯一的区别是它们接收参数的形式不同。除了第一个参数外,call() 是以枚举的形式传入一个个的参数,而 apply() 是传入一个数组。


function add(a, b) {
return a + b;
} add.apply(null, [2, 3]); // 5

注意:apply() 接受的第二个参数为数组(也可以是一个类数组对象),但不意味着调用它的函数接收的是数组参数。这里的 add() 函数依旧是 a 和 b 两个参数,分别赋值为 2 和 3,而不是 a 被赋值为 [2, 3]。

接下来说说 bind(),它和另外两个大有区别。


var leo = {
name: 'Leo',
sayHi: function() {
return "Hi! I'm " + this.name;
}
};
var neil = {
name: 'Neil'
};
var neilSayHi = leo.sayHi.bind(neil);
console.log(typeof neilSayHi); // "function"
neilSayHi(); // "Hi! I'm Neil"

与 call() 和 apply() 直接执行原函数不同的是,bind() 返回的是一个新函数。简单说,bind() 的作用就是将原函数的 this 绑定到指定对象,并返回一个新的函数,以延迟原函数的执行,这在异步流程中(比如回调函数,事件处理程序)具有很强大的作用。你可以将 bind() 的过程简单的理解为:


function bind(fn, ctx) {
return function() {
fn.apply(ctx, arguments);
};
}

如何实现

这一部分应该是经常出现在面试中。最常见的应该是 bind() 的实现,就先来说说如何实现自己的 bind()。

◆ bind() 的实现

上一节已经简单地实现了一个 bind(),稍作改变,为了和内置的 bind() 区别,我么自己实现的函数叫做 bound(),先看一下:


Function.prototype.bound = function(ctx) {
var fn = this;
return function() {
return fn.apply(ctx);
};
}

这里的 bound() 模拟了一个最基本的 bind() 函数的实现,即返回一个新函数。这个新函数包裹了原函数,并且绑定了 this 的指向为传入的 ctx。

对于内置的 bind() 来说,它还有一个特点:


var student = { id: '2015' }; function showDetail (name, major) {
console.log('The id ' + this.id +
' is for ' + name +
', who major in ' + major);
} showDetail.bind(student, 'Leo')('CS');
// "The id 2015 is for Leo, who major in CS" showDetail.bind(student, 'Leo', 'CS')();
// "The id 2015 is for Leo, who major in CS"

在这里两次调用参数传递的方式不同,但是具有同样的结果。下面,就继续完善我们自己的 bound() 函数。


var slice = Array.prototype.slice; Function.prototype.bound = function(ctx) {
var fn = this;
var _args = slice.call(arguments, 1);
return function() {
var args = _args.concat(slice.call(arguments));
return fn.apply(ctx, args);
};
}

这里需要借助 Array.prototype.slice() 方法,它可以将 arguments 类数组对象转为数组。我们用一个变量保存传入 bound() 的除第一个参数以外的参数,在返回的新函数中,将传入新函数的参数与 bound() 中的参数合并。

其实,到现在整个 bound() 函数的实现都离不开闭包,你可以查看文章 理解 JavaScript 闭包

在文章 理解 JavaScript this 中,我们提到 new 也能改变 this 的指向,那如果 new 和 bind() 同时出现,this 会听从谁?


function Student() {
console.log(this.name, this.age);
} Student.prototype.name = 'Neil';
Student.prototype.age = 20; var foo = Student.bind({ name: 'Leo', age: 21 });
foo(); // 'Leo' 21 new foo(); // 'Neil' 20

从例子中已经可以看出,使用 new 改变了 bind() 已经绑定的 this 指向,而我们自己的 bound() 函数则不会:


var foo = Student.bound({ name: 'Leo', age: 21 });
foo(); // 'Leo' 21 new foo(); // 'Leo' 21

所以我们还要接着改进 bound() 函数。要解决这个问题,我们需要清楚原型链以及 new 的原理,在后面的文章中我再来分析,这里只提供解决方案。


var slice = Array.prototype.slice; Function.prototype.bound = function(ctx) {
if (typeof this !== 'function') {
throw TypeError('Function.prototype.bound - what is trying to be bound is not callable');
}
var fn = this;
var _args = slice.call(arguments);
var fBound = function() {
var args = _args.concat(slice.call(arguments)); // 在绑定原函数 fn 时增加一次判断,如果 this 是 fBound 的一个实例
// 那么此时 fBound 的调用方式一定是 new 调用
// 所以,this 直接绑定 this(fBound 的实例对象) 就好
// 否则,this 依旧绑定到我们指定的 ctx 上
return fn.apply(this instanceof fBound ? this : ctx, args);
}; // 这里我们必须要声明 fBound 的 prototype 指向为原函数 fn 的 prototype
fBound.prototype = Object.create(fn.prototype); return fBound;
}

大功告成。如果看不懂最后一段代码,可以先放一放,后面的文章会分析原型链和 new 的原理。

◆ call() 的实现


function foo() {
console.log(this.bar);
}
var obj = { bar: 'baz' };
foo.call(obj); // "baz"

我们观察 call 的调用,存在下面的特点:

  • 当函数 foo 调用 call,并传入 obj 时,似乎是在 obj 的原型上增加了一个 foo 方法。
  • foo.call() 除第一个参数外的所有参数都应该传给 foo(),这一点在实现 bind() 时已处理过。
  • 不能对 foo 和 obj 做任何修改。

那就来看看,以示区别,我们自己实现的 call 叫做 calling。


Function.prototype.calling = function(ctx) {
ctx.fn = this;
ctx.fn();
}

我们完成了第一步。

在完成第二步时,我们需要用到 eval(),它可以执行一段字符串类型的 JavaScript 代码。


var slice = Array.prototype.slice; Function.prototype.calling = function(ctx) {
ctx.fn = this;
var args = [];
for (var i = 1; i < args.length; i++) {
args.push('arguments[' + i + ']');
}
eval('ctx.fn(' + args + ')');
}

这里我们避免采用和实现 bind() 同样的方法获取剩余参数,因为要使用到 call,所以这里采用循环。我们需要一个一个的将参数传入 ctx.fn(),所以就用到 eval(),这里的 eval() 中的代码在做 + 运算时,args 会发生类型转换,自动调用 toString() 方法。

实现到这里,大部分的功能以及完成,但是我们不可避免的为 ctx 手动添加了一个 fn 方法,改变了 ctx 本身,所以要把它给删除掉。另外,call 应该有返回值,且它的值是 fn 执行过后的结果,并且如果 ctx 传入 null 或者 undefined,应该将 this 绑定到全局对象。我们可以得到下面的代码:


var slice = Array.prototype.slice; Function.prototype.calling = function(ctx) {
ctx = ctx || window || global;
ctx.fn = this;
var args = [];
for (var i = 1; i < args.length; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('ctx.fn(' + args + ')');
delete ctx.fn;
return result;
}

◆ apply() 的实现

apply() 的实现与 call() 类似,只是参数的处理不同,直接看代码吧。


var slice = Array.prototype.slice; Function.prototype.applying = function(ctx, arr) {
ctx = ctx || window || global;
ctx.fn = this;
var result = null;
var args = [];
if (!arr) {
result = ctx.fn();
} else {
for (var i = 1; i < args.length; i++) {
args.push('arr[' + i + ']');
}
result = eval('ctx.fn(' + args + ')');
}
delete ctx.fn;
return result;
}

小结

这篇文章在上一篇文章的基础上,更进一步地讨论了 call() / apply() / bind() 的用法以及实现,其中三者的区别和 bind() 的实现是校招面试的常考点,初次接触可能有点难理解 bind(),因为它涉及到闭包、new 以及原型链。

我会在接下来的文章中介绍对象、原型以及原型链、继承、new 的实现原理,敬请期待。

本文原文发布在公众号 cameraee,点击查看

文章参考

Function.prototype.call() / apply() / bind() | MDN

Invoking JavaScript Functions With 'call' and 'apply' | A Drop of JavaScript

Implement your own - call(), apply() and bind() method in JavaScript | Ankur Anand

JavaScript .call() .apply() and .bind() - explained to a total noob | Owen Yang

JavaScript call() & apply() vs bind()? | Stack Overflow

Learn & Solve: call(), apply() and bind() methods in JavaScript

JavaScript 系列文章

理解 JavaScript this

理解 JavaScript 闭包

理解 JavaScript 执行栈

理解 JavaScript 作用域

理解 JavaScript 数据类型与变量

Be Good. Sleep Well. And Enjoy.

理解 JavaScript call()/apply()/bind()

前端技术 | 个人成长

来源:https://segmentfault.com/a/1190000017747952