[課程筆記]克服JS的奇怪部分-CH1 執行環境與詞彙環境

Udemy課程連結
課程總目錄

本篇筆記目錄:

觀念小叮嚀:語法解析器、執行環境與詞彙環境
觀念小叮嚀:名稱/值配對與物件
U1 全域環境、全域物件
U2 執行環境:創造與提升
觀念小叮嚀:JS 與 undefined
觀念小叮嚀:單執行序、同步執行
U3 函數呼叫與執行堆
U4 函數、環境與變數環境
U5 範圍鏈
U6 範圍、ES6 與 let
U7 關於非同步回呼

觀念小叮嚀:語法解析器、執行環境與詞彙環境

  1. Syntax parsers 語法解析器:
    當你的語法是有效時,能夠讀取程式碼,並決定做何動作的一個程式。
    

    其他人也寫了一個程式來轉換你的 JS 程式碼成為電腦懂的東西。

  2. Lexial enviorments 詞彙環境= execution context 執行派絡
    程式碼在程式中的實際所在位置。
    
  3. Execution contexts 執行環境
    一個包裹,幫助管理(驗證、執行)正在執行的程式。
    

    我們有許多的詞彙環境,但真正在執行的會被執行環境所管理,執行環境包含的不只是你所寫的程式碼,下面會有完整的介紹。


觀念小叮嚀:名稱/值配對與物件

  1. Name/Value Pair 名稱/值配對
代表一個名稱會對應到唯一的值

名稱可能會被定義很多次,但如果在一種執行環境下,它只能有一個值。
也就是說一段正在執行的程式碼,同樣名稱只會有一個,而一個名稱只能被一個值定義。

不過,這個值可以是另一個名稱/值的配對—>「物件」

  1. 物件
名稱/值配對的組合

其他的程式語言物件可能有更多複雜的概念,但在 JS 中,物件就是這樣而已。


U1 全域環境、全域物件

不論何時執行JS程式,你的程式碼都被包在執行環境裡。

Global context 全域環境 = Basic context 基本執行環境

全域表示不再函數中,我們可以在任何地方取用它,只要執行 JS 檔,執行環境就會被創造出來,全域執行環境 JS 會幫我們創造幾件事情:

(1) Global Object 全域物件

瀏覽器裡的全域物件是 window,每個視窗都有自己的執行環境和自己的全域物件。

舉例:當我們執行以下程式

var a='Hello world!'
function b(){
}

我們可以在 window 這個全域物件中找到 a 和 b,
也就是說我們輸入 a window.a 可以得到一樣的結果,

(2) this

執行環境可以決定 this 裡面是什麼。在用瀏覽器的情況下,this 就等於 window

(3) Outer Enviroment 外部環境

當你在全域等級執行,你沒有外部環境(已經在最外面了),所以這時外部環境為 null 。

(4) 你的程式

最後,執行環境在執行你的程式碼了!


U2 執行環境:創造與提升

在大部分的程式語言中,下面的語法會出現錯誤,因為 a 還沒有被定義,

b();
console.log(a)

var a='Hello World';

function b(){
	console.log('Called B!');
}

但在 JS 中,實際上會出現:

Called b!
undefined

錯誤解釋:JS 的變數和函數被移動到最前面
正確解釋:因為執行環境的建立會被分成兩階段:

  1. Creation 創造階段:建立執行環境(全域物件、this、外部物件)
  2. Hoisting 提升階段:在逐行執行程式碼前,設定變數和函數在記憶體裡

提升階段時,JS 先為b() 空出記憶體空間,所以當我們可以呼叫b(),然而,a變數的情況不太一樣,雖然 JS 為a空出記憶體空間時,它不知道的會是什麼值(undefined),直到被執行。雖然技術上來說可以,但依賴提升不是個好方法


觀念小叮嚀:JS 與 undefined

Undefined 和 NotDefined 是不同的東西

var a;
console.log(a)
//undefined

console.log(b)
//notdefined

-undefined :當執行環境被創造時,JS 會設定所有變數都是 undefinded
-notdefined :變數從未被宣告,記憶體裡沒有這個東西

永遠不要將變數設值為 undefined,很難 debug


觀念小叮嚀:單執行序、同步執行

Javascipt 一次只會發生一件事

  1. Singled threaded 單執行序
一次執行一個指令

在程式碼裡會有許多指令,而單執行序表示我們一次只能執行一個指令,當我們在使用瀏覽器時,要知道 JS 不是瀏覽器唯一在執行的東西,因此當我們說 JS 是單執行序的時候,不是指瀏覽器,而是從我們的角度看,JS 一次只做一件事。

  1. Synchronous execution 同步執行
一次一個

程式碼會依照出現的順序,一次執行一行。


U3 函數呼叫與執行堆

Invocation 函數呼叫

函數呼叫:執行或呼叫函數

以下面的程式為例:

function b(){
}

function a(){
b();
}

a()

執行過程:

  1. 語法解析器逐行分析程式內容,編譯器編譯程式
  2. Creation 創造階段:建立全域執行環境(創造全域物件、this、window),並放進執行堆中
  3. Hoisting 提升階段:將函式、變數放進記憶體中
  4. 程式逐行執行
  5. 遇到 a() 時:呼叫 a()
    1. Creation 創造階段:新的執行環境被創造,並放進執行堆中
    2. 逐行執行
  6. 遇到 b() 時:呼叫 b()
    1. 停止執行a(),執行b()
    2. Creation 創造階段:新的執行環境被創造,並放進執行堆中
  7. b() 執行完畢後:
    1. b()離開執行堆
    2. 因為a()沒有要執行的東西,所以a()也離開執行堆

Execution stack 執行堆:一個一個堆起來,誰在最上面就是正在執行的東西

另一個範例

function a(){
	b();
	var c;
}

function (){
	var d;
}

a();
var d;

執行過程:

  1. 語法解析器逐行分析程式內容,編譯器編譯程式
  2. 創造新的執行環境
    1. Creation 創造階段:建立全域執行環境(創造全域物件、this、window),並放進執行堆中
    2. Hoisting 提升階段:將函式、變數放進記憶體中
  3. 程式逐行執行
  4. 遇到 a() 時:呼叫 a()
    1. 創造新的執行環境、放進執行堆
    2. 逐行執行
  5. 遇到 b() 時:呼叫 b()
    1. 停止執行a(),執行b()
    2. 新的執行環境被創造,並放進執行堆中
    3. 逐行執行:var d
  6. b() 執行完畢後:
    1. b() 離開執行堆,繼續執行 a()
    2. 逐行執行:var c
  7. a() 執行完畢後:
    1. a() 離開執行堆
    2. 逐行執行:var d

U4 函數、環境與變數環境

Variable Environment 變數環境

描述創造變數的位置還有他在記憶體中和其他變數的關係
function b(){
	var myVar;
}
function a(){
	var myVar=2;
	b();
}

var myVar=1;
a();

myVar 的值會是多少?先了解執行過程:

執行過程:

  1. 創造:建立全域執行環境,並放進執行堆中,提升:myVarb()a()被放進記憶體中
  2. 逐行執行:遇到 var myVar=1 ,記憶體中的變數得到 1 的值
  3. 逐行執行:遇到 a()
    1. 創造新的執行環境
      1. Creation 創造階段:建立全域執行環境,並放進執行堆中
      2. Hoisting 提升階段:遇到 var myVar=2,將myVar放進記憶體中,得到 2 的值
    2. 逐行執行:var myVar=2
  4. 遇到 b() 時:呼叫 b()
    1. 停止執行a(),執行b()
    2. 創造新的執行環境
      1. Creation 創造階段:建立全域執行環境,並放進執行堆中
      2. Hoisting 提升階段:遇到 var myVar,將myVar放進記憶體中,得到 undefined 的值
  5. b() 執行完畢後, b() 離開執行堆,繼續執行 a()
  6. a() 沒有要執行的, a() 離開執行堆

Scope:表示我們可以在哪裡看到變數

這裡的每個變數都被定義在自己的執行環境,彼此沒有關聯!


U5 範圍鏈

Reference to the outer enviroment 外部環境的參照

當我們需要使用變數時,JS 不只會在目前的執行環境的變數環境中尋找,當你需要某個執行環境內的程式碼的變數,而他無法找到變數,它就會到外部環境尋找變數,每個執行環境都有一個參照到它的外部環境參照。

function b(){
	console.log(myVar);
}
function a(){
	var myVar=2;
	b();
}

var myVar=1;
a();

//1

為什麼答案會是1呢?因為外部環境的參照!
以上面的程式碼為例,b()的外部環境是全域執行環境,a()也是。

執行環境取得的變數環境,不必是正下方的執行環境,反之,它會是某個我們已討論的東西-「詞彙環境」

「Lexial enviorments 詞彙環境」

JS 很特別它很注重詞彙環境,詞彙環境是程式碼實際所在的位置。b()的位置不在 a()裡,而是在全域環境,所以我們在 b()找不到 mayVar 時,他會往它的外部環境:全域環境做尋找。

往下搜索的鏈子,就是我們所說的「範圍鏈」,「範圍」代表我能夠取用這個變數的地方,「鏈」是外部環境參照的連結。

接著我們嘗試改變 b()的位置:


function a(){
	var myVar=2;
	function b(){
		console.log(myVar);
	}
}

var myVar=1;
a();

//2

從上面的程式碼來看,我們可以知道b()被我們改建立在 a()裡面,而不是全域環境,所以 b() 的外部環境就是 a()

當我們改變b()的詞彙環境,表示我改變了他的物理位置,他現在在 a() 裡面
所以 JS 會認定他的外部參照是 a!函數的位置決定了它的外部環境,我們也可以想成因為 b() 在 a() 的執行環境創造階段被創造了,所以 a() 就是它的外部環境

當然,依照詞彙環境來看程式碼實際所在位置是最快的!


範圍、ES6 與 let

  1. Scope 範圍
範圍是變數可以取用的區域

如果你有兩個相同變數,他在記憶體中會是兩個不一樣的變數

  1. Let
    ES6 中引入新的宣告變數方法:let,let 讓 JS 引擎使用「區塊範圍 Block scoping」

兩個重點:
(1) 宣告變數後我們才能使用 let

if(pa>b){
	console.log(c)
 	let c= true;
//error
}

c會被創造在執行階段,變數仍會被放入記憶體中,設值為undefined,然而,直到執行階段那一行程式被執行,真的宣告變數時,我們才能使用 let,他仍然在記憶體中,但引擎不讓我們使用

(2) 當變數被宣告在區塊裡面,它只能在裡面被取用

當你有一個迴圈正在執行相同的程式,但你有個 let,每次進行迴圈時,你的變數在記憶體中都是不同的


U6 關於非同步回呼

Asynchrounous 非同步

一個時間點不止執行一個程式

Javascript引擎本身,不是獨自存在的,例如:瀏覽器,它還有其它引擎,像是呈現引擎(rendering engine)、處理瀏覽器的HTTP請求,它們會與 JS 引擎互動,但這些可能都是非同步執行,只有 JS 引擎是同步的

JS 引擎內的等待列稱為事件佇列(event queue),這裡面都是事件(如:click 、HTTP Request)

當執行堆是空的 JS 才會注意到事件佇列,如果事件佇列有東西,它才會看是否有函數會被這個事件處發,如果事件觸發函數,會出現在執行堆(依據事件發生的順序處理)。

所以這不是真正的非同步,JS 用同步的方式處理非同步事件,把瀏覽器非同步東西放到事件佇列,但原本的程式仍然繼續一行行執行。

我們可以來看範例:

//long running function
function waitThreeSeconds(){
	var ms = 3000+new Date().getTime();
	while(new Date()<ms){}
	console.log('finished function');
}

function clickHandler(){
	console.log('click event')
}

//listen for the click event
document.addEventListener('click',clickHandler);

waitThreeSeconds();
console.log('finished execution');

當我們在 3 秒內點擊畫面,會出現:

finished function
finished execution
click event

為什麼呢?
因為 JS 引擎直到執行堆是空的才會看事件佇列,這表示長時間函數可以干擾事件,但這就是 JS 如何同步處理在瀏覽器別處非同步的事件發生。當全部完成後,它會不斷繼續看事件佇列的迴圈,這稱為持續檢查(continous check)