前言
如果你是或者你想成為一名合格的前端開發工作者,你必須知道JavaScript程式碼在執行過程,知道執行上下文、作用域、變數提升等相關概念,並且熟練應用到自己的程式碼中。本文參考了你不知道的JavaScript,和JavaScript高階程式設計,以及部分部落格。
正文
1.JavaScript程式碼的執行過程相關概念
js程式碼的執行分為編譯器的編譯和js引擎與作用域執行兩個階段,其中編譯器編譯的階段(預編譯階段)分為分詞/詞法分析、解析/語法分析、程式碼生成三個階段。
(1)在分詞/詞法分析階段,編譯器負責將程式碼進行分割處理,將語句分割成詞法單元流/陣列;
(2)在解析/詞法分析階段,將上一階段的詞法單元流轉換成由元素巢狀組成的符合程式語法結構的抽象語法樹;
(3)在程式碼生成階段,將抽象語法樹轉換成可執行程式碼,並交付給js引擎。
js程式碼執行的三個重要角色:
(1)js引擎:負責程式碼執行的整個過程
(2)編譯器:負責js程式碼語法解析和生成可執行程式碼
(3)作用域:手機並維護所有宣告識別符號,根據特定規則確定當前程式碼對宣告的識別符號的訪問許可權
2. 執行上下文和執行棧
每當js程式碼在執行的時候,它都是在執行上下文中執行。說到執行上下文,需要知道什麼時執行棧,執行棧,就是其他程式語言中的“呼叫棧”,是一種擁有LIFO(後進先出)資料結構的棧,被用來儲存程式碼執行時所建立的執行上下文。當js引擎第一次遇到要執行的程式碼的時候,首先會建立一個全域性的執行上下文並壓入當前執行棧,每當引擎遇到一個函式呼叫,它會為該函式建立一個新的執行上下文並壓入棧頂,js引擎執行棧頂的函式,當該函式執行完畢,執行上下文從棧中彈出,控制流程到達下一個上下文。對於每一個執行上下文都含有三個重要屬性:變數物件,作用域鏈,this。這些屬性也需要徹底理解。
2.1 、上下文呼叫棧
var scope1 = "global scope";
function checkscope1(){
var scope1 = "local scope";
function f(){
console.log(scope1);
}
return f();
}
checkscope1();
var scope2 = "global scope";
function checkscope2(){
var scope2 = "local scope";
function f(){
console.log(scope2);
}
return f;
}
checkscope2()();
上面兩段程式碼都會輸出 local scope
上面程式碼中scope一定是區域性變數,查詢塊級作用域即可,不管何時何地執行 f(),這種繫結在執行f()時依然有效。出現了一樣的結果,但是兩段程式碼的執行上下文棧的變化不一樣 :
第一段程式碼:push(<checkscope1>functionContext)=>push(<f>functionContext)=>pop()=>pop()
第二段程式碼:push(<checkscope2>functionContext)=>pop()=>push(<f>functionContext)=>pop()
2.2 、三種執行上下文型別
(1)全域性上下文
js引擎開始解析js程式碼的時候首先遇到的就是全域性程式碼,初始化的時候會在呼叫棧中壓入一個全域性執行的上下文,當整個應用程式結束的時候才會清空執行上下文棧,棧的最底部永遠時全域性執行上下文。這是預設的或者說基礎的全域性作用域,任何函式內部的程式碼都在全域性作用域中,首先建立一個全域性的window物件,然後設定this的值等於這個全域性物件,一個程式中只有一個全域性執行上下文。在頂層js程式碼中可以使用this引用全域性物件,因為全域性物件時是域鏈的頭,意味著所有非限定性的變數和函式都作為該物件的函式來查詢。
總之,全域性執行上下文只有一個,在客戶端中一般由瀏覽器建立,也就是我們熟知的window物件,我們能通過this直接訪問到它。
(2)函式上下文
每當一個函式被呼叫是,都會外該函式建立一個新的上下文,每個函式都擁有自己的上下文,不過是在函式呼叫的時候建立的,需要注意的是同一個函式被多次呼叫,都會建立一個新的上下文。
(3)eval和with上下文
執行在 eval和with
函式內部的程式碼也會有它屬於自己的執行上下文,但由於 JavaScript 開發者並不經常使用 eval
,所以在這裡我不會討論它。
2.3 、執行上下文建立階段
執行上下文建立分為建立階段與執行階段兩個階段
js引擎在執行上下文建立階段主要負責三件事:確定this==>建立詞法環境元件==>建立變數環境元件(目前還不太理解)
(1)確定this,這個不做詳解
(2)建立詞法環境元件
詞法環境是一種規範型別,基於 ECMAScript 程式碼的詞法巢狀結構來定義識別符號和具體變數和函式的關聯。一個詞法環境由環境記錄器和一個可能的引用外部詞法環境的空值組成。其中環境記錄用於儲存當前環境中的變數和函式宣告的實際位置;外部環境引入記錄很好理解,它用於儲存自身環境可以訪問的其它外部環境,那麼說到這個,是不是有點作用域鏈的意思?
詞法環境有兩種型別:
-
- 全域性環境(在全域性執行上下文中)是沒有外部環境引用的詞法環境。全域性環境的外部環境引用是 null。它擁有內建的 Object/Array/等、在環境記錄器內的原型函式(關聯全域性物件,比如 window 物件)還有任何使用者定義的全域性變數,並且
this
的值指向全域性物件。 - 在函式環境中,函式內部使用者定義的變數儲存在環境記錄器中。並且引用的外部環境可能是全域性環境,或者任何包含此內部函式的外部函式。
- 全域性環境(在全域性執行上下文中)是沒有外部環境引用的詞法環境。全域性環境的外部環境引用是 null。它擁有內建的 Object/Array/等、在環境記錄器內的原型函式(關聯全域性物件,比如 window 物件)還有任何使用者定義的全域性變數,並且
(3)建立變數環境元件
變數環境可以說也是詞法環境,它具備詞法環境所有屬性,一樣有環境記錄與外部環境引入。在ES6中唯一的區別在於詞法環境用於儲存函式宣告與let const宣告的變數,而變數環境僅僅儲存var宣告的變數。
3. JavaScript作用域和作用域鏈
3.1、作用域
詞法作用域是在寫程式碼或者定義的時候確定的,而動態作用域是在執行時確定的,(this也是)詞法作用域關注函式在何處宣告,而動態作用域關注函式從何處呼叫,JavaScript採用詞法作用域,其作用域由你在寫程式碼是將變數和塊作用域寫在哪裡決定,因此當詞法分析器處理程式碼時會保持作用域不變。可以理解為作用域就是一個獨立的地盤,讓變數不會外洩、暴露出去。也就是說作用域最大的用處就是隔離變數,不同作用域下同名變數不會有衝突。
理解作用域之前先來看一道題
function foo() {
console.log(value);
}
var value = 1;
function bar() {
var value = 2;
console.log(value);
foo();
}
bar();
上面的程式碼會輸出什麼呢,首先在全域性上下文中宣告foo()函式、value變數(其值為undefined)、bar()函式,程式碼執行階段,bar函式上下文入棧並執行,列印出value為2,然後執行foo(),foo()入棧,列印value時找不到該變數,js引擎會查詢上層作用域,即全域性作用域,於是列印出1。後面函式執行完畢上下文出棧。再來看下面這個函式,作用域是分層的,內層作用域可以訪問外層作用域的變數,反之則不行。
ES6以來,js中的作用域分為全域性作用域,函式作用域,塊級作用域和欺騙作用域。
3.1.1、全域性作用域
在程式碼中任何地方都能訪問到的物件擁有全域性作用域,最外層函式和在最外層函式外面定義的變數擁有全域性作用域,所有末定義直接賦值的變數自動宣告為擁有全域性作用域。
3.1.2、函式作用域
3.2、作用域鏈
作用域鏈本質上就是根據名稱查詢變數(識別符號名稱)的一套規則。規則非常簡單,在自己的變數物件裡找不到變數,就上父級的變數物件查詢,當抵達最外層的全域性上下文中,無論找到還是沒找到,查詢過程都會停止。查詢會在找到第一個匹配的變數時停止,被稱為遮蔽效應