【第764期】你不懂JS:this是什麼?

圖片
前言


臨近年末,相信接下來的文章正是你所需要的。本文由前端早讀課專欄作者@HetfieldJoe翻譯授權原創分享。

ps:如果想看代碼,可通過點擊圖片查看


正文從這開始~


這是 你不懂JS:this與對象原型 第一章:this是什麼?


JavaScript中最令人困惑的機制之一就是this關鍵字。它是一個在每個函數作用域中自動定義的特殊標識符關鍵字,但即便是一些老練的開發者也對它到底指向什麼感到困擾。


任何足夠 先進 的技術都跟魔法沒有區別。-- Arthur C. Clarke


JavaScript的this機制實際上沒有 那麼 先進,但是開發者們總是在大腦中引用這句話來表達“複雜”和“混亂”,毫無疑問,如果沒有清晰的理解,在 你的 困惑中this可能看起來就是徹頭徹尾的魔法。


注意: “this”這個詞是在一般的論述中極常用的代詞。所以,特別是在口頭論述中,很難確定我們是在將“this”作爲一個代詞使用,還是在將它作爲一個實際的關鍵字識別符使用。爲了表意清晰,我會總是使用this來代表特殊的關鍵字,而在其他情況下使用“this”或 this 或this。


爲什麼用 this?

如果對於那些老練的JavaScript開發者來說this機制都是如此的令人費解,那麼有人會問爲什麼這種機制會有用?它帶來的麻煩不是比好處多嗎?在講解 如何 有用之前,我們應當先來看看 爲什麼 有用。


讓我們試着展示一下this的動機和用途:

圖片


如果這個代碼段 如何 工作讓你困惑,不要擔心!我們很快就會講解它。只是簡要地將這些問題放在旁邊,以便於我們可以更清晰的探究 爲什麼。


這個代碼片段允許identify()和speak()函數對多個 環境 對象(me和you)進行復用,而不是針對每個對象定義函數的分離版本。


與使用this相反地,你可以明確地將環境對象傳遞給identify()和speak()。

image.png


然而,this機制提供了更優雅的方式來隱含地“傳遞”一個對象引用,導致更加乾淨的API設計和更容易的複用。


你的使用模式越複雜,你就會越清晰地看到:將執行環境作爲一個明確參數傳遞,通常比傳遞this執行環境要亂。當我們探索對象和原型時,你將會看到一組可以自動引用恰當執行環境對象的函數是多麼有用。


困惑

我們很快就要開始講解this是如何 實際 工作的,但我們首先要摒棄一些誤解——它實際上 不是 如何工作的。


在開發者們用太過於字面的方式考慮“this”這個名字時就會產生困惑。這通常會產生兩種臆測,但都是不對的。


它自己

第一種常見的傾向是認爲this指向函數自己。至少,這是一種語法上的合理推測。


爲什麼你想要在函數內部引用它自己?最通常的理由是遞歸(在函數內部調用它自己)這樣的情形,或者是一個在第一次被調用時會解除自己綁定的事件處理器。


初次接觸JS機制的開發者們通常認爲,將函數作爲一個對象(JavaScript中所有的函數都是對象!),可以讓你在方法調用之間儲存 狀態(屬性中的值)。這當然是可能的,而且有一些有限的用處,但這本書的其餘部分將會闡述許多其他的模式,提供比函數對象 更好 的地方來存儲狀態。


過一會兒我們將探索一個模式,來展示this是如何不讓一個函數像我們可能假設的那樣,得到它自身的引用的。


考慮下面的代碼,我們試圖追蹤函數(foo)被調用了多少次:

image.png


foo.count 依然 是0, 即便四個console.log語句明明告訴我們foo(..)實際上被調用了四次。這種失敗來源於對於this (在this.count++中)的含義進行了 過於字面化 的解釋。


當代碼執行foo.count = 0時,它確實在函數對象foo中加入了一個count屬性。但是對於函數內部的this.count引用,this其實 根本就不 指向那個函數對象,即便屬性名稱一樣,但根對象也不同,因而產生了混淆。


注意: 一個負責任的開發者 應當 在這裏提出一個問題:“如果我遞增的count屬性不是我以爲的那個,那是哪個count被我遞增了?”。實際上,如果他再挖的深一些,他會發現自己不小心創建了一個全局變量count(第二章解釋了這是 如何 發生的),而且它當前的值是NaN。當然,一旦他發現這個不尋常的結果後,他會有一堆其他的問題:“它怎麼是全局的?爲什麼它是NaN而不是某個正確的計數值?”。(見第二章)


與停在這裏來深究爲什麼this引用看起來不是如我們 期待 的那樣工作,並且回答那些尖銳且重要的問題相反,許多開發者簡單地完全迴避這個問題,轉向一些其他的另類解決方法,比如創建另一個對象來持有count屬性:

image.png


雖然這種方式確實“解決”了問題,但不幸的是它簡單地忽略了真正的問題——缺乏對於this的含義和其工作方式上的理解——反而退回到了一個他更加熟悉的機制的舒適區:詞法作用域。


注意: 詞法作用域是一個完善且有用的機制;我不是在用任何方式貶低它的作用(參見本系列的 "作用域與閉包")。但在如何使用this這個問題上總是靠 猜,而且通常都犯 錯,並不是一個退回到詞法作用域,而且從不學習 爲什麼 this不跟你合作的好理由。


爲了從函數對象內部引用它自己,一般來說通過this是不夠的。你用通常需要通過一個指向它的詞法標識符(變量)得到函數對象的引用。


考慮這兩個函數:

image.png


第一個函數,稱爲“命名函數”,foo是一個引用,可以用於在它內部引用自己。


但是在第二個例子中,傳遞給setTimeout(..)的回調函數沒有名稱標識符(所以被稱爲“匿名函數”),所以沒有恰當的辦法引用函數對象自己。


注意: 在函數中有一個老牌兒但是現在被廢棄的,而且令人皺眉頭的arguments.callee引用 也 指向當前正在執行的函數的函數對象。這個引用通常是匿名函數在自己內部訪問函數對象的唯一方法。然而,最佳的辦法是完全避免使用匿名函數,至少是對於那些需要自引用的函數,而使用命名函數(表達式)。arguments.callee已經被廢棄而且不應該再使用。


對於當前我們的例子來說,另一個 好用的 解決方案是在每一個地方都使用foo標識符作爲函數對象的引用,而根本不用this:

image.png


然而,這種方法也類似地迴避了對this的 真正 理解,而且完全依靠變量foo的詞法作用域。


另一種解決問題的方法是強迫this指向foo函數對象:

圖片


與迴避this相反,我們接受它。 我們將會更完整地講解這樣的技術 如何 工作,所以如果你依然有點兒糊塗,不要擔心!


它的作用域

第二常見的對this的含義的誤解,是它不知怎的指向了函數的作用域。這是一個刁鑽的問題,因爲在某一種意義上它有正確的部分,而在另外一種意義上,它是嚴重的誤導。


明確地說,this不會以任何方式指向函數的 詞法作用域。作用域好像是一個將所有可用標識符作爲屬性的對象,這從內部來說是對的。但是JavasScript代碼不能訪問作用域“對象”。它是 引擎 的內部實現。


考慮下面代碼,它(失敗的)企圖跨越這個邊界,用this來隱含地引用函數的詞法作用域:

圖片


這個代碼段裏不只有一個錯誤。雖然它看起來是在故意瞎搞,但你看到的這段代碼,是從公共的幫助論壇社區中被交換的真實代碼中提取出來的。真是難以想象對this的臆想是多麼的誤導人。


首先,試圖通過this.bar()來引用bar()函數。它幾乎可以說是 碰巧 能夠工作,我們過一會兒再解釋它是 如何 工作的。調用bar()最自然的方式是省略開頭的 this.,而僅對標識符進行詞法引用。


然而,寫下這段代碼的開發者試圖用this在foo()和bar()的詞法作用域間建立一座橋,使得bar()可以訪問foo()內部作用域的變量a。這樣的橋是不可能的。 你不能使用this引用在詞法作用域中查找東西。這是不可能的。


每當你感覺自己正在試圖使用this來進行詞法作用域的查詢時,提醒你自己:這裏沒有橋。


什麼是this?

我們已經列舉了各種不正確的臆想,現在讓我們把注意力this機制是如何真正工作的。


我們早先說過,this不是編寫時綁定,而是運行時綁定。它依賴於函數調用的上下文條件。this綁定和函數聲明的位置無關,反而和函數被調用的方式有關。


當一個函數被調用時,會建立一個活動記錄,也稱爲執行環境。這個記錄包含函數是從何處(call-stack)被調用的,函數是 如何 被調用的,被傳遞了什麼參數等信息。這個記錄的屬性之一,就是在函數執行期間將被使用的this引用。


複習

對於那些沒有花時間學習this綁定機制如何工作的JavaScript開發者來說,this綁定一直是困惑的根源。猜測,試錯,或者盲目地從Stack Overflow的回答中複製粘貼,都不是有效或正確利用this這麼重要的機制的方法。


爲了學習this,你必須首先學習this不是 什麼,不論是哪種把你誤導至何處的臆測或誤解。this既不是函數自身的引用,也不是函數詞法作用域的引用。


this實際上是在函數被調用時建立的一個綁定,它指向 什麼 是完全由函數被調用的調用點來決定的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章