【克服 JS 】第二章 執行環境與詞彙環境

JavaScript 全攻略:克服 JS 的奇怪部分,第二章 執行環境與詞彙環境 Execution Contexts and Lexical Environments,學習紀錄。

Udemy 課程連結:

JavaScript 全攻略:克服 JS 的奇怪部分

2-1 觀念小叮嚀

  • 語法解析器 Syntax parsers
  • 詞彙環境 Lexical environments
  • 執行環境 Execution contexts
  • 名稱/值配的配對與物件 Name/Value pair and Object

1. 語法解析器

將合乎語法的程式,依照規則將其轉換成電腦認識的語言,而這個中介的引擎被稱為直譯器轉譯器

2. 詞彙環境

當我們在編輯器存檔,在瀏覽器重新整理時,JS 不會給電腦直接執行,而是透過引擎和語法解析器轉譯、翻譯給電腦執行,而程式語法所寫在的位置,可以影響執行階段時,它對應的記憶體位置,也能影響它和其他變數函式的互動。所以談到詞彙環境就是指開發環境中的程式位置。

3. 執行環境

JavaScript 在執行時,其實並不會完全照我們開發時期寫的 JS 逐字逐行去執行。

我們的程式碼會在執行時,會先依照詞彙環境被解析器轉換,在電腦中被創造並擺到該放的記憶體位置去,最後電腦才執行。尤其 JavaScript 某些特性(例如提升)就是在創造階段產生,因此理解 JavaScript 在執行環境中的狀態是很重要的一件事。

4. 名稱/值的配對與物件

JavaScript 中的物件,就是一堆名稱/值的組合而已,不像其他語言的物件有一堆特性。

var Address = {
  Street: 'Main',
  Number: 100,
  Apartment:{
    Floor: 3,
    Number: 301
  }
}

物件中也可以包含物件,像是上面的 Address 物件包含了一個 Apartment 物件。

2-2 全域環境與全域物件 Global environment and Global Object

JavaScript 一開始在還沒寫任何程式碼時,就會先自動建立一個全域的執行環境,在瀏覽器的執行環境下,會先建立個 window 的全域物件,在全域環境中,他和 this 是相同的。。

全域(Global):程式碼或變數不在一個函式裡,就是全域的(Not inside a function.)。

全域的變數或函式會被放在 window 物件之下。

CodePen:2-2 全域環境與全域物件

2-3 執行環境:創造與提升(Hoisting)

在同一個執行環境中,會先將使用到的變數存至記憶體,例如:

b(); // 顯示 Called b!
console.log(a); // 顯示 undefined

var a = 'Hello World!'

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

函式 b 定義的內容會先存至記憶體,所以雖然定義寫在後面,但在前面也可以正確的呼叫

變數 a 的定義寫在後面,雖然可以呼叫,但卻顯示 undefined

在使用等號賦值的情況下,只會先將變數放至記憶體,再依執行順序將等號的值填進去。

可以想像程式碼變成這樣:

var a;

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

b(); // 顯示 Called b!
console.log(a); // 顯示 undefined

a = 'Hello World!'

var a 被移到最前面去了,而賦值的地方還是一樣在 console.log(a) 後面,而函數 b 是連內容都移到前面去了。

但其實不是 JS 真的會先把程式碼改成這樣,而是執行的順序會變得像這樣。

CodePen:2-3 執行環境:創造與提升(Hoisting)

提升 Hoisting 的參考資料

2-4 觀念小叮嚀:JavaScript 與 undefined

undefined 和 not defined,字面上來看都是未定義、無定義、沒有定義之類的的。但兩者的意涵完全不相同。

undefined 是 JS 的一種特殊值,代表還沒定義。

當使用 var a 建立一個變數 a,而沒有賦值時,a的值就是 undefined,可以用 === 來判斷 a 是否為 undefined

if(a === undefined) {
  console.log('a is undefined')
} else {
  console.log('a is defined')
}

另外,undefined不代表什麼都沒有,在 JS 中undefined 既是個型別,也是一個值,這個型別 undefined 的值就叫 undefined,既然不代表什麼都沒有,那僅宣告變數自然也會佔據記憶體空間。

也可以用 a = undefineda 的值設定 undefined ,但不建議這麼做,應該讓undefined 就代表一個變數剛建立還沒賦值。

小結

  • undefined,是建立後尚未賦值時,預設的值,而這個值表示值還沒被定義。
  • not defined,是變數、函式等,未被宣告建立、定義,在電腦執行程式碼時找不到這個東西,可以想成在程式與記憶體中未被定義。

CodePen:2-4 JavaScript 與 undefined

2-5 執行環境:程式執行

在 JavaScript 產生執行環境時,會有兩個階段:

  1. 創造階段(Creation Phase):設定變數與 function 到記憶體中、抬升(hoisting)。
  2. 執行階段(Execution Phase):一行一行執行你寫的程式碼。

2-6 觀念小叮嚀:單執行緒、同步執行

JavaScript 是一步一步執行,並且在一個時間只會做一件事。

2-7 函式呼叫與執行堆 Function invocation and Execution stack

重點字提示: invocation,他的意思是: 執行或是呼叫函式

所以當我們說 invoke the function 或是 function invocation,意思是執行這個函數。

接著,就來看看,執行函式時,JavaScript 會做什麼:

function b() {
  
}

function a() {
  b()
}

a()
  1. Global Execution Context:產生全域物件、this、設定變數與 function 的記憶體,然後逐行執行程式碼。
  2. 第九行執行到後,會產生一個 a() 的 Execution Context,放置到 Execution stack 中,stack 的定義就是後進來的先做,接著,在逐行執行 a 中的程式碼。所以,當在 JavaScript 環境下執行到function的時候,就會產生一個Execution Context 放置到 stack 的最頂部,並逐行執行 function 內的程式碼。
  3. 接著跑到第六行,又碰到了一個 function b,所以,把b() 放置到 stack 頂端,接著,逐行執行 b 的程式碼。
  4. b 執行完之後,就會 pop 出 stack,回到剛剛呼叫 b 的第 6 行,再往後面執行。
  5. 接著,a 執行完之後,也會 pop 出 stack。
  6. 最後,stack 最頂端的是 Global execution context,再往下繼續執行程式碼。

CodePen:2-7 函式呼叫與執行堆

2-8 函式、環境與變數環境

變數環境 Variable environment

定義:Where the variable live ,and how they relate to each other in memory.(描述你創造變數的位置,還有他在記憶體中和其他變數的關係。)

來看看以下程式碼:

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

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

var myVar = 1
console.log(myVar)
a()
  1. 生成Global execution context,並放置在 stack 最頂端,當程式跑到 myVar = 1 時,賦予給它值。
  2. 接著,執行 a(),產生 a 的 Execution context,放置在 stack 最頂端,當程式跑到 myVar = 2時,賦予給它值。
  3. 最後,執行 b(),產生 b 的 Execution context,放置在 stack 最頂端,當程式跑到 myVar 時,賦予給它值 undefined。

雖然 myVar 被宣告了 3 次,但因為它都在不同的執行環境中,所以這 3 個其實是不同的變數。

如果我們在剛剛的程式 a() 下面再加一行 console.log(myVar) 執行後回發現重新印出 myVar 仍然顯示 1,因為這個 myVar 就是原本全域的變數 myVar,代表其實他沒有被覆蓋掉。

CodePen:2-8 函式、環境與變數環境

2-9 範圍鍊 Scope Chain

但是如果函式 b 沒有宣告變數,如下:


function b() {
  console.log(myVar) // 1
}

function a() {
  var myVar = 2
  b()
}

var myVar = 1
a()

會印出 1 而非 undefined。 當函式中使用到沒有定義的變數時,JavaScript 會接著到外部環境尋找(外部參照),注意這裡的外部環境不是指呼叫這個函式的執行環境,而是指定義宣告這個函式的外部環境。函式 b 是在全域環境定義,在函式 a 中呼叫執行,既然是向定義環境找,所以函式 b 的外部環境就是全域環境,而不是函式 a,故它就找到全域環境的變數 myVar,最後印出 1。

如剛剛的範例所示,為了找 myVar 而從內層找到最外層(global)的這條鍊,就稱之範圍鍊。

再看一個例子:


function a() {

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


var myVar = 1
a()

我們此時把 函式 b 放置 函式 a 之中,這個時候再來看看 myVar 是多少,來看看這條 scope chain長甚麼樣子。

此時函式 b 的定義寫在函式 a 的執行環境了,所以當呼叫函式 b 裡的 console.log(myVar) 時,因為函式 b 的執行環境沒有 var myVar,會到函數b的外部環境尋找,而函式 b 的外部環境為函式 a 的執行環境,所以會顯示在函數a裡設定的 2。

2-10 範圍、ES6 與 let

重點字提示:範圍是變數可以被取用的區域

如果你有兩個相同變數,但它會各有一個自己的執行環境,在記憶體中它們其實是兩個不同的變數。

但到最後 我們知道所有談論到的東西

ECMAScript 2015(ES6),引入新的宣告變數方法: 叫作 let,他可以像 var 一樣使用,但 var 還是存在可以使用。

let 讓 JavaScript 引擎 使用一種東西叫作區塊範圍(block scoping)。

你可以宣告一個變數就像用 var 一樣,它會被創造在執行階段 變數仍會被放入記憶體中,設值為 undefined,然而直到執行階段,那一行程式被執行, 真的宣告變數後,你才能使用 let ,然而直到執行階段,那一行程式被執行,真的宣告變數後,你才能使用 let

if (a > b) {
let c = true
}

在這個例子, 如果你試著在 let c = true 之前取用 c 變數,會回傳錯誤訊息。

2-11 關於非同步回呼 Asynchronous Callbacks

之前有提到,JavaScript 是一個單執行緒、同步的程式,它逐行執行程式碼,並不會非同步的執行程式,這樣它究竟要怎麼去監控(monitor)瀏覽器的一些事件呢?

JavaScript 在瀏覽器執行時依賴引擎來驅動,而瀏覽器不是只有JavaScript 引擎的存在,還有其他引擎在處理瀏覽器的其他狀態、程式。有可以呈現東西到瀏覽器畫面上的渲染引擎(Rendering Engine),也有其他專門處理http請求的東西存在。

所以 JavaScript 要如何處理非同步的事件?

非同步的事情會先放入一個叫做事件佇列(Event Queue) 的東西裡面,JavaScript 會先把所有 Execution context 的事情都做完,才去事件佇列裡面繼續處理這些事情。

JavaScript 在瀏覽器的非同步行為,其實是指將非同步事件放到事件佇列,而自己執行同步程式結束後,才處理事件。如果事件觸發函式,就創造執行環境給對應函式,所以說,當瀏覽器的其他引擎或處理器有事件、狀態發生並與 JavaScript 引擎互動時,這些事件就會被註冊進事件佇列裡。

範例程式:

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

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

document.addEventListener('click', clickHandler);

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

執行一個等 3 秒的函式,然後有設定一個 click 事件。

執行此程式,並在 'finished function' 出現前點擊頁面,執行結果為: 'finished function' 'finished execution' 'click event!'

點擊事件只會在 waitThreeSeconds() 執行完成,並且執行完全域的 console.log('finished execution')之後才會發生。

JavaScript 是用同步的方式(一行一行執行程式碼),來處理非同步的事件(如click event)。也就是說執行時間太長的同步程式可以幹擾非同步事件的處理時間

CodePen:2-11 關於非同步回呼

參考資料:

JavaScript 基礎二三事|2018 iT 邦幫忙鐵人賽

那克斯的學習筆記

史考特の工程師之旅

TOP