在上一篇博客裏我總結了下辨認this指向的四種方式,可是有師兄拋出一個問題來,我發現那些this的指向並不能說明更復雜的狀況,先看下這段代碼javascript
var a = { name: 'a', getName: function(){ console.log(this.name) } } var c = a.getName.bind(a) var b={ } b.getName=a.getName; c();//a
那麼爲何最後執行c會獲得a呢?this在其中的指向究竟是啥呢?我以爲利用上篇的博文並不能很好的解釋。因此,這裏查找了不少資料,而後我以爲還要再作一個二次總結。也但願各位看官在看完博文以後可以思考出這個例子中的this最後的指向。java
this 是在運行時進行綁定的,並非在編寫時綁定,它的上下文取決於函數調用時的各類條件。 this 的綁定和函數聲明的位置沒有任何關係,只取決於函數的調用方式。當一個函數被調用時,會建立一個活動記錄(有時候也稱爲執行上下文)。這個記錄會包含函數在哪裏被調用(調用棧)、函數的調用方法、傳入的參數等信息。this 就是記錄的其中一個屬性,會在函數執行的過程當中用到。app
因此,咱們要明白,this其實既不指向函數自身,也不指向函數的詞法做用域。實際上,this是在函數被調用時綁定,它指向什麼徹底取決於函數在哪裏被調用。函數
調用位置,也就是所謂的函數實際的調用位置,而不是函數的聲明位置。這個決定了this最後的調用。其中最爲重要的就是分析調用棧(爲了到達當前執行位置所調用的全部函數)。咱們所關心的調用位置就在當前執行函數的前一個調用中。測試
function baz() { // 當前調用棧是:baz // 所以,當前調用位置是全局做用域 console.log( "baz" ); bar(); // <-- bar 的調用位置 } function bar() { // 當前調用棧是 baz -> bar // 所以,當前調用位置在 baz 中 console.log( "bar" ); foo(); // <-- foo 的調用位置 } function foo() { // 當前調用棧是 baz -> bar -> foo // 所以,當前調用位置在 bar 中 console.log( "foo" ); } baz(); // <-- baz 的調用位置
在函數的執行過程當中調用位置如何決定 this 的綁定對象。
你必須找到調用位置,而後判斷須要應用下面四條規則中的哪一條。咱們首先會分別解釋這四條規則,而後解釋多條規則均可用時它們的優先級如何排列。this
function foo() { console.log( this.a ); } var a = 2; foo(); // 2
function foo() { "use strict"; console.log( this.a ); } var a = 2; foo(); // TypeError: this is undefined
這裏有一個微妙可是很是重要的細節,雖然 this 的綁定規則徹底取決於調用位置,可是隻有 foo() 運行在非 strict mode 下時,默認綁定才能綁定到全局對象;嚴格模式下與 foo()的調用位置無關:編碼
function foo() { console.log( this.a ); } var a = 2; (function(){ "use strict"; foo(); // 2 })();
首先,咱們來看一段代碼:spa
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
咱們能夠看出,foo()函數的聲明方式和最後的obj中做爲引用屬性添加到obj裏,但即便是這樣,嚴格意義上來講,foo()也不屬於obj對象,就算咱們把聲明放入obj中,亦是如此。然而,調用位置會使用 obj 上下文來引用函數,所以你能夠說函數被調用時 obj 對象「擁
有」或者「包含」它。prototype
可是不管如何去稱呼這個模式,當foo()被調用的時候,指向確實指到了obj對象。當函數引用有上下文對象時,隱式綁定規則會把函數調用中的 this 綁定到這個上下文對象。由於調用 foo() 時 this 被綁定到 obj ,所以 this.a 和 obj.a 是同樣的。code
這裏還會出現一個問題,隱式丟失。 這個怎麼來理解呢?其實就是說在隱式綁定的函數中,可能會出現綁定對象丟失,而後就會應用默認規則,從而把this綁定到全局對象或者undefined上。這種狀況的出現取決因而否使用嚴格模式。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函數別名! var a = "Kevin"; // a 是全局對象的屬性 bar(); // "Kevin"
來看這個例子,雖然bar是obj.foo的一個引用,可是實際上引用的倒是foo這個函數自己,因此此時,bar()是一個沒有任何修飾的函數調用,天然就應用了默認規則進行綁定。
還有一種狀況,十分常見,也會出現隱式丟失,那就是在回調函數中。來看下例子:
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var a = "Kevin"; // a 是全局對象的屬性 setTimeout( obj.foo, 100 ); // "Kevin"
參數傳遞其實就是一種隱式賦值,所以咱們傳入函數時也會被隱式賦值,因此結果和上一個例子同樣。就像咱們看到的那樣,回調函數丟失 this 綁定是很是常見的。除此以外,還有一種狀況 this 的行爲會出乎咱們意料:調用回調函數的函數可能會修改 this 。不管是哪一種狀況, this 的改變都是意想不到的,實際上你沒法控制回調函數的執行方式,所以就沒有辦法控制會影響綁定的調用位置。
在分析隱式綁定時,咱們必須在一個對象內部包含一個指向函數的屬性,並經過這個屬性間接引用函數,從而把 this 間接(隱式)綁定到這個對象上。那麼若是咱們不想在對象內部包含函數引用,而想在某個對象上強制調用函數,該怎麼作呢?這個時候,咱們就須要使用call或者apply兩個方法來調用了。嚴格來講,JavaScript 的宿主環境有時會提供一些很是特殊的函數,它們並無這兩個方法。可是這樣的函數很是罕見,JavaScript 提供的絕大多數函數以及你本身建立的全部函數均可以使用 call(..) 和 apply(..) 方法。這兩個方法是如何工做的呢?它們的第一個參數是一個對象,它們會把這個對象綁定到this ,接着在調用函數時指定這個 this 。由於你能夠直接指定 this 的綁定對象,所以咱們稱之爲顯式綁定。
來看下這個例子:
function foo() { console.log( this.a ) } var obj = { a:2 }; foo.call( obj ); // 2
經過 foo.call(..) ,咱們能夠在調用 foo 時強制把它的 this 綁定到 obj 上。若是你傳入了一個原始值(字符串類型、布爾類型或者數字類型)來看成 this 的綁定對象,這個原始值會被轉換成它的對象形式(也就是 new String(..) 、 new Boolean(..) 或者new Number(..) )。這一般被稱爲「裝箱」。
可是即便這樣,也沒法徹底解決以前出現的綁定丟失的狀況
1. 硬綁定
可是顯式綁定的一個變種能夠解決這個問題。
咱們來看這樣一個例子
function foo() { console.log( this.a ); } var obj = { a:2 }; var bar = function() { foo.call( obj ); }; bar(); // 2 setTimeout( bar, 100 ); // 2 // 硬綁定的 bar 不可能再修改它的 this bar.call( window ); // 2
咱們來看看這個變種究竟是怎樣工做的。咱們建立了函數 bar() ,並在它的內部手動調用了 foo.call(obj) ,所以強制把 foo 的 this 綁定到了 obj 。不管以後如何調用函數 bar ,它總會手動在 obj 上調用 foo 。這種綁定是一種顯式的強制綁定,所以咱們稱之爲硬綁定。硬綁定的典型應用場景就是建立一個包裹函數,傳入全部的參數並返回接收到的全部值:
function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = { a:2 }; var bar = function() { return foo.apply( obj, arguments ); }; var b = bar( 3 ); // 2 3 console.log( b ); // 5
另外一種使用方法是建立一個 i 能夠重複使用的輔助函數:
function foo(something) { console.log( this.a, something ); return this.a + something; } // 簡單的輔助綁定函數 function bind(fn, obj) { return function() { return fn.apply( obj, arguments ); }; } var obj = { a:2 }; var bar = bind( foo, obj ); var b = bar( 3 ); // 2 3 console.log( b ); // 5
因爲硬綁定是一種很是經常使用的模式,因此在 ES5 中提供了內置的方法 Function.prototype.bind ,它的用法以下:
function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = { a:2 }; var bar = foo.bind( obj ); var b = bar( 3 ); // 2 3 console.log( b ); // 5
bind(..) 會返回一個硬編碼的新函數,它會把參數設置爲 this 的上下文並調用原始函數。
2. API調用的「上下文」
第三方庫的許多函數,以及 JavaScript 語言和宿主環境中許多新的內置函數,都提供了一個可選的參數,一般被稱爲「上下文」(context),其做用和 bind(..) 同樣,確保你的回調函數使用指定的 this 。舉例來講:
function foo(el) { console.log( el, this.id ); } var obj = { id: "awesome" }; // 調用 foo(..) 時把 this 綁定到 obj [1, 2, 3].forEach( foo, obj ); // 1 awesome 2 awesome 3 awesome
這些函數實際上就是經過 call(..) 或者 apply(..) 實現了顯式綁定,這樣你能夠少些一些代碼。
這是第四條也是最後一條 this 的綁定規則,在講解它以前咱們首先須要澄清一個很是常見的關於 JavaScript 中函數和對象的誤解。在傳統的面向類的語言中,「構造函數」是類中的一些特殊方法,使用 new 初始化類時會調用類中的構造函數。一般的形式是這樣的:
something = new MyClass(..);
JavaScript 也有一個 new 操做符,使用方法看起來也和那些面向類的語言同樣,絕大多數開發者都認爲 JavaScript 中 new 的機制也和那些語言同樣。然而,JavaScript 中 new 的機制實際上和麪向類的語言徹底不一樣。首先咱們從新定義一下 JavaScript 中的「構造函數」。在 JavaScript 中,構造函數只是一些使用 new 操做符時被調用的函數。它們並不會屬於某個類,也不會實例化一個類。實際上,它們甚至都不能說是一種特殊的函數類型,它們只是被 new 操做符調用的普通函數而已。
舉例來講,思考一下 Number(..) 做爲構造函數時的行爲,ES5.1 中這樣描述它:
15.7.2 Number 構造函數當 Number 在 new 表達式中被調用時,它是一個構造函數:它會初始化新建立的對象。因此,包括內置對象函數(好比 Number(..) ,詳情請查看第 3 章)在內的全部函數均可以用 new 來調用,這種函數調用被稱爲構造函數調用。這裏有一個重要可是很是細微的區別:實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」。使用 new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。
1. 建立(或者說構造)一個全新的對象。
2. 這個新對象會被執行 [[ 原型 ]] 鏈接。
3. 這個新對象會綁定到函數調用的 this 。
4. 若是函數沒有返回其餘對象,那麼 new 表達式中的函數調用會自動返回這個新對象。
再來看看下面的例子
function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2
使用 new 來調用 foo(..) 時,咱們會構造一個新對象並把它綁定到 foo(..) 調用中的 this上。 new 是最後一種能夠影響函數調用時 this 綁定行爲的方法,咱們稱之爲 new 綁定。
有種狀況咱們須要進行考慮,就是在函數調用過程當中,某個位置若是出現應用了多條綁定規則怎麼辦?那麼要解決這種問題,咱們就須要知道規則的優先級。
毫無疑問,默認規則是四條規則中最低的,因此暫時不作考慮。因此首先,咱們須要比對一下顯式綁定與隱式綁定的優先級,看看哪個更高一些。
來看以下的例子:
function foo() { console.log( this.a ); } var obj1 = { a: 2, foo: foo }; var obj2 = { a: 3, foo: foo }; obj1.foo(); // 2 obj2.foo(); // 3 obj1.foo.call( obj2 ); // 3 obj2.foo.call( obj1 ); // 2
能夠看到,顯式綁定優先級更高,也就是說在判斷時應當先考慮是否能夠應用顯式綁定。
接下來,咱們須要比對下隱式和new的優先級,看看誰高誰低。來看以下例子:
function foo(something) { this.a = something; } var obj1 = { foo: foo }; var obj2 = {}; obj1.foo( 2 ); console.log( obj1.a ); // 2 obj1.foo.call( obj2, 3 ); console.log( obj2.a ); // 3 var bar = new obj1.foo( 4 ); console.log( obj1.a ); // 2 console.log( bar.a ); // 4
能夠看到 new 綁定比隱式綁定優先級高。可是 new 綁定和顯式綁定誰的優先級更高呢?
但這裏咱們須要注意一個問題:new 和 call / apply 沒法一塊兒使用,所以沒法經過 new foo.call(obj1) 來直接進行測試。可是咱們可使用硬綁定來測試它倆的優先級。
而後回憶下硬綁定,Function.prototype.bind(...)建立了一個新的包裝函數,這個函數會忽略當前this的綁定,而且強制把咱們提供的對象綁定到this上。那麼由此看來,new綁定的優先級彷佛比硬綁定(顯式綁定)要底,可是真的是這樣的麼?
這裏有個例子:
function foo(something) { this.a = something; } var obj1 = {}; var bar = foo.bind( obj1 ); bar( 2 ); console.log( obj1.a ); // 2 var baz = new bar(3); console.log( obj1.a ); // 2 console.log( baz.a ); // 3
這個結果彷佛有點出乎意料,bar被硬綁定到了obj1上,可是new bar(3)並非按照以前所想的那樣把obj1.a修改成3。偏偏相反的是,new修改了硬綁定(到 obj1 的)而且調用 bar(..) 中的 this 。由於使用了
new 綁定,咱們獲得了一個名字爲 baz 的新對象,而且 baz.a 的值是 3。
判斷this
綜上所述,咱們能夠根據優先級來判斷函數在某個調用位置應用的是哪條規則。也能夠按照以下的順序來進行判斷:
1. 函數是否在 new 中調用( new 綁定)?若是是的話 this 綁定的是新建立的對象。
var bar = new foo();
2. 函數是否經過 call 、 apply (顯式綁定)或者硬綁定調用?若是是的話, this 綁定的是指定的對象。
var bar = foo.call(obj2);
3. 函數是否在某個上下文對象中調用(隱式綁定)?若是是的話, this 綁定的是那個上下文對象。
var bar = obj1.foo();
4. 若是都不是的話,使用默認綁定。若是在嚴格模式下,就綁定到 undefined ,不然綁定到全局對象。
var bar = foo();
就是這樣。對於正常的函數調用來講,理解了這些知識你就能夠明白 this 的綁定原理了。
關於this的指向,以及優先級,咱們上面已經總結了不少,可是這個時候就不得不提ES6了,由於在其中有一個沒法使用上述規則的特殊函數類型:箭頭函數
箭頭函數並非使用 function 關鍵字定義的,而是使用被稱爲「胖箭頭」的操做符 => 定義的。箭頭函數不使用 this 的四種標準規則,而是根據外層(函數或者全局)做用域來決定 this 。
咱們來看看箭頭函數的詞法做用域:
function foo() { // 返回一個箭頭函數 return (a) => { //this 繼承自 foo() console.log( this.a ); }; } var obj1 = { a:2 }; var obj2 = { a:3 }; var bar = foo.call( obj1 ); bar.call( obj2 ); // 2, 不是 3 !
foo()內部建立的箭頭函數會捕獲調用時 foo() 的 this 。因爲 foo() 的 this 綁定到 obj1 ,bar (引用箭頭函數)的 this 也會綁定到 obj1 ,箭頭函數的綁定沒法被修改。 ( new 也不行!)
箭頭函數最經常使用於回調函數中,例如事件處理器或者定時器:
function foo() { setTimeout(() => { // 這裏的 this 在此法上繼承自 foo() console.log( this.a ); },100); } var obj = { a:2 }; foo.call( obj ); // 2
箭頭函數能夠像 bind(..) 同樣確保函數的 this 被綁定到指定對象,此外,其重要性還體如今它用更常見的詞法做用域取代了傳統的 this 機制。實際上,在 ES6 以前咱們就已經在使用一種幾乎和箭頭函數徹底同樣的模式。
function foo() { var self = this; // lexical capture of this setTimeout( function(){ console.log( self.a ); }, 100 ); } var obj = { a: 2 }; foo.call( obj ); // 2
雖然 self = this 和箭頭函數看起來均可以取代 bind(..) ,可是從本質上來講,它們想替代的是 this 機制。若是你常常編寫 this 風格的代碼,可是絕大部分時候都會使用 self = this 或者箭頭函數來否認 this 機制,那你或許應當:
1. 只使用詞法做用域並徹底拋棄錯誤 this 風格的代碼;
2. 徹底採用 this 風格,在必要時使用 bind(..) ,儘可能避免使用 self = this 和箭頭函數。
固然,包含這兩種代碼風格的程序能夠正常運行,可是在同一個函數或者同一個程序中混合使用這兩種風格一般會使代碼更難維護,而且可能也會更難編寫。
若是要判斷一個運行中函數的 this 綁定,就須要找到這個函數的直接調用位置。找到以後就能夠順序應用下面這四條規則來判斷 this 的綁定對象。
1. 由 new 調用?綁定到新建立的對象。
2. 由 call 或者 apply (或者 bind )調用?綁定到指定的對象。
3. 由上下文對象調用?綁定到那個上下文對象。
4. 默認:在嚴格模式下綁定到 undefined ,不然綁定到全局對象。
ES6 中的箭頭函數並不會使用四條標準的綁定規則,而是根據當前的詞法做用域來決定this ,具體來講,箭頭函數會繼承外層函數調用的 this 綁定(不管 this 綁定到什麼) 。這d其實和 ES6 以前代碼中的 self = this 機制同樣。
ps.其實關於這篇,例如對於this指向的顯式綁定與後續的優先級,我都屬於能理解是什麼意思,可是卻無法用我本身的語言去概括與總結...因此那部份內容仍是已整理爲主。但願在後續的工做中可以有更多的幾乎去實踐博客中所說起的語法糖,同時也但願這篇博文能給你們帶來一點點幫助。若是博文中有錯誤或者不詳之處,請各位批評指正!完結撒花~