javascript this的一些誤解

來源:互聯網
上載者:User

標籤:java

            

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

介紹之前先解釋下什麼是動態範圍

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

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

650) this.width=650;" src="http://images2015.cnblogs.com/blog/1029816/201702/1029816-20170211145658885-1407631546.png" style="margin:0px;padding:0px;border:0px;" />

 

1.指向自身


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

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

 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?

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

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

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

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

2 它的範圍

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />

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

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


javascript this的一些誤解

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.