理解 JavaScript 中的 this
前言
理解this
是我們要深入理解 JavaScript 中必不可少的一個步驟,同時只有理解了 this
,你才能更加清晰地寫出與自己預期一致的 JavaScript 代碼。
本文是這系列的第三篇,往期文章:
- 理解 JavaScript 中的作用域
- 理解 JavaScript 中的閉包
什麽是 this
消除誤解
在解釋什麽是this
之前,需要先糾正大部分人對this
的誤解,常見的誤解有:
- 指向函數自身。
- 指向它所在的作用域。
關於為何會誤解的原因這裏不多講,這裏只給出結論,有興趣可以自行查詢資料。
this
在任何情況下都不指向函數的詞法作用域。你不能使用 this
this 到底是什麽
排除了一些錯誤理解之後,我們來看看 this
到底是一種什麽樣的機制。
this
是在運行時(runtime
)進行綁定的,而不是在編寫時綁定的,它的上下文(對象)取決於函數調用時的各種條件。this
的綁定和函數聲明的位置沒有任何關系,只取決於函數的調用方式。
當一個函數被調用時,會創建一個活動記錄(有時候也稱為執行上下文)。這個記錄會包含函數在哪裏被調用(調用棧)、函數的調用方法、傳入的參數等信息。this
就是記錄的其中一個屬性,會在函數執行的過程中用到。( PS:所以this
並不等價於執行上下文)
this 全面解析
前面 我們排除了一些對於 this
this
是在調用時被綁定的,完全取決於函數的調用位置。
調用位置
通常來說,尋找調用位置就是尋找“函數被調用的位置“,其中最重要的是要分析調用棧(就是為了到達當前執行位置所調用的所有函數)。我們關心的調用位置就在當前正在執行的函數的前一個調用中。
下面我們來看看到底什麽是調用棧和調用位置:
function foo(){
// 當前調用棧是:foo
// 因此,當前調用位置是全局作用域
console.log("foo");
bar(); // <-- bar 的調用位置
}
function bar(){
// 當前調用棧是 foo -> bar
console.log("bar");
}
foo(); // <-- foo 的調用位置
你可以把調用棧想象成一個函數調用鏈, 就像我們在前面代碼段的註釋中所寫的一樣。但是這種方法非常麻煩並且容易出錯。 另一個查看調用棧的方法是使用瀏覽器的調試工具。 絕大多數現代桌面瀏覽器都內置了開發者工具,其中包含 JavaScript 調試器。
綁定規則
在找到調用位置後,則需要判定代碼屬於下面四種綁定規則中的哪一種,然後才能對this
進行綁定。 註意: this
綁定的是上下文對象,並不是函數自身也不是函數的詞法作用域
默認綁定
這是最常見的函數調用類型:獨立函數調用:
對函數直接使用而不帶任何修飾的函數引用進行調用,簡單點一個函數直接是func()
這樣調用,不同於通過對象屬性調用例如obj.func()
,也沒有通過 new 關鍵字new Function()
,也沒有通過apply
、call
、bind
強制改變this
指向。
當被用作獨立函數調用時(不論這個函數在哪被調用,不管全局還是其他函數內),this
默認指向到Window
。(註意:在嚴格模式下this
不再默認指向全局,而是undefined
)。
示例代碼:
function foo(){
console.log(this.name);
}
var name = "window";
foo(); // window
隱式綁定
函數被某個對象擁有或者包含,也就是函數被作為對象的屬性所引用,例如obj.func()
,此時this
會綁定到該對象上,這就是隱式綁定。
示例代碼:
var obj = {
name : "obj",
foo : function(){
console.log(this.name);
}
}
obj.foo(); // obj
隱式丟失:
大部分的this
綁定問題就是被“隱式綁定”的函數會丟失綁定對象,也就是說它會應用“默認綁定”,從而把this
綁定到Window
或undefined
上,這取決於是否是嚴格模式。
最常見的情況就是把對象方法作為回調函數進行傳遞時:
var obj = {
name : "obj",
foo : function(){
console.log(this.name);
}
}
var name = "window";
setTimeout(obj.foo,1000); // 一秒後輸出 window
顯式綁定
我們可以通過apply
、call
、bind
方法來顯示地修改this
的指向。
關於這三個方法的定義(它們第一個參數都是接受this
的綁定對象):
apply
:調用函數,第二個參數傳入一個參數數組。call
:調用函數,其余參數正常傳遞。bind
:返回一個已經綁定this
的函數,其余參數正常傳遞。
比如我們可以使用bind
方法解決上一節“隱式丟失”中的例子:
var obj = {
name : "obj",
foo : function(){
console.log(this.name);
}
}
var name = "window";
setTimeout(obj.foo.bind(obj),1000); // 一秒後輸出 obj
new 綁定
使用 new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操作:
- 創建(或者說構造)一個全新的對象。
- 這個新對象會被執行[[原型]]連接。
- 這個新對象會綁定到函數調用的
this
。 - 如果函數沒有返回其他對象,那麽
new
表達式中的函數調用會自動返回這個新對象。
示例代碼:
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
優先級
直接上結論:
new 綁定=顯示綁定>隱式綁定>默認綁定
判斷 this: 現在我們可以根據優先級來判斷函數在某個調用位置應用的是哪條規則。可以按照下面的順序來進行判斷:
-
使用 new 綁定,
this
綁定的是新創建的對象。var bar = new foo();
-
通過
call
之類的顯式綁定,this
綁定的是指定的對象。var bar = foo.call(obj2);
-
在某個上下文對象中調用(隱式綁定),this 綁定的是那個上下文對象。
var bar = obj1.foo();
-
如果都不是的話,使用默認綁定。
this
綁定到Window
或undefined
上,這取決於是否是嚴格模式。var bar = foo();
對於正常的函數調用來說,理解了這些知識你就可以明白 this 的綁定原理了。
this 詞法
ES6 中介紹了一種無法使用上面四條規則的特殊函數類型:箭頭函數。
箭頭函數不使用 this 的四種標準規則,而是根據外層(函數或者全局)作用域來決定 this。(而傳統的 this 與函數作用域沒有任何關系,它只與調用位置的上下文對象有關)。
重要:
- 箭頭函數最常用於回調函數中,例如事件處理器或者定時器.
- 箭頭函數可以像
bind
一樣確保函數的this
被綁定到指定對象 - 箭頭函數用更常見的詞法作用域取代了傳統的
this
機制。
示例代碼:
var obj = {
name : "obj",
foo : function(){
setTimeout(()=>{
console.log(console.log(this.name)); // obj
},1000);
}
}
obj.foo();
這在 ES6 之前是這樣解決的:
var obj = {
name : "obj",
foo : function(){
var self = this;
setTimeout(function(){
console.log(console.log(self.name)); // obj
},1000);
}
}
obj.foo();
總結
總之如果要判斷一個運行中函數的this
綁定,就需要找到這個函數的直接調用位置。找到之後就可以順序應用下面這四條規則來判斷this
的綁定對象。
- 由 new 調用?綁定到新創建的對象。
- 由 call 或者 apply(或者 bind)調用?綁定到指定的對象。
- 由上下文對象調用?綁定到那個上下文對象。
- 默認:在嚴格模式下綁定到
undefined
,否則綁定到全局對象。
ES6 中的箭頭函數並不會使用四條標準的綁定規則,而是根據當前的詞法作用域來決定 this
,具體來說,箭頭函數會繼承外層函數調用的 this
綁定(無論 this
綁定到什麽)。這其實和 ES6 之前代碼中的 self = this
機制一樣。
(轉載於:https://www.v2ex.com/t/529532#reply1)
理解 JavaScript 中的 this