詳解JavaScript中的作用域
什么是作用域
JS中的作用域是一個存儲變量、函數(shù)以及對象的位置,每個變量、函數(shù)和對象都被存儲在一個特定的作用域中,它是指變量和函數(shù)在代碼中的可訪問范圍,作用域決定了代碼中哪些部分可以訪問特定的變量和函數(shù),通過作用域,我們可以將變量和函數(shù)封裝在不同的作用域中,使其在合適的范圍內(nèi)可訪問
就像上圖中的示例,F(xiàn)oo1無法獲取Foo2中的var4變量
作用域類型
常見的作用域類型包括全局作用域(Global Scope)、局部作用域(Local Scope)和塊級作用域(Block Scope),JS也不例外
全局作用域
全局作用域是在整個代碼中都可訪問的作用域。在瀏覽器環(huán)境中,全局作用域通常是指window對象;在node環(huán)境下,則是globalThis或global
在全局作用域中聲明的變量或函數(shù)是全局變量或全局函數(shù),在代碼任何地方都可以訪問和使用
比如上面圖示中的Foo4可以訪問到全局作用域的變量及函數(shù)
const var1 = "var1"; const var2 = "var2"; function Foo1() { const var3 = "var3"; } function Foo2() { const var4 = "var4"; function Foo3() {} } function Foo4() { console.log(var1, var2, Foo1, Foo2); // var1 var2 [Function: Foo1] [Function: Foo2] } Foo4();
局部作用域
JS中的局部作用域一般代指函數(shù)作用域(Function Scope),它是在函數(shù)內(nèi)部聲明的作用域,函數(shù)內(nèi)部的變量和函數(shù)只能在函數(shù)內(nèi)部訪問,外部無法直接訪問
就像上述Foo3的效果,可以訪問到全局以及當(dāng)前所在函數(shù)的作用域的變量及函數(shù)
const var1 = "var1"; const var2 = "var2"; function Foo1() { const var3 = "var3"; } function Foo2() { const var4 = "var4"; function Foo3() { console.log(var4, Foo3); // var4 [Function: Foo3] } Foo3(); } Foo2();
塊級作用域
塊級作用域是在代碼塊(通常是由大括號{}包裹起來的部分)內(nèi)聲明的作用域。比如if(){...}、for(...){...}、try{...}等
ES6之前
在ES6之前,由于變量都是使用var聲明的,所以沒有塊級作用域此類概念,只有全局作用域和函數(shù)(局部)作用域。那么需要模擬塊級作用域如何怎么操作呢?
答案是使用立即執(zhí)行函數(shù)表達式(IIFE):通過將代碼包裝在匿名函數(shù)中并立即執(zhí)行該函數(shù),可以創(chuàng)建一個獨立的作用域,使得內(nèi)部聲明的變量在函數(shù)外部不可訪問
(function () { var var5 = "var5"; console.log(var5); // var5 })(); console.log(var5); // var5 is not defined
ES6以后
在es6以后,官方推出了塊級作用域的概念,使用let和const關(guān)鍵字聲明的變量具有塊級作用域,它們只能在聲明的代碼塊內(nèi)部訪問。
if (true) { let var5 = "var5"; console.log(var5); // var5 } console.log(var5); // var5 is not defined
作用域鏈
作用域鏈(Scope Chain)是JS用于解析標(biāo)識符(變量和函數(shù))的機制,它是由多個嵌套的作用域組成的,它決定了變量和函數(shù)的查找順序。
上面我們說到局部作用域時談到的Foo3可以訪問全局以及當(dāng)前函數(shù)作用域中的標(biāo)識符這個特點就歸功于作用域鏈這個概念。當(dāng)訪問一個變量時,JS引擎會先從當(dāng)前作用域開始查找,如果找不到這個名稱的標(biāo)識符則繼續(xù)向上一級作用域查找,直到找到變量或達到全局作用域為止,如果在全局作用域中仍然找不到,則認為該標(biāo)識符未定義。
變量提升
變量提升的概念出現(xiàn)在面試題中的頻率十分高,對于開發(fā)者來說也是不可忽略的重要知識點;
基礎(chǔ)概念
變量提升是JS在代碼執(zhí)行前將變量和函數(shù)聲明提升到作用域頂部的行為,它由JavaScript引擎在代碼執(zhí)行前的編譯階段處理。變量提升影響了整個作用域范圍內(nèi)的代碼,它允許我們在聲明之前使用變量,但是需要注意一點:只有變量聲明被提升,賦值不會提升。了解了上述概念后,我們思考下面的代碼:
console.log(var6); // undefined var var6 = 10;
上面的代碼相當(dāng)于
var var6; console.log(var6); // undefined var6 = 10;
優(yōu)先級問題
當(dāng)同一個作用域中同時出現(xiàn)同名的函數(shù)和變量時,函數(shù)提升的優(yōu)先級更高,也就是說函數(shù)會在變量之上聲明,參考下面的代碼
var a = 10; function a() {} console.log(a); // 10
可以看成是
var a; // 函數(shù)a var a; // 變量a a = function () {};// 使用function聲明函數(shù)可以看成是聲明+賦值 a = 10; console.log(a); // 10
何以見得?思考以下代碼
function a() {} var a; console.log(a); // [Function: a]
當(dāng)把a的賦值去除時,函數(shù)的賦值順序就可以得到驗證,相當(dāng)于:
var a; // 函數(shù)a var a; // 變量a a = function () {}; console.log(a); // [Function: a]
閉包
定義
當(dāng)函數(shù)開始執(zhí)行時,函數(shù)中的變量以及函數(shù)會壓入棧中,那么此時如果當(dāng)前的作用域中有另一個函數(shù)正在使用該作用域的變量,該變量占用的內(nèi)存也不會被垃圾回收機制回收,這個現(xiàn)象就是閉包
換句話說,閉包是指函數(shù)能夠"記住"并訪問其創(chuàng)建時的詞法環(huán)境,在函數(shù)定義的詞法作用域之外執(zhí)行同樣適用
思考以下代碼
const foo = (function iife() { const num = 10; function foo() { return num; } return foo; })(); console.log(foo()); // 10
上述代碼中使用立即執(zhí)行函數(shù)iife作為外部函數(shù)的作用域,它返回內(nèi)部函數(shù)foo,而foo函數(shù)使用了iife函數(shù)中的num變量,形成了閉包,最后在iife函數(shù)的外部使用foo時依然可以訪問num變量
特點
- 即使外部函數(shù)已經(jīng)執(zhí)行完畢,內(nèi)部函數(shù)依然可以訪問外部函數(shù)作用域中的變量(當(dāng)棧將函數(shù)彈出時,變量依然處于內(nèi)存中)
- 閉包可以持有對外部變量的引用,使得外部變量的值在內(nèi)部函數(shù)中保持活動狀態(tài)(不被垃圾回收機制回收)
- 閉包中的內(nèi)部函數(shù)可以修改并更新外部變量的值
- 閉包的函數(shù)可以獲取到創(chuàng)建時的整個作用域鏈的標(biāo)識符
- 閉包可能會導(dǎo)致內(nèi)存泄漏,被閉包引用的變量無法被垃圾回收機制處理
使用場景
封裝私有變量
JS中沒有TS的private關(guān)鍵詞,無法直接定義私有變量,但是可以通過閉包產(chǎn)生私有環(huán)境作用域(ES2022后引入了#關(guān)鍵字,用于定義私有變量,相比于使用閉包,更直觀和方便)
const Animal = (function () { const name = "dog"; function Animal() {} Animal.prototype.getName = function () { return name; }; return Animal; })(); console.log(new Animal().getName()); // dog
延長變量周期
延長變量的生命周期也是閉包的特性之一,該效果通過內(nèi)部函數(shù)對外部作用域的可訪問性實現(xiàn)
function delayMessage(msg) { return function () { return msg; }; } const msg = delayMessage("msg"); console.log(msg()); // msg
模塊化、命名空間
我們使用JS中的立即執(zhí)行函數(shù)實現(xiàn)了命名空間功能,達到模塊化的效果
var moduleA = (function () { var privateVariable = "Hello"; // 私有函數(shù) function privateMethod() { console.log(privateVariable); } return { // 公共函數(shù) publicMethod: function () { privateMethod(); }, }; })(); moduleA.publicMethod(); // Hello console.log(moduleA.privateVariable); // undefined
緩存
閉包還可以用于創(chuàng)建緩存函數(shù),以提高函數(shù)的執(zhí)行效率。緩存函數(shù)可以將輸入?yún)?shù)與其對應(yīng)的結(jié)果保存在內(nèi)部,避免重復(fù)計算
function createCache() { var cache = {}; return function (key, val) { if (!cache[key]) { cache[key] = val; console.log("保存"); } return cache[key]; }; } var cacheModule = createCache(); cacheModule("num1", "123"); // 保存 cacheModule("num2", "456"); // 保存 cacheModule("num1", "123"); cacheModule("num2", "456");
ES6的作用域
在ES6以后引入了const、let和箭頭函數(shù),這三者對作用域分別有什么影響呢?
const、let
塊級作用域
上面說到const、let的出現(xiàn)奠定了JS中的塊級作用域的概念,使if、for等語句中也存在作用域這一功能;然而不僅僅是條件語句、循環(huán)語句,使用{...}定義的范圍都是塊級作用域,這意味著在塊級作用域內(nèi)部聲明的變量只在該作用域內(nèi)有效,并且在作用域外部無法訪問
{ let msg = "hello"; console.log(msg); // hello } console.log(msg); // msg is not defined
變量提升
使用let和const聲明的變量不會被提升到作用域的頂部,它們只能在聲明后才能被訪問;這點與var不太一樣
暫時性死區(qū)
暫時性死區(qū)指的是在變量聲明前訪問變量會拋出錯誤。只有在變量聲明語句執(zhí)行完成之后,變量才會進入有效狀態(tài),才能被訪問和使用。這點效果與上面的變量提升效果一樣
不可重復(fù)聲明
在同一個作用域中不能被重復(fù)聲明,否則會報錯;而使用var定義變量時后聲明的會覆蓋先聲明的
這三個特點可以參考下面的代碼:
console.log(var1); // 在賦值前使用了變量“var1” const var1 = 11; let var2 = 22; // 無法重新聲明塊范圍變量“var2” let var2 = 22;
箭頭函數(shù)
箭頭函數(shù)在JavaScript中仍然與普通函數(shù)一樣有函數(shù)作用域的概念
題外話
動態(tài)作用域與詞法作用域
作用域的種類有兩種:分別是動態(tài)作用域和詞法作用域(靜態(tài)作用域)。上述我們介紹的是詞法作用域,什么是詞法作用域?
詞法作用域
詞法作用域是基于代碼的靜態(tài)結(jié)構(gòu)來確定變量的訪問規(guī)則。也就是說它由變量和函數(shù)在代碼中的聲明位置而不是調(diào)用的位置來確定,思考下面的代碼
function foo1() { var var1 = 10; // foo2可訪問的作用域 // 聲明foo2的作用域 return function foo2() { console.log(var1);// 10 }; } var foo2 = foo1(); function foo3() { var var1 = 20; // foo2不可訪問的作用域 // 執(zhí)行foo2的作用域 foo2(); } foo3();
在foo1中聲明了函數(shù)foo2,在foo3中執(zhí)行foo2,可以看到,foo2取的var1是聲明foo2的作用域中的變量(10)。這個現(xiàn)象說明JS采用的是詞法作用域。
動態(tài)作用域
那么反之,如果還是上述代碼,foo2取的var1是執(zhí)行foo2的作用域中的變量(20),就說明語言采用的是動態(tài)作用域
總結(jié)
JavaScript作用域與變量提升是編寫高質(zhì)量代碼所必須掌握的重要概念。本文介紹了作用域的定義、作用和目的,以及JavaScript中的不同作用域類型,包括全局作用域、函數(shù)作用域和塊級作用域。我們還討論了變量提升的概念、原理和影響范圍,以及作用域鏈和閉包的關(guān)系。此外,還探討了ES6的作用域,并對比了其與早期作用域的區(qū)別。最后針對動態(tài)作用域與詞法作用域作了一個簡單的說明。
以上就是詳解JavaScript中的作用域的詳細內(nèi)容,更多關(guān)于JavaScript作用域的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
前端高頻面試題之JS中堆和棧的區(qū)別和瀏覽器的垃圾回收機制
本文給大家分享前端高頻面試題JS中堆和棧的區(qū)別和瀏覽器的垃圾回收機制,本文分文別類給大家介紹了棧(stack)和堆(heap)的區(qū)別基本類型和引用類型的相關(guān)知識,瀏覽器垃圾回收機制包括基本概念給大家介紹的非常詳細,需要的朋友參考下吧2023-10-10javascript高仿熱血傳奇游戲?qū)崿F(xiàn)代碼
這篇文章主要介紹了javascript高仿熱血傳奇游戲的實現(xiàn)代碼,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2018-02-02