深度解析JavaScript中作用域與作用域鏈
概述
作用域(Scope)與作用域鏈(Scope Chain)是 JavaScript 的核心概念,它們決定了變量的可訪問范圍、生命周期,以及代碼運(yùn)行時(shí)變量查找的規(guī)則,理解這兩個概念,可以回答我們 “變量在這里為什么能訪問”,“為什么這里訪問到的變量值是 undefined” 等諸多疑問,同時(shí)還能幫助我們在開發(fā)過程中規(guī)避變量污染、提升代碼的可維護(hù)性,對于 ES Module 等模塊方案也可以有更好的理解。
作用域類型
全局作用域(Global Scope)
全局作用域是最頂層的作用域,代碼中未被任何函數(shù)或塊級結(jié)構(gòu)包裹的變量 / 函數(shù),都屬于全局作用域。
- 生命周期:與程序運(yùn)行周期一致,頁面加載時(shí)創(chuàng)建,頁面關(guān)閉時(shí)銷毀;
- 瀏覽器環(huán)境:全局作用域的變量會掛載到
window? 對象上(Node.js 環(huán)境掛載到global對象); - 訪問范圍:代碼中的任何位置都能訪問;
// 全局變量:屬于全局作用域
const globalVar = "我是全局變量";
function bar() {
// 可訪問全局變量
console.log(globalVar); // 輸出:我是全局變量
}
bar();
console.log(window.globalVar); // 瀏覽器環(huán)境輸出:我是全局變量
函數(shù)作用域(Function Scope)
函數(shù)作用域是指變量 / 函數(shù)僅在定義它們的函數(shù)內(nèi)部可訪問,函數(shù)外部無法直接訪問。
- 生命周期:函數(shù)調(diào)用時(shí)創(chuàng)建,函數(shù)執(zhí)行結(jié)束后銷毀(閉包除外);
- 訪問范圍:僅函數(shù)內(nèi)部及嵌套的子函數(shù)可訪問;
- 核心特性:函數(shù)參數(shù)也屬于函數(shù)作用域的變量。
function foo() {
// 函數(shù)作用域變量:僅foo內(nèi)部可訪問
const funcVar = "我是函數(shù)作用域變量";
// 嵌套函數(shù)可訪問外層函數(shù)作用域的變量
function inner() {
console.log(funcVar); // 輸出:我是函數(shù)作用域變量
}
inner();
}
foo();
console.log(funcVar); // 報(bào)錯:funcVar is not defined(外部無法訪問)
塊級作用域(Block Scope)
塊級作用域是 ES6 引入的特性,由 { }? 包裹的代碼塊(如 if?、for?、while?、try/catch?,以及直接用 { }? 定義的塊)形成,僅 let/const 聲明的變量會綁定到塊級作用域。
- 生命周期:代碼塊執(zhí)行時(shí)創(chuàng)建,執(zhí)行結(jié)束后銷毀;
- 訪問范圍:僅塊內(nèi)部可訪問;
- 核心特性:不存在變量提升(或稱為暫時(shí)性死區(qū)),避免變量泄露。
if (true) {
// let聲明的變量:綁定到塊級作用域
let blockVar = "我是塊級作用域變量";
const blockConst = "塊級常量";
// var聲明的變量:不綁定塊級作用域,屬于外層作用域(如全局)
var nonBlockVar = "我不屬于塊級作用域";
}
console.log(blockVar); // 報(bào)錯:blockVar is not defined
console.log(blockConst); // 報(bào)錯:blockConst is not defined
console.log(nonBlockVar); // 輸出:我不屬于塊級作用域(全局變量)
雖然我這里劃分了三種類型的作用域,但其實(shí)是兩個大類型:全局和局部,函數(shù)作用域和塊級作用域就屬于是局部的作用域。
作用域的本質(zhì)
作用域本質(zhì)上是定義了一套?變量的訪問規(guī)則,用于確定在代碼執(zhí)行過程中,某個變量何時(shí)被創(chuàng)建、何時(shí)被銷毀,在何處可以被訪問、修改。
JavaScript 的作用域是?靜態(tài)作用域?(通常也稱為詞法作用域), 靜態(tài)作用域是在代碼定義階段而非運(yùn)行階段確定的,通俗的說就是,你把變量寫在代碼的哪里,它的作用域就在哪個范圍內(nèi),舉個例子:
function outer() {
const a = 1; // 定義在 outer 作用域中的變量
function inner() {
console.log(a); // inner函數(shù)定義時(shí),嵌套在outer內(nèi)部,所以可以訪問這個 a 變量
}
return inner;
}
console.log(a); // 這個 log 是在全局作用域下,是在 outer 作用域外的,所以訪問不到 outer 中的變量
// 但這里有一個注意點(diǎn),a 雖然訪問不到 outer 中的變量 a,但是他可以訪問到全局作用域的 a,由于未定義,所以輸出是 undefined (非嚴(yán)格模式下)
const fn = outer();
fn(); // 輸出:1(即使 fn 在 outer 外部執(zhí)行,仍能訪問 outer 的 a,這就是經(jīng)典的閉包)
一般和作用域同時(shí)提到的還有?執(zhí)行上下文,這是兩個概念,需要注意區(qū)分:
- 作用域是靜態(tài)的,代碼定義時(shí)確定,不關(guān)注代碼的執(zhí)行
- 執(zhí)行上下文是動態(tài)的,在代碼執(zhí)行的過程中動態(tài)的創(chuàng)建,包含
this,變量對象,作用域鏈等信息,在每次調(diào)用函數(shù)時(shí)都會創(chuàng)建新的執(zhí)行上下文。
作用域鏈
作用域鏈,顧名思義,就是一個鏈表,是?由當(dāng)前作用域和外層作用域組成的鏈表,用于解析變量引用。當(dāng)代碼在某個作用域訪問一個變量時(shí),會從當(dāng)前作用域出發(fā)逐級向外層作用域去尋找變量,舉個例子:
// 全局作用域
const globalVar = "全局變量";
function outer() {
// 外層函數(shù)作用域
const outerVar = "外層變量";
function inner() {
// 內(nèi)層函數(shù)作用域
const innerVar = "內(nèi)層變量";
console.log(innerVar); // 查找鏈:【找到】inner 作用域
console.log(outerVar); // 查找鏈:inner 作用域 -> 【找到】outer 作用域
console.log(globalVar); // 查找鏈:innter 作用域 -> outer 作用域 -> 【找到】全局作用域
}
inner();
}
outer();
在 inner? 函數(shù)中訪問 globalVar 變量時(shí)就是沿著作用域鏈逐級查找的。
鏈的構(gòu)建過程
函數(shù)定義時(shí):JavaScript 引擎會為函數(shù)關(guān)聯(lián)一個 [[Scopes]] 內(nèi)部屬性,存儲函數(shù)定義時(shí)所處的所有外層作用域;
函數(shù)調(diào)用時(shí):創(chuàng)建該函數(shù)的執(zhí)行上下文,此時(shí)作用域鏈會被初始化:
- 鏈的第一個元素是當(dāng)前執(zhí)行上下文的變量對象(存儲當(dāng)前作用域的變量、函數(shù));
- 后續(xù)元素是函數(shù)
[[Scopes]]? 屬性中的外層作用域變量對象,按從內(nèi)到外的順序排列;
作用域鏈固化:作用域鏈在執(zhí)行上下文創(chuàng)建時(shí)確定,后續(xù)不會因代碼執(zhí)行而改變。
以本節(jié)開始的代碼為例:
- ?
inner? 函數(shù)定義時(shí),引擎會為其添加一個[[Scopes]]? 屬性,其中包含:外層函數(shù)作用域(outer)、全局作用域; - ?
inner調(diào)用時(shí),會創(chuàng)建執(zhí)行上下文,該上下文的作用域鏈為:[inner 變量對象({innerVar: "內(nèi)層變量"})→ outer 變量對象({outerVar: "外層變量"})→ 全局變量對象({globalVar: "全局變量"})],箭頭表示鏈表的方向及連接。
變量查找規(guī)則
當(dāng)訪問一個變量時(shí),JavaScript 引擎的查找步驟為:
- 從作用域鏈的第一個元素(當(dāng)前作用域)開始查找
- 若找到變量,直接返回其值(或引用),停止查找
- 若未找到,繼續(xù)查找作用域鏈的下一個元素(外層作用域)
- 依次類推,直到找到變量或遍歷完整個作用域鏈
- 若遍歷完所有作用域仍未找到,拋出
ReferenceError(變量未定義)、
需要注意的是,修改是直接作用到這個查找到的變量上的,比如:
const x = 1; // 全局變量
function foo() {
x = 2; // 修改的是全局變量x,而非創(chuàng)建局部變量
console.log(x); // 輸出:2
}
foo();
console.log(x); // 輸出:2(全局變量被修改)
在過去不默認(rèn)聲明嚴(yán)格模式的時(shí)候,我們在 foo 函數(shù)里面 x =2 會隱式的創(chuàng)建一個全局變量,在編碼的時(shí)候是一個很大的坑,但是現(xiàn)在的項(xiàng)目基本都默認(rèn)嚴(yán)格模式了,這種使用方式就會報(bào)錯,提前為我們規(guī)避一些問題。
應(yīng)用
閉包
閉包應(yīng)該是作用域鏈的一個最典型、廣泛的應(yīng)用了,他是由函數(shù)和定義時(shí)的詞法作用域組合成的,他允許函數(shù)在外部作用域執(zhí)行時(shí),依舊能夠訪問到該函數(shù)定義時(shí)的局部變量(函數(shù)作用域內(nèi)的變量),舉個例子:
function createCounter() {
let count = 0; // 外層函數(shù)作用域變量
// 內(nèi)部函數(shù)引用了count,且被返回(導(dǎo)出到外部)
return function increment() {
count++;
console.log(count);
};
}
// increment在createCounter作用域之外執(zhí)行
const counter = createCounter();
counter(); // 輸出:1
counter(); // 輸出:2
counter(); // 輸出:3
從作用域的視角上看:
- ?
increment? 函數(shù)定義的時(shí)候,其對應(yīng)的[[Scopes]]? 屬性存儲的是createCounter? 的作用域和一個最外層的全局作用域,也就是說這時(shí)候相關(guān)的作用域就已經(jīng)被這個increment函數(shù)持有了。 - 在
createCounter? 執(zhí)行結(jié)束后,該函數(shù)的上下文環(huán)境被銷毀,但是由于increment? 依舊持有這個函數(shù)的作用域,而且increment? 被外部的 counter 變量所引用,不能被 GC,所以increment? 這時(shí)候也還存在著,并且此時(shí)有個別名counter。 - 當(dāng)
counter被調(diào)用的時(shí)創(chuàng)建的執(zhí)行上下文的作用域鏈為:[increment 變量對象 → createCounter 作用域變量對象 → 全局變量對象]。
由于這樣的鏈?zhǔn)疥P(guān)系和引用的持有,最終形成了閉包。
變量遮蔽
內(nèi)存作用域和外層作用域存在同名變量時(shí),內(nèi)層的變量會遮蔽外層變量,在查找時(shí)優(yōu)先訪問內(nèi)層變量。
const x = 10; // 外層變量
function bar() {
const x = 20; // 內(nèi)層變量,遮蔽外層x
console.log(x); // 輸出:20(訪問內(nèi)層x)
}
bar();
console.log(x); // 輸出:10(訪問外層x)
模塊化方案
ES Module 的核心就是模塊級作用域:
每個模塊都是一個獨(dú)立的詞法作用域,這意味著頂層的變量不會再自動掛載到 window 對象上了,模塊內(nèi)的變量/函數(shù)僅在模塊內(nèi)可以訪問。
需要通過export 導(dǎo)出變量/函數(shù),其他模塊通過 import 導(dǎo)入。
這里有沒有感覺很像閉包,必須將函數(shù)作用域的內(nèi)容 return 后,才可以在外層作用域使用。
一些問題
變量提升
在代碼執(zhí)行前,JavaScript 引擎會將 var? 聲明的變量提升到作用域頂部,提升后的值為undefined,將函數(shù)聲明提升到作用域頂部,值為函數(shù)本身。
值得注意的是,變量提升僅在當(dāng)前作用域上生效,不會出現(xiàn)跨作用域提升的情況,如下:
console.log(a); // 輸出:undefined(var聲明的變量提升)
var a = 1;
function foo() {
console.log(b); // 輸出:undefined(函數(shù)作用域內(nèi)的變量提升)
var b = 2;
}
foo();
console.log(b); // 報(bào)錯:b is not defined(b的提升僅在foo作用域內(nèi))
自 ES6 后,很少會在代碼中再使用 var 關(guān)鍵字了,基本用的是 const/let? 來聲明變量,因?yàn)?code>let/const?聲明的變量不會被提升,而是存在?暫時(shí)性死區(qū),即從作用域開始到變量聲明前,訪問該變量會報(bào)錯。這是塊級作用域的特性,避免了變量提升導(dǎo)致的邏輯混亂。
全局作用域污染
function badFunc() {
// 未聲明直接賦值,隱式創(chuàng)建全局變量
unDeclaredVar = "我是污染的全局變量";
}
badFunc();
console.log(window.unDeclaredVar); // 輸出:我是污染的全局變量
對于這種沒有聲明就直接賦值的寫法,在非嚴(yán)格模式下,會隱式的創(chuàng)建一個全局變量,導(dǎo)致全局作用域被污染。
總結(jié)
理解了 JavaScript 的作用域和作用域鏈對我們理解閉包、模塊化、高階函數(shù)等特性能夠有更好的幫助,也能讓我們在實(shí)際開發(fā)中,更合理運(yùn)用塊級作用域(let/const)、模塊化(ES Module),規(guī)避變量污染、邏輯混亂等問題,寫出更健壯、可擴(kuò)展的 JavaScript 代碼。
作用域(Scope)與作用域鏈(Scope Chain)是 JavaScript 的核心概念,它們決定了變量的可訪問范圍、生命周期,以及代碼運(yùn)行時(shí)變量查找的規(guī)則,理解這兩個概念,可以回答我們 “變量在這里為什么能訪問”,“為什么這里的變量是 undefined” 等諸多疑問,還能幫助我們在開發(fā)過程中規(guī)避變量污染、提升代碼的可維護(hù)性,對于 ES Module 等模塊方案有更好的理解。
作用域的本質(zhì)
很多開發(fā)者會把作用域簡單理解為 “變量的存儲位置”,但這只說對了一半。作用域的核心是一套 “變量訪問規(guī)則” —— 它定義了在代碼的哪個位置可以訪問哪些變量,同時(shí)也隱含了變量的 “生存空間”(即變量何時(shí)被創(chuàng)建、何時(shí)被銷毀)。
到此這篇關(guān)于深度解析JavaScript中作用域與作用域鏈的文章就介紹到這了,更多相關(guān)JavaScript作用域與作用域鏈內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS實(shí)現(xiàn)數(shù)組/對象數(shù)組刪除其中某一項(xiàng)
這篇文章主要介紹了JS實(shí)現(xiàn)數(shù)組/對象數(shù)組刪除其中某一項(xiàng),具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09
簡介EasyUI datagrid editor combogrid搜索框的實(shí)現(xiàn)
這篇文章主要介紹了EasyUI datagrid editor combogrid搜索框的實(shí)現(xiàn),涉及到EasyUI中combogrid的使用方法的相關(guān)知識,非常具有參考價(jià)值,需要的朋友可以參考下2016-04-04

