理解JavaScript中的作用域

理解JavaScript中的作用域

范圍,或確定變量所在位置的一組規(guī)則,是任何編程語言的最基本概念之一。事實上,它是如此基本,以至于我們很容易忘記這些規(guī)則是多么微妙!

準確理解 JavaScript 引擎如何“思考”作用域將使您避免編寫提升可能導致的常見錯誤,讓您做好準備專注于閉包,并讓您離永遠不再編寫錯誤更近一步 再次。

…無論如何,它會幫助您理解提升和關閉。

在本文中,我們將了解:

立即學習Java免費學習筆記(深入)”;

  • JavaScript 中作用域的基礎知識
  • 解釋器如何決定哪些變量屬于哪個作用域
  • 吊裝的實際工作原理
  • es6 關鍵字 let 和 const 如何改變游戲規(guī)則

讓我們深入探討。

如果您有興趣了解有關 ES6 的更多信息以及如何利用語法和功能來改進和簡化 JavaScript 代碼,為什么不看看這兩門課程:

詞法范圍

如果您之前編寫過一行 JavaScript,您就會知道定義變量的位置決定了您可以使用的位置> 他們。變量的可見性取決于源代碼的結(jié)構(gòu),這一事實稱為詞法 范圍。

在 JavaScript 中創(chuàng)建作用域有三種方法:

  1. 創(chuàng)建函數(shù)。在函數(shù)內(nèi)部聲明的變量僅在該函數(shù)內(nèi)部可見,包括在嵌套函數(shù)中。
  2. 使用 let 或 const 在代碼塊內(nèi)聲明變量。此類聲明僅在塊內(nèi)可見。
  3. 創(chuàng)建catch 。不管你相信與否,這實際上確實創(chuàng)建了一個新的范圍!
"use strict"; var mr_global = "Mr Global";  function foo () {     var mrs_local = "Mrs Local";     console.log("I can see " + mr_global + " and " + mrs_local + ".");          function bar () {         console.log("I can also see " + mr_global + " and " + mrs_local + ".");     } }  foo(); // Works as expected  try {     console.log("But /I/ can't see " + mrs_local + ".");  } catch (err) {     console.log("You just got a " + err + "."); }  {     let foo = "foo";     const bar = "bar";     console.log("I can use " + foo + bar + " in its block..."); }  try {     console.log("But not outside of it.");    } catch (err) {     console.log("You just got another " + err + "."); }  // Throws ReferenceError! console.log("Note that " + err + " doesn't exist outside of 'catch'!")  

上面的代碼片段演示了所有三種作用域機制。您可以在 Node 或 firefox 中運行它,但 chrome 還無法與 let 很好地配合。

我們將詳細討論其中的每一個。讓我們首先詳細了解 JavaScript 如何確定哪些變量屬于哪個作用域。

編譯過程:鳥瞰

當您運行一段 JavaScript 時,會發(fā)生兩件事使其正常工作。

  1. 首先,編譯您的源代碼。
  2. 然后,編譯后的代碼將被執(zhí)行。

編譯步驟期間,JavaScript 引擎:

  1. 記下所有變量名稱
  2. 將它們注冊到適當?shù)姆秶?/li>
  3. 為自己的價值觀保留空間

只有在執(zhí)行期間,JavaScript 引擎才真正將變量引用的值設置為等于其賦值。在那之前,它們是 undefined

第 1 步:編譯

// I can use first_name anywhere in this program  var first_name = "Peleke";  function popup (first_name) {     // I can only use last_name inside of this function     var last_name = "Sengstacke";     alert(first_name + ' ' + last_name); }  popup(first_name); 

讓我們逐步了解一下編譯器的作用。

首先,它讀取行 var first_name = “Peleke”。接下來,它確定將變量保存到的范圍。因為我們位于腳本的頂層,所以它意識到我們處于全局范圍。然后,它將變量 first_name 保存到全局范圍,并將其值初始化為 undefined。

其次,編譯器讀取帶有 function popup (first_name) 的行。因為 function 關鍵字是該行的第一個內(nèi)容,因此它為函數(shù)創(chuàng)建一個新作用域,將函數(shù)的定義注冊到全局作用域,并查看內(nèi)部以查找變量聲明。

果然,編譯器找到了一個。由于我們函數(shù)的第一行中有 var last_name = “Sengstacke”,因此編譯器會將變量 last_name 保存到 class=”inline”>popup —到全局范圍 — 并將其值設置為 undefined。

由于函數(shù)內(nèi)不再有變量聲明,編譯器將退回到全局作用域。由于不再有變量聲明,因此此階段已完成。

請注意,我們實際上還沒有運行任何東西。此時編譯器的工作只是確保它知道每個人的名字;它不關心他們做什么。

此時,我們的程序知道:

  1. 全局范圍內(nèi)有一個名為 first_name 的變量。
  2. 全局范圍內(nèi)有一個名為 popup 的函數(shù)。
  3. 在 popup 范圍內(nèi)有一個名為 last_name 的變量。
  4. first_name 和 last_name 的值均為 undefined。

它并不關心我們是否在代碼中的其他地方分配了這些變量值。引擎在執(zhí)行時會處理這個問題。

第 2 步:執(zhí)行

在下一步中,引擎再次讀取我們的代碼,但這一次,執(zhí)行它。

首先,它讀取行 var first_name = “Peleke”。為此,引擎會查找名為 first_name 的變量。由于編譯器已經(jīng)用該名稱注冊了一個變量,引擎會找到它,并將其值設置為 “Peleke”。

接下來,它讀取行 function popup (first_name)。由于我們沒有在這里執(zhí)行該函數(shù),因此引擎不感興趣并跳過它。

最后,它讀取行 popup(first_name)。由于我們在這里執(zhí)行一個函數(shù),因此引擎:

  1. 查找 popup 的值
  2. 查找 first_name 的值
  3. 將 popup 作為函數(shù)執(zhí)行,并傳遞 first_name 的值作為參數(shù)

當執(zhí)行 popup 時,它會經(jīng)歷相同的過程,但這次是在函數(shù) popup 內(nèi)。它:

  1. 查找名為 last_name 的變量
  2. 將 last_name 的值設置為等于 “Sengstacke”
  3. 查找 alert,將其作為函數(shù)執(zhí)行,并以 “Peleke Sengstacke” 作為參數(shù)

事實證明,幕后發(fā)生的事情比我們想象的要多得多!

既然您已經(jīng)了解了 JavaScript 如何讀取和運行您編寫的代碼,我們就準備好解決一些更貼近實際的問題:提升的工作原理。

顯微鏡下的吊裝

讓我們從一些代碼開始。

bar();  function bar () {     if (!foo) {         alert(foo + "? This is strange...");     }     var foo = "bar"; }  broken(); // TypeError! var broken = function () {     alert("This alert won't show up!"); } 

如果運行此代碼,您會注意到三件事:

  1. 在分配之前,您可以引用 foo,但其值為 undefined。
  2. 您可以在定義之前調(diào)用 已損壞的,但您會收到 TypeError。
  3. 可以在定義之前調(diào)用 bar,它會按需要工作。

提升是指 JavaScript 使我們聲明的所有變量名稱在其作用域內(nèi)的任何地方都可用,包括在我們分配給它們之前 .

代碼段中的三種情況是您在自己的代碼中需要注意的三種情況,因此我們將一一逐步介紹它們。

提升變量聲明

請記住,當 JavaScript 編譯器讀取像 var foo = “bar” 這樣的行時,它會:

  1. 將名稱 foo 注冊到最近的范圍
  2. 將 foo 的值設置為未定義

我們可以在賦值之前使用 foo 的原因是,當引擎查找具有該名稱的變量時,它確實存在。這就是為什么它不會拋出 ReferenceError。

相反,它獲取值 undefined,并嘗試使用該值執(zhí)行您要求的任何操作。通常,這是一個錯誤。

記住這一點,我們可能會想象 JavaScript 在我們的函數(shù) bar 中看到的更像是這樣:

function bar () {     var foo; // undefined     if (!foo) {         // !undefined is true, so alert         alert(foo + "? This is strange...");     }     foo = "bar"; } 

如果您愿意的話,這是提升的第一條規(guī)則:變量在其整個范圍內(nèi)都可用,但其值為 undefined ,直到您代碼分配給他們。

常見的 JavaScript 習慣用法是將所有 var 聲明寫入其作用域的頂部,而不是首次使用它們的位置。用 Doug Crockford 的話來說,這可以幫助您的代碼閱讀更像它運行

仔細想想,這是有道理的。當我們以 JavaScript 讀取代碼的方式編寫代碼時,為什么 bar 的行為方式非常清楚,不是嗎?那么為什么不一直這樣寫呢?

提升函數(shù)表達式

事實上,當我們在定義之前嘗試執(zhí)行 broken 時,我們得到了 TypeError,這只是第一條提升規(guī)則的一個特例。

我們定義了一個名為 broken 的變量,編譯器會在全局范圍內(nèi)注冊該變量,并將其設置為等于 undefined。當我們嘗試運行它時,引擎會查找 broken 的值,發(fā)現(xiàn)它是 undefined,并嘗試將 undefined 作為函數(shù)執(zhí)行.

顯然,undefined 不是一個函數(shù),這就是為什么我們得到 TypeError!

提升函數(shù)聲明

最后,回想一下,在定義 bar 之前,我們可以調(diào)用它。這是由于第二條提升規(guī)則:當 JavaScript 編譯器找到函數(shù)聲明時,它會使其名稱和定義在其作用域的頂部可用。再次重寫我們的代碼:

function bar () {     if (!foo) {         alert(foo + "? This is strange...");     }     var foo = "bar"; }  var broken; // undefined  bar(); // bar is already defined, executes fine  broken(); // Can't execute undefined!  broken = function () {     alert("This alert won't show up!"); } 

同樣,當您編寫作為JavaScript讀取時,它更有意義,您不覺得嗎?

查看:

  1. 變量聲明和函數(shù)表達式的名稱在其整個范圍內(nèi)都可用,但它們的值在賦值之前為 undefined。
  2. 函數(shù)聲明的名稱??和定義在其整個范圍內(nèi)都可用,甚至在其定義之前。

現(xiàn)在讓我們來看看兩個工作方式稍有不同的新工具:let 和 const。

letconst 和臨時死區(qū)強>強>

與 var 聲明不同,使用 let 和 const 聲明的變量?被編譯器提升。

至少,不完全是。

還記得我們?nèi)绾握{(diào)用 已損壞的,但卻因為嘗試執(zhí)行 undefined 而收到 TypeError 嗎?如果我們使用 let 定義 broken,我們就會得到 ReferenceError,而不是:

"use strict";  // You have to "use strict" to try this in Node broken(); // ReferenceError! let broken = function () {     alert("This alert won't show up!"); } 

當 JavaScript 編譯器在第一遍中將變量注冊到其作用域時,它對待 let 和 const 的方式與處理 var 的方式不同。

當它找到 var 聲明時,我們將該變量的名稱注冊到其范圍,并立即將其值初始化為 undefined。

但是,使用 let,編譯器將變量注冊到其作用域,但不會初始化其值為 undefined。相反,它會使變量保持未初始化狀態(tài),直到引擎執(zhí)行您的賦值語句。訪問未初始化變量的值會拋出 ReferenceError,這解釋了為什么上面的代碼片段在運行時會拋出異常。

let 聲明和賦值語句的頂部開頭之間的空間稱為臨時死區(qū)。該名稱源自以下事實:即使引擎知道名為 foo 的變量(位于 bar 范圍的頂部),該變量是“死的”,因為它沒有值。

…還因為如果您嘗試盡早使用它,它會殺死您的程序。

const 關鍵字的工作方式與 let 相同,但有兩個主要區(qū)別:

  1. 使用 const 聲明時,必須分配一個值。
  2. 不能為使用 const 聲明的變量重新賦值。

這可以保證 const 將始終擁有您最初分配給它的值。

// This is legal const React = require('react');  // This is totally not legal const crypto; crypto = require('crypto'); 

塊范圍

let 和 const 與 var 在另一方面有所不同:它們的范圍大小。

當您使用 var 聲明變量時,它在作用域鏈的盡可能高的位置可見 – 通常是在最近的函數(shù)聲明的頂部,或者在全局范圍,如果您在頂層聲明它。

但是,當您使用 let 或 const 聲明變量時,它會盡可能本地可見 – >?在最近的街區(qū)內(nèi)。

塊是由大括號分隔的一段代碼,如 if/else 塊、for?循環(huán),以及顯式“阻止”的代碼塊,如本段代碼所示。

"use strict";  {   let foo = "foo";   if (foo) {       const bar = "bar";       var foobar = foo + bar;        console.log("I can see " + bar + " in this bloc.");   }      try {     console.log("I can see " + foo + " in this block, but not " + bar + ".");   } catch (err) {     console.log("You got a " + err + ".");   } }  try {   console.log( foo + bar ); // Throws because of 'foo', but both are undefined } catch (err) {   console.log( "You just got a " + err + "."); }  console.log( foobar ); // Works fine 

如果您在塊內(nèi)使用 const 或 let 聲明變量,則該變量在塊內(nèi)可見,且僅 分配后。

但是,使用 var 聲明的變量在盡可能遠的地方可見 – 在本例中是在全局范圍內(nèi)。

如果您對 let 和 const 的具體細節(jié)感興趣,請查看 Rauschmayer 博士在《探索 ES6:變量和范圍》中對它們的介紹,并查看有關它們的 MDN 文檔。

詞法 this 和箭頭函數(shù)

從表面上看,this 似乎與范圍沒有太大關系。事實上,JavaScript 并沒有根據(jù)我們在這里討論的范圍規(guī)則來解析 this 的含義。

至少,通常不會。眾所周知,JavaScript 不會根據(jù)您使用該關鍵字的位置來解析 this 關鍵字的含義:

var foo = {     name: 'Foo',     languages: ['Spanish', 'French', 'Italian'],     speak : function speak () {         this.languages.foreach(function(language) {             console.log(this.name + " speaks " + language + ".");         })     } };  foo.speak(); 

我們大多數(shù)人都認為 this 表示 foo 在 forEach 循環(huán)內(nèi),因為這就是它在循環(huán)之外的含義。換句話說,我們期望 JavaScript 能夠解析 this 詞法的含義。

但事實并非如此。

相反,它會在您定義的每個函數(shù)中創(chuàng)建一個 this,并根據(jù)您如何調(diào)用該函數(shù)來決定其含義 -不是您定義它的位置

第一點類似于在子作用域中重新定義任何變量的情況:

function foo () {     var bar = "bar";     function baz () {         // Reusing variable names like this is called "shadowing"          var bar = "BAR";         console.log(bar); // BAR     }     baz(); }  foo(); // BAR 

將 bar 替換為 this,整個事情應該立即清楚!

傳統(tǒng)上,要讓 this 按照我們期望的普通舊詞法范圍變量的方式工作,需要以下兩種解決方法之一:

var foo = {     name: 'Foo',     languages: ['Spanish', 'French', 'Italian'],     speak_self : function speak_s () {         var self = this;         self.languages.forEach(function(language) {             console.log(self.name + " speaks " + language + ".");         })     },     speak_bound : function speak_b () {         this.languages.forEach(function(language) {             console.log(this.name + " speaks " + language + ".");         }.bind(foo)); // More commonly:.bind(this);     } }; 

在 speak_self 中,我們將 this 的含義保存到變量 self 中,并使用該變量來得到我們想要的參考。在 speak_bound 中,我們使用 bind 來永久將 this 指向給定對象

ES2015 為我們帶來了一種新的選擇:箭頭函數(shù)。

與“普通”函數(shù)不同,箭頭函數(shù)不會通過設置自己的值來隱藏其父作用域的 this 值。相反,他們從詞匯上解析其含義。

換句話說,如果您在箭頭函數(shù)中使用 this,JavaScript 會像查找任何其他變量一樣查找其值。

首先,它檢查本地范圍內(nèi)的 this 值。由于箭頭函數(shù)沒有設置一個,因此它不會找到一個。接下來,它檢查 this 值的范圍。如果找到,它將使用它。

這讓我們可以像這樣重寫上面的代碼:

var foo = {     name: 'Foo',     languages: ['Spanish', 'French', 'Italian'],     speak : function speak () {         this.languages.forEach((language) => {             console.log(this.name + " speaks " + language + ".");         })     } };??? 

如果您想了解有關箭頭函數(shù)的更多詳細信息,請查看 Envato Tuts+ 講師 Dan Wellman 的有關 JavaScript ES6 基礎知識的精彩課程,以及有關箭頭函數(shù)的 MDN 文檔。

結(jié)論

到目前為止,我們已經(jīng)了解了很多內(nèi)容!在本文中,您了解到:

  • 變量在編譯期間注冊到其作用域,并在執(zhí)行期間與其賦值相關聯(lián)。
  • 在賦值之前引用使用?let 或 class=”inline”>const 聲明的變量會引發(fā) ReferenceError,并且此類變量是范圍到最近的塊。
  • 箭頭函數(shù) 允許我們實現(xiàn) this 的詞法綁定,并繞過傳統(tǒng)的動態(tài)綁定。

您還了解了提升的兩條規(guī)則:

  • 第一條提升規(guī)則:函數(shù)表達式和 var 聲明在其定義的整個范圍內(nèi)都可用,但其值為 undefined?直到您的賦值語句執(zhí)行。
  • 第二條提升規(guī)則:函數(shù)聲明的名稱及其主體在定義它們的范圍內(nèi)可用。

下一步最好是利用 JavaScript 作用域的新知識來理解閉包。為此,請查看 Kyle Simpson 的 Scopes & Closures。

最后,關于 this 有很多話要說,我無法在此介紹。如果該關鍵字看起來仍然像是黑魔法,請查看此和對象原型來了解它。

與此同時,利用您所學到的知識并減少編寫錯誤!

學習 JavaScript:完整指南

我們構(gòu)建了一個完整的指南來幫助您學習 JavaScript,無論您是剛剛開始作為 Web 開發(fā)人員還是想探索更高級的主題。

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點贊10 分享