天天看點

javascript this的一些誤解

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

介紹之前先解釋下什麼是動态作用域

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

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

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

引擎會檢查bar() 的作用域,并在其中找到值為3 的變量a。很奇怪吧?現在你可能會這麼想。但這其實是因為你可能隻寫過基于詞法作用域的代碼(或者至少以詞法作用域為基礎進行

了深入的思考),是以對動态作用域感到陌生。如果你隻用基于動态作用域的語言寫過代碼,就會覺得這是很自然的,而詞法作用域看上去才怪怪的。需要明确的是,事實上JavaScript 并不具有動态作用域。它隻有詞法作用域,簡單明了。但是this 機制某種程度上很像動态作用域。

主要差別:詞法作用域是在寫代碼或者說定義時确定的,而動态作用域是在運作時确定的。(this 也是!)詞法作用域關注函數在何處聲明,而動态作用域關注函數從何處調用。

最後,this 關注函數如何調用,這就表明了this 機制和動态作用域之間的關系多麼緊密。

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

javascript this的一些誤解

1.指向自身

人們很容易把this 了解成指向函數自身,這個推斷從英語的文法角度來說是說得通的。那麼為什麼需要從函數内部引用函數自身呢?常見的原因是遞歸(從函數内部調用這個函數)或者可以寫一個在第一次被調用後自己解除綁定的事件處理器。

JavaScript 的新手開發者通常會認為,既然函數看作一個對象(JavaScript 中的所有函數都是對象),那就可以在調用函數時存儲狀态(屬性的值)。這是可行的,有些時候也确實有用,但在許多模式中你會發現,除了函數

對象還有許多更合适存儲狀态的地方。不過現在我們先來分析一下這個模式,讓大家看到this 并不像我們所想的那樣指向函數本身。

我們想要記錄一下函數foo 被調用的次數,思考一下下面的代碼:

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 屬性的對象。

從某種角度來說這個方法确實“解決”了問題,但可惜它忽略了真正的問題——無法了解this 的含義和工作原理——而是傳回舒适區,使用了一種更熟悉的技術:詞法作用域。詞法作用域是一種非常優秀并且有用的技術。我絲毫沒有貶低它的意思(可

以參考本書第一部分“作用域和閉包”)。但是如果你僅僅是因為無法猜對this 的用法,就放棄學習this 而去使用詞法作用域,就不能算是一種很好的解決辦法了。如果要從函數對象内部引用它自身,那隻使用this 是不夠的。一般來說你需要通過一個指

向函數對象的詞法辨別符(變量)來引用它。

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

第一個函數被稱為具名函數,在它内部可以使用foo 來引用自身。但是在第二個例子中,傳入setTimeout(..) 的回調函數沒有名稱辨別符(這種函數被稱為

匿名函數),是以無法從函數内部引用自身。還有一種傳統的但是現在已經被棄用和批判的用法,是使用arguments.callee 來引用目前正在運作的函數對象。這是唯一一種可以從匿名函數對象内部引用自身的方法。然而,更好的方式是避免使用匿名函數,至少在需要自引用時使用具名函數(表達式)。arguments.callee 已經被棄用,不應該再使用它。

是以,對于我們的例子來說,另一種解決方法是使用foo 辨別符替代this 來引用函數

對象:

然而,這種方法同樣回避了this 的問題,并且完全依賴于變量foo 的詞法作用域。

另一種方法是強制this 指向foo 函數對象:

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

2 它的作用域

第二種常見的誤解是,this 指向函數的作用域。這個問題有點複雜,因為在某種情況下它是正确的,但是在其他情況下它卻是錯誤的。需要明确的是,this 在任何情況下都不指向函數的詞法作用域。在JavaScript 内部,作用域确實和對象類似,可見的辨別符都是它的屬性。但是作用域“對象”無法通過JavaScript代碼通路,它存在于JavaScript 引擎内部。

思考一下下面的代碼,它試圖(但是沒有成功)跨越邊界,使用this 來隐式引用函數的詞法作用域:

這段代碼中的錯誤不止一個。雖然這段代碼看起來好像是我們故意寫出來的例子,但是實際上它出自一個公共社群中互助論壇的精華代碼。這段代碼非常完美(同時也令人傷感)

地展示了this 多麼容易誤導人。首先,這段代碼試圖通過this.bar() 來引用bar() 函數。這是絕對不可能成功的,我們之後會解釋原因。調用bar() 最自然的方法是省略前面的this,直接使用詞法引用辨別符。此外,編寫這段代碼的開發者還試圖使用this 聯通foo() 和bar() 的詞法作用域,進而讓bar() 可以通路foo() 作用域裡的變量a。這是不可能實作的,你不能使用this 來引用一個詞法作用域内部的東西。每當你想要把this 和詞法作用域的查找混合使用時,一定要提醒自己,這是無法實作的。

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

本文轉自帥氣的頭頭部落格51CTO部落格,原文連結http://blog.51cto.com/12902932/1924599如需轉載請自行聯系原作者

sshpp

繼續閱讀