1. 程式人生 > >javascript this的一些誤解

javascript this的一些誤解

java

太拘泥於“this”的字面意思就會產生一些誤解。有兩種常見的對於this 的解釋,但是它們都是錯誤的。

介紹之前先解釋下什麽是動態作用域

簡要地分析一下動態作用域,重申它與詞法作用域的區別。但實際上動態作用域是JavaScript 另一個重要機制this 的表親。詞法作用域是一套關於引擎如何尋找變量以及會在何處找到變量的規則。詞法作用域最重要的特征是它的定義過程發生在代碼的書寫階段(假設你沒有使用eval() 或with)。動態作用域似乎暗示有很好的理由讓作用域作為一個在運行時就被動態確定的形式,而不是在寫代碼時進行靜態確定的形式,事實上也是這樣的。通過示例代碼來說明:

技術分享

function foo() {
console.log( a ); // 2}function bar() {var a = 3;
foo();
}var a = 2;
bar();

技術分享

詞法作用域讓foo() 中的a 通過RHS(js中賦值的一種形式) 引用到了全局作用域中的a,因此會輸出2。而動態作用域並不關心函數和作用域是如何聲明以及在何處聲明的,只關心它們從何處調用。換句話說,作用域鏈是基於調用棧的,而不是代碼中的作用域嵌套。因此,如果JavaScript 具有動態作用域,理論上,下面代碼中的foo() 在執行時將會輸出3。

技術分享

function foo() {
console.log( a ); // 3(不是2 !)}function bar() {var a = 3;
foo();
}var a = 2;
bar();

技術分享

為什麽會這樣?因為當foo() 無法找到a 的變量引用時,會順著調用棧在調用foo() 的地方查找a,而不是在嵌套的詞法作用域鏈中向上查找。由於foo() 是在bar() 中調用的,

引擎會檢查bar() 的作用域,並在其中找到值為3 的變量a。很奇怪吧?現在你可能會這麽想。但這其實是因為你可能只寫過基於詞法作用域的代碼(或者至少以詞法作用域為基礎進行
了深入的思考),因此對動態作用域感到陌生。如果你只用基於動態作用域的語言寫過代碼,就會覺得這是很自然的,而詞法作用域看上去才怪怪的。需要明確的是,事實上JavaScript 並不具有動態作用域。它只有詞法作用域,簡單明了。但是this 機制某種程度上很像動態作用域。
主要區別:詞法作用域是在寫代碼或者說定義時確定的,而動態作用域是在運行時確定的。(this 也是!)詞法作用域關註函數在何處聲明,而動態作用域關註函數從何處調用。
最後,this 關註函數如何調用,這就表明了this 機制和動態作用域之間的關系多麽緊密。

可以在chrome中的Call Stack中查看調用棧,需要在調試模式下(當然,這是廢話)

技術分享

1.指向自身


人們很容易把this 理解成指向函數自身,這個推斷從英語的語法角度來說是說得通的。那麽為什麽需要從函數內部引用函數自身呢?常見的原因是遞歸(從函數內部調用這個函數)或者可以寫一個在第一次被調用後自己解除綁定的事件處理器。
JavaScript 的新手開發者通常會認為,既然函數看作一個對象(JavaScript 中的所有函數都是對象),那就可以在調用函數時存儲狀態(屬性的值)。這是可行的,有些時候也確實有用,但在許多模式中你會發現,除了函數

對象還有許多更合適存儲狀態的地方。不過現在我們先來分析一下這個模式,讓大家看到this 並不像我們所想的那樣指向函數本身。
我們想要記錄一下函數foo 被調用的次數,思考一下下面的代碼:

技術分享

 1 function foo(num) { 2 console.log( "foo: " + num ); 3 // 記錄foo 被調用的次數 4 this.count++; 5 } 6 foo.count = 0; 7 var i; 8 for (i=0; i<10; i++) { 9 if (i > 5) {10 foo( i );11 }12 }13 // foo: 614 // foo: 715 // foo: 816 // foo: 917 // foo 被調用了多少次?18 console.log( foo.count ); // 0 -- WTF?

技術分享

console.log 語句產生了4 條輸出,證明foo(..) 確實被調用了4 次,但是foo.count 仍然是0。顯然從字面意思來理解this 是錯誤的。執行foo.count = 0 時,的確向函數對象foo 添加了一個屬性count。但是函數內部代碼
this.count 中的this 並不是指向那個函數對象,所以雖然屬性名相同,根對象卻並不相同,困惑隨之產生。負責的開發者一定會問“如果我增加的count 屬性和預期的不一樣,那我增加的是哪個count ?”實際上,如果他深入探索的話,就會發現這段代碼在
無意中創建了一個全局變量count(原理參見第2 章),它的值為NaN。當然,如果他發現了這個奇怪的結果,那一定會接著問:“為什麽它是全局的,為什麽它的值是NaN 而不是其他更合適的值?”(參見第2 章。)
遇到這樣的問題時,許多開發者並不會深入思考為什麽this 的行為和預期的不一致,也不會試圖回答那些很難解決但卻非常重要的問題。他們只會回避這個問題並使用其他方法來達到目的,比如創建另一個帶有count 屬性的對象。

技術分享

function foo(num) {
console.log( "foo: " + num );// 記錄foo 被調用的次數data.count++;
}var data = {
count: 0};var i;for (i=0; i<10; i++) {if (i > 5) {
foo( i );
}
}// foo: 6// foo: 7// foo: 8// foo: 9// foo 被調用了多少次?console.log( data.count ); // 4

技術分享

從某種角度來說這個方法確實“解決”了問題,但可惜它忽略了真正的問題——無法理解this 的含義和工作原理——而是返回舒適區,使用了一種更熟悉的技術:詞法作用域。詞法作用域是一種非常優秀並且有用的技術。我絲毫沒有貶低它的意思(可
以參考本書第一部分“作用域和閉包”)。但是如果你僅僅是因為無法猜對this 的用法,就放棄學習this 而去使用詞法作用域,就不能算是一種很好的解決辦法了。如果要從函數對象內部引用它自身,那只使用this 是不夠的。一般來說你需要通過一個指
向函數對象的詞法標識符(變量)來引用它。

思考一下下面這兩個函數:

function foo() {
foo.count = 4; // foo 指向它自身}
setTimeout( function(){// 匿名(沒有名字的)函數無法指向自身}, 10 );

第一個函數被稱為具名函數,在它內部可以使用foo 來引用自身。但是在第二個例子中,傳入setTimeout(..) 的回調函數沒有名稱標識符(這種函數被稱為
匿名函數),因此無法從函數內部引用自身。還有一種傳統的但是現在已經被棄用和批判的用法,是使用arguments.callee 來引用當前正在運行的函數對象。這是唯一一種可以從匿名函數對象內部引用自身的方法。然而,更好的方式是避免使用匿名函數,至少在需要自引用時使用具名函數(表達式)。arguments.callee 已經被棄用,不應該再使用它。
所以,對於我們的例子來說,另一種解決方法是使用foo 標識符替代this 來引用函數
對象:

技術分享

function foo(num) {
console.log( "foo: " + num );// 記錄foo 被調用的次數foo.count++;
}
foo.count=0var i;for (i=0; i<10; i++) {if (i > 5) {
foo( i );
}
}
關於this | 79// foo: 6// foo: 7// foo: 8// foo: 9// foo 被調用了多少次?console.log( foo.count ); // 4

技術分享

然而,這種方法同樣回避了this 的問題,並且完全依賴於變量foo 的詞法作用域。
另一種方法是強制this 指向foo 函數對象:

技術分享

function foo(num) {
console.log( "foo: " + num );// 記錄foo 被調用的次數// 註意,在當前的調用方式下(參見下方代碼),this 確實指向foothis.count++;
}
foo.count = 0;var i;for (i=0; i<10; i++) {if (i > 5) {// 使用call(..) 可以確保this 指向函數對象foo 本身foo.call( foo, i );
}
}// foo: 6// foo: 7// foo: 8// foo: 9// foo 被調用了多少次?console.log( foo.count ); // 4

技術分享

這次我們接受了this,沒有回避它。

2 它的作用域

第二種常見的誤解是,this 指向函數的作用域。這個問題有點復雜,因為在某種情況下它是正確的,但是在其他情況下它卻是錯誤的。需要明確的是,this 在任何情況下都不指向函數的詞法作用域。在JavaScript 內部,作用域確實和對象類似,可見的標識符都是它的屬性。但是作用域“對象”無法通過JavaScript代碼訪問,它存在於JavaScript 引擎內部。
思考一下下面的代碼,它試圖(但是沒有成功)跨越邊界,使用this 來隱式引用函數的詞法作用域:

技術分享

function foo() {var a = 2;this.bar();
}function bar() {
console.log( this.a );
}
foo(); // ReferenceError: a is not defined

技術分享

這段代碼中的錯誤不止一個。雖然這段代碼看起來好像是我們故意寫出來的例子,但是實際上它出自一個公共社區中互助論壇的精華代碼。這段代碼非常完美(同時也令人傷感)
地展示了this 多麽容易誤導人。首先,這段代碼試圖通過this.bar() 來引用bar() 函數。這是絕對不可能成功的,我們之後會解釋原因。調用bar() 最自然的方法是省略前面的this,直接使用詞法引用標識符。此外,編寫這段代碼的開發者還試圖使用this 聯通foo() 和bar() 的詞法作用域,從而讓bar() 可以訪問foo() 作用域裏的變量a。這是不可能實現的,你不能使用this 來引用一個詞法作用域內部的東西。每當你想要把this 和詞法作用域的查找混合使用時,一定要提醒自己,這是無法實現的。

this的作用類似於動態作用域, 而動態作用域並不關心函數和作用域是如何聲明以及在何處聲明的,只關心它們從何處調用。換句話說,作用域鏈是基於調用棧的,而不是代碼中的作用域嵌套。


javascript this的一些誤解