理解JavaScript中的作用域
Aug 31, 2023 pm 08:17 PM範圍,或確定變數(shù)所在位置的一組規(guī)則,是任何程式語言的最基本概念之一。事實上,它是如此基本,以至於我們很容易忘記這些規(guī)則是多麼微妙!
確切理解JavaScript 引擎如何「思考」作用域將使您避免編寫提升可能導致的常見錯誤,讓您做好準備專注於閉包,並讓您離永遠不再編寫錯誤更近一步再次。
...無論如何,它會幫助您理解提升和關閉。
在本文中,我們將了解:
- JavaScript 中作用域的基礎知識
- 解釋器如何決定哪些變數(shù)屬於哪個作用域
- 吊掛的實際運作原理
- ES6 關鍵字
let
和const
如何改變遊戲規(guī)則
讓我們深入探討。
如果您有興趣了解有關 ES6 的更多資訊以及如何利用語法和功能來改進和簡化 JavaScript 程式碼,為什麼不看看這兩門課程:
詞法範圍
如果您之前寫過一行 JavaScript,您就會知道定義變數(shù)的位置決定了您可以使用的位置> 他們。變數(shù)的可見性取決於原始碼的結構,這一事實稱為詞法?? 範圍。
在 JavaScript 中建立作用域有三種方法:
- 建立函數(shù)。在函數(shù)內(nèi)部宣告的變數(shù)僅在該函數(shù)內(nèi)部可見,包括在巢狀函數(shù)中。
-
使用
let
或const
在程式碼區(qū)塊內(nèi)宣告變數(shù)。此類聲明僅在區(qū)塊內(nèi)可見。 -
建立
catch
區(qū)塊。不管你相信與否,這實際上確實創(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 如何確定哪些變數(shù)屬於哪個作用域。
編譯過程:鳥瞰
當您執(zhí)行一段 JavaScript 時,會發(fā)生兩件事使其正常運作。
- 首先,編譯您的原始碼。
- 然後,編譯後的程式碼將會執(zhí)行。
在編譯步驟期間,JavaScript 引擎:
- 記下所有變數(shù)名稱
- 將它們註冊到適當?shù)墓爣?/li>
- 為自己的價值觀保留空間
只有在執(zhí)行期間,JavaScript 引擎才真正將變數(shù)引用的值設為等於其賦值。在那之前,它們是 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"
。接下來,它確定將變數(shù)保存到的範圍。因為我們位於腳本的頂層,所以它意識到我們處於全域範圍。然後,它將變數(shù) first_name
儲存到全域範圍,並將其值初始化為 undefined
。
其次,編譯器會讀取帶有 function popup (first_name)
的行。因為 function 關鍵字是該行的第一個內(nèi)容,因此它為函數(shù)建立一個新作用域,將函數(shù)的定義註冊到全域作用域,並查看內(nèi)部以尋找變數(shù)宣告。
果然,編譯器找到了一個。由於我們函數(shù)的第一行有var last_name = "Sengstacke"
,因此編譯器會將變數(shù)last_name
儲存到</em> class=" inline">popup
—不到全域範圍— 並將其值設為undefined
。
由於函數(shù)內(nèi)不再有變數(shù)聲明,編譯器將退回到全域作用域。由於不再有變數(shù)聲明,因此此階段已完成。
請注意,我們實際上還沒有運行任何東西。 此時編譯器的工作只是確保它知道每個人的名字;它不關心他們做什麼。
此時,我們的程式知道:
- 全局范圍內(nèi)有一個名為
first_name
的變量。 - 全局范圍內(nèi)有一個名為
popup
的函數(shù)。 - 在
popup
范圍內(nèi)有一個名為last_name
的變量。 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ù),因此引擎:
- 查找
popup
的值 - 查找
first_name
的值 - 將
popup
作為函數(shù)執(zhí)行,并傳遞first_name
的值作為參數(shù)
當執(zhí)行 popup
時,它會經(jīng)歷相同的過程,但這次是在函數(shù) popup
內(nèi)。它:
- 查找名為
last_name
的變量 - 將
last_name
的值設置為等于"Sengstacke"
- 查找
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!"); }
如果運行此代碼,您會注意到三件事:
- 在分配之前,您可以引用
foo
,但其值為undefined
。 - 您可以在定義之前調(diào)用
已損壞的
,但您會收到TypeError
。 - 您可以在定義之前調(diào)用
bar
,它會按需要工作。
提升是指 JavaScript 使我們聲明的所有變量名稱在其作用域內(nèi)的任何地方都可用,包括在我們分配給它們之前 .
代碼段中的三種情況是您在自己的代碼中需要注意的三種情況,因此我們將一一逐步介紹它們。
提升變量聲明
請記住,當 JavaScript 編譯器讀取像 var foo = "bar"
這樣的行時,它會:
- 將名稱
foo
注冊到最近的范圍 - 將
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讀取時,它更有意義,您不覺得嗎?
查看:
- 變量聲明和函數(shù)表達式的名稱在其整個范圍內(nèi)都可用,但它們的值在賦值之前為
undefined
。 - 函數(shù)聲明的名稱??和定義在其整個范圍內(nèi)都可用,甚至在其定義之前。
現(xiàn)在讓我們來看看兩個工作方式稍有不同的新工具:let
和 const
。
<strong>let</strong>
、<strong></strong>const
和臨時死區(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ū)別:
- 使用
const
聲明時,必須分配一個值。 - 您不能為使用
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)可見,且僅< /em> 分配后。
但是,使用 var
聲明的變量在盡可能遠的地方可見 - 在本例中是在全局范圍內(nèi)。
如果您對 let
和 const
的具體細節(jié)感興趣,請查看 Rauschmayer 博士在《探索 ES6:變量和范圍》中對它們的介紹,并查看有關它們的 MDN 文檔。
詞法 <strong>this</strong>
和箭頭函數(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 文檔。
結論
到目前為止,我們已經(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:完整指南
#我們建立了一個完整的指南來幫助您學習 JavaScript,無論您是剛開始擔任 Web 開發(fā)人員還是想探索更高級的主題。
以上是理解JavaScript中的作用域的詳細內(nèi)容。更多資訊請關注PHP中文網(wǎng)其他相關文章!

熱AI工具

Undress AI Tool
免費脫衣圖片

Undresser.AI Undress
人工智慧驅動的應用程序,用於創(chuàng)建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發(fā)環(huán)境

Dreamweaver CS6
視覺化網(wǎng)頁開發(fā)工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

WordPress導致服務器CPU使用率飆升的主要原因包括插件問題、數(shù)據(jù)庫查詢效率低、主題代碼質(zhì)量差或流量激增。 1.首先通過top、htop或控制面板工具確認是否為WordPress引起的高負載;2.進入故障排查模式逐步啟用插件排查性能瓶頸,使用QueryMonitor分析插件執(zhí)行情況並刪除或替換低效插件;3.安裝緩存插件、清理冗餘數(shù)據(jù)、分析慢查詢?nèi)照I以優(yōu)化數(shù)據(jù)庫;4.檢查主題是否存在過度加載內(nèi)容、複雜查詢或缺乏緩存機制等問題,建議用標準主題測試對比並優(yōu)化代碼邏輯。按照上述步驟逐一排查可定位並解

MinifyingJavaScript文件可通過刪除空白、註釋和無用代碼來提升WordPress網(wǎng)站加載速度。 1.使用支持合併壓縮的緩存插件如W3TotalCache,在“Minify”選項中啟用並選擇壓縮模式;2.使用專用壓縮插件如FastVelocityMinify,提供更精細控制;3.手動壓縮JS文件並通過FTP上傳,適用於熟悉開發(fā)工具的用戶。注意部分主題或插件腳本可能與壓縮功能衝突,啟用後需徹底測試網(wǎng)站功能。

優(yōu)化WordPress站點不依賴插件的方法包括:1.使用輕量級主題,如Astra或GeneratePress,避免功能堆砌的主題;2.手動壓縮和合併CSS、JS文件,減少HTTP請求;3.上傳前優(yōu)化圖片,使用WebP格式並控製文件大?。?.配置.htaccess啟用瀏覽器緩存,並接入CDN提升靜態(tài)資源加載速度;5.限製文章修訂版本並定期清理數(shù)據(jù)庫冗餘數(shù)據(jù)。

TransientsAPI是WordPress中用於臨時存儲可自動過期數(shù)據(jù)的內(nèi)置工具,其核心函數(shù)為set_transient、get_transient和delete_transient。相比OptionsAPI,transients支持設置生存時間(TTL),適合緩存API請求結果、複雜計算數(shù)據(jù)等場景。使用時需注意key命名唯一性與命名空間、緩存“懶刪除”機制及對象緩存環(huán)境下可能不持久的問題。典型應用場景包括減少外部請求頻率、控制代碼執(zhí)行節(jié)奏和提升頁面加載性能。

防止評論垃圾信息最有效的方式是通過程序化手段自動識別並攔截。 1.使用驗證碼機制(如GooglereCAPTCHA或hCaptcha)可有效區(qū)分人類與機器人,尤其適合公眾網(wǎng)站;2.設置隱藏字段(Honeypot技術),利用機器人自動填寫特性識別垃圾評論,不影響用戶體驗;3.檢查評論內(nèi)容關鍵詞黑名單,通過敏感詞匹配過濾垃圾信息,需注意避免誤判;4.判斷評論頻率與來源IP,限制單位時間內(nèi)的提交次數(shù)並建立黑名單;5.使用第三方反垃圾服務(如Akismet、Cloudflare)提升識別準確性??筛鶕?jù)網(wǎng)站

在開發(fā)Gutenberg塊時,正確enqueue資產(chǎn)的方法包括:1.使用register_block_type指定editor_script、editor_style和style的路徑;2.在functions.php或插件中通過wp_register_script和wp_register_style註冊資源,並設置正確的依賴和版本;3.配置構建工具輸出合適的模塊格式,並確保路徑一致;4.通過add_theme_support或enqueue_block_assets控制前端樣式的加載邏輯,確保

要添加自定義用戶字段需根據(jù)平臺選擇擴展方式並註意數(shù)據(jù)驗證與權限控制。常見做法包括:1.利用數(shù)據(jù)庫額外表或鍵值對結構存儲信息;2.在前端加入輸入框並與後端集成;3.對敏感數(shù)據(jù)進行格式校驗和訪問權限限制;4.更新接口及模板以支持新字段展示與編輯,同時兼顧移動端適配和用戶體驗。

robots.txt對WordPress網(wǎng)站的SEO至關重要,能引導搜索引擎抓取行為,避免重複內(nèi)容並提升效率。 1.屏蔽如/wp-admin/、/wp-includes/等系統(tǒng)路徑,但避免誤封/uploads/目錄;2.添加Sitemap路徑如Sitemap:https://yourdomain.com/sitemap.xml以幫助搜索引擎快速發(fā)現(xiàn)站點地圖;3.限制/page/和帶參數(shù)的URL以減少爬蟲浪費,但需注意勿封重要歸檔頁;4.避免常見錯誤如誤封全站、緩存插件影響更新及忽略移動端與子域名配
