理解 JavaScript this 文章中已经比较全面的分析了 this 在 JavaScript 中的指向问题,用一句话来总结就是:this 的指向必定是在执行时决定的,指向被调用函数的对象。固然,上篇文章也指出能够经过 call() / apply() / bind() 这些内置的函数方法来指定 this 的指向,以达到开发者的预期,而这篇文章将进一步来讨论这个问题。javascript
先来回顾一下,举个简单的例子:html
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 指向的对象,好比今天的引例中就是这样。java
第一个参数也能够是 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 没有定义segmentfault
除了第一个参数,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]。app
接下来讲说 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() 区别,我么本身实现的函数叫作 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 的原理。
function foo() { console.log(this.bar); } var obj = { bar: 'baz' }; foo.call(obj); // "baz"
咱们观察 call 的调用,存在下面的特色:
那就来看看,以示区别,咱们本身实现的 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() 的实现与 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
Be Good. Sleep Well. And Enjoy.
前端技术 | 我的成长