我們大概經常能聽到“執行環境”、“作用域”、“原型(鏈)”、“執行背景關系”等內容,它們都在描述什么?
JS代碼的運行
我們知道了js是弱型別語言,在運行時才確定變數型別,js引擎在執行js代碼時,也會從上到下進行 詞法分析、語法分析、語意分析 等處理,并在代碼決議完成后生成AST(抽象語法樹),最終根據AST生成CPU可以執行的機器碼并執行,
除此之外,JS引擎在執行代碼時還會進行其它處理,如 V8 中還有兩個階段:
- 編譯階段:該階段會進行執行背景關系的創建,包括創建變數物件(VO)(此時會被初始化為undefined)、建立作用域鏈、確定 this 指向等,每進入一個不同的運行環境,V8 都會創建一個新的執行背景關系,
- 執行階段:將編譯階段中創建的執行背景關系壓入呼叫堆疊,并成為正在運行的執行背景關系,代碼執行結束后,將其彈出呼叫堆疊,(這里有一個VO - AO的程序:JavaScript對變數賦值時變數被用到,此時變數物件會轉為活動物件,轉換后的活動物件才可被訪問)
這就引出了兩個概念:“執行背景關系” 和 “作用域鏈”,
JavaScript執行背景關系
由上面我們可以知道:當js代碼執行一段可執行代碼時,會創建對應的執行背景關系,
首先,js中可執行代碼對應著有一個概念:“執行環境” —— 全域環境、函式環境 和 eval,
其次,對于每個執行背景關系,都有三個重要屬性:
- 變數物件(即“VO”)
- 作用域鏈
- this
我們來看兩段代碼:
var scope="global scope";
function checkscope(){
var scope="local scope";
function f(){
return scope;
}
return f();
}
checkscope();
var scope="global scope";
function checkscope(){
var scope="local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
它們會列印什么?

為什么?答案是它們的執行背景關系堆疊不一樣!
什么是“執行背景關系堆疊”?
當執行一個可執行代碼時,就會提前做準備作業,這里的“準備作業”,專業的說法就是“執行背景關系”,但隨著可執行代碼如函式的增多,如何管理那么多的執行背景關系呢?所以JS引擎創建了執行背景關系堆疊的概念,
我們完全可以用陣列去模擬其行為(堆疊底永遠有一個全域執行背景關系globalContext)
我們定義一個EStack,首先
EStack=[globalContext];
然后來模擬第一段代碼:
EStack.push(<checkscope> functionContext);
EStack.push(<f> functionContext);
EStack.pop();
EStack.pop();
而第二段代碼是這樣的:
EStack.push(<checkscope> functionContext);
EStack.pop();
EStack.push(<f> functionContext);
EStack.pop();
究其原因,你可能需要先研究一下“閉包”的概念了!
這里順便說下“在前端模塊化”中怎么實作“長時間保存資料”?
快取?不,閉包!
JavaScript作用域和作用域鏈
首先,作用域是指程式中定義變數的區域,作用域規定了如何查找變數,也就是確定了當前執行代碼對變數的訪問權限,
作用域有兩種:靜態作用域 和 動態作用域,
JS采用的靜態作用域,也叫“詞法作用域”,函式的作用域在函式定義的時候就確定了,
由上,詞法作用域中的變數,在編譯程序中會產生一個確定的作用范圍,這個作用范圍即“當前的執行背景關系”,在ES5后我們用“詞法環境”替代作用域來描述該執行背景關系,詞法環境由兩個成員組成:
- 自身詞法環境記錄:用于記錄自身詞法環境中的變數物件
- 外部詞法環境參考:用于記錄外層詞法環境中存在的參考
我們依然來看一個例子:
var value=1;
function foo(){
console.log(value);
}
function bar(){
var value=2;
foo();
}
bar();
回看上面的定義,該列印什么?

讓我們分析下執行程序:
執行foo()函式,先從foo函式內部查找是否有區域變數value,如果沒有,就根據定義時的位置,查找上面一層的代碼,也就是value=1.所以結果會列印1,
這里面當然不是如此簡單能概括的,你可以從執行背景關系的角度分析一下,
建立作用域鏈
上面我們說了詞法環境(作用域)的兩個組成,再結合執行背景關系,我們不難發現:通過外部詞法環境的參考,作用域可以順著堆疊層層拓展,建立起從當前環境向外延伸的一條鏈式結構,
再來看一個例子:
function foo(){
console.dir(bar);
var a=1;
function bar(){
a=2;
}
}
console.dir(foo);
foo();
由靜態作用域,全域函式foo創建了一個自身物件的 [[scope]] 屬性
foo[[scope]]=[globalContext];
而當我們執行foo()時,也會先后進入foo函式的定義期和執行期,在foo函式的定義期時,函式bar的 [[scope]] 將會包含全域內置scope和foo的內置scope
bar[[scope]]=[fooContext,globalContext];
這證明了這一點:“JS會通過外部詞法環境參考來創建變數物件的一個作用域鏈,從而保證對執行環境有權訪問的變數和函式的有序訪問,”
讓我們再回頭看看執行背景關系中的那道題,在前面我們說了它們有什么不同,這里說下為什么它們相同地列印了“local scope”:還是那句話“JS采用的是詞法作用域,函式的作用域取決于函式創建的位置” —— JS函式的執行用到了作用域鏈,這個作用域鏈是在函式定義的時候創建的,嵌套的函式 f() 定義在這個作用域鏈里,其中的變數scope一定是指區域變數,不管何時何地執行 f() ,這種系結在執行 f() 時依然有效,
基于作用域鏈的變數查詢
當某個變數無法在自身詞法環境記錄中找到時,可以根據外部詞法環境參考向外層進行尋找,直到最外層的詞法環境中外部詞法環境參考為null,
與此相似的是“物件中基于原型鏈的查找”:
- 原型:每一個JS物件(null 除外)在創建時就會與另一個物件關聯,這個物件就是我們說的原型,每一個物件都會從原型中“繼承”屬性,
- 當讀取實體的屬性時,如果找不到,就會查找與物件關聯的原型中的屬性,如果還找不到,就去找原型的原型,一直到最頂層(
__proto__為null)為止
它們的區別也顯而易見:原型鏈是通過 prototype 屬性建立物件繼承的鏈接;而作用域鏈是指內部函式能訪問到外部函式的閉包,不管直接還是間接,所有函式的作用域鏈最終都鏈接到全域背景關系,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/279609.html
標籤:其他
