理解 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.
前端技術 | 我的成長