JavaScript 程式執行原理:Scope Chain 與 var, let, const


Posted by backas36 on 2021-08-16

EC 裡面裝些什麼?Scope Chain 怎麼來的 ?

當我們遇到 function 被呼叫時,JS 生出 EC,每一個 EC 都會有 variable Object (之後簡稱 VO), 這個 VO 裡面初始化function 裡面的變數以及 function 的宣告。

初始化:

EC: {
    VO{
        a:undefined
    }
}

function test() {
    var a = 666
}

test()

EC 初始化後,進入執行階段後會變這樣:

EC: {
    VO{
        a:666
    }
}

function test() {
    var a = 666
}

test()

如果 function 帶有參數的話呢? 有值的會帶入值,如果沒有值就會初始化成 undefined。

EC: {
    VO{
        a:666,
        b:undefined

    }
}

function test(a, b) {
    //...
}

test(666)

而如果遇到 function呢? 一樣會先初始化。

EC: {
    VO{
        a:666,
        b:undefined,
        c:function

    }
}

function test(a, b) {
    //...
    function c () { //... }
    c()
}

test(666)

那如果剛好這個 function 跟 參數名字一樣呢,那就會被取代:

EC: {
    VO{
        a: <function>,

    }
}

function test(a) {

    function a () { //... }

}

test(666)

而對於如果遇到宣告的變數是同名呢? 那就會忽略這個變數宣告,值也不會被改變,因為剛剛已經宣告過了。如果原本沒存在的,就是 undefined。

EC: {
    VO{
        a: 666,
        b: <function>,
        c: 888
    }
}

function test(a,b) {
    function b () { //... }
    var a = 777 // 被跳過了
    var c = 888

}

test(666)

好,這只是 EC 裡面 VO 的部分, EC 裡面還有一個很重要的東西叫 Scope Chain,當我們進入 EC 時,會建立 VO 以及 建立 Scope Chain 還有個 this,而且如果 VO 裡面也有 function 宣告的話會為這個 function 建立 [[Scope]],而 Scope Chain 跟 VO 一樣,也是在做初始化的事情。

如果此 EC 是 global EC,則 global EC 中的 Scope Chain 就稱為 global variable object (global VO)。

如果是此 EC 是 function 的話,就會有 activation object (簡稱 AO),,什麼是 AO 呢? 可以把他跟 VO 當成是一樣的就好,只是在 global EC 裡面我們稱為 VO,在 function EC 裡我們稱為 AO。

function EC 跟 global EC 一樣 裡面放著 variable object 、function EC 的 Scope Chain (自己的AO + function EC的 [[Scope]] )、this、以及如果有遇到 function 的宣告就產生的 [[ Scope ]]

我們帶個例子來看會比較清楚:

var a = 1
function test() {
    var b = 2
    function inner() {
        var c = 3 
    }
    inner()
}
test()

首先是進入 global EC :

此時的 global.scopeChain = [globalEC.VO]

此時的 test.[[Scope]] = globalEC.scopeChain = [globalEC.VO]

之後執行完畢,會遇到 test() ,一樣先初始化:

testEC.scopeChaine = [testEC.AO, test.[[Scope]] ] = [testEC.AO, [globalEC.VO] ]

inner.[[Scope]] = testEC.scopeChain

初始化完成,繼續執行 => 遇到 inner() => 初始化 inner :

scopeChain: [innerEC.AO,testEC.scopeChain] = [innerEC.AO, testEC.AO, gloEC.VO]

hoisting

認識了 Scope Chain 後,默的就幫我們演示了一層一層往上找的 Scope Chain 的及 hoisting 過程了,不是嗎?

進入 某 EC 的時候,會先宣告變數或 function,因為還沒有賦值,在執行賦值之前如果我們去使用變數,這個時候的變數就是 undefined,如此而已。

例如當我們在宣告變數之前就使用變數的話,是不會丟出錯誤的,只是會變成 undefined。

console.log(b) // undefined
var b = 666

其實我們可以解讀成這樣子:

var b
console.log(b)
b = 666

還有我們常使用的 function 也是有 hoisting 的效果,看下面的例子,程式不會報錯
,也會順利執行 test function 。

test()
function test(){
    //....
}

再提供一些比較 tricky 的例子:

var a = 666
function test(){
    console.log(a)
    var a = 777
}
test()

曾經的我,一直覺得他會 console 666 ,但其實不是喔!!! 因為在建立 testEC AO 的時候,我們將 var a 放進去了,所以進入賦值之前是 undefined 。

還有 function 宣告 hoisting 會比變數還高 :

function test(){
    console.log(a) // <function>
    var a = 777
    function a() {
        console.log('OMG')
    }
}
test()

那麼如果是帶參數的 function 並且又宣告一樣的變數呢? 這樣參數會蓋過去。

function test(a){
    console.log(a) // 666
    var a = 777
    console.log(a) // 777
}

test(666)

但是如果 function 的話,優先度會提高:

function test(a){
    console.log(a) // <function>
    function a(){
    }
}

test(666)

總之優先順序是 1. function > 2. arguments > 3. var

還有要注意在寬鬆模式下的話,如果有個變數在 global 中沒有宣告,但是在 function 裡面出現一個沒宣告過的變數,但是有給賦值,JS 就會在 global 新增這個變數,並且給予值。(如果是嚴格模式下就會報錯)

function fn(){
  fn2()
  function fn2(){
    b='OMG'
  }
}
fn()
console.log(b) // OMG

let, const 與 var

let, const 與 var 最大的不同是, var 變數的生存範圍是以 function 區塊來界定,然而 const, let 是以 block 來界定的。
比如說 if { .... } 或迴圈都是 block,如果在 block 裡面使用 let, const ,在 block 外面使用變數的話是會報錯的。

Temporal Dead Zone,TDZ

之前的我一直以為 let, const 是沒有 hoisting 的,但是如果沒有 hoisting ,下面的程式會跑出 666 才對,但卻是跑出 ReferenceError ...

let a = 666
function test(){
    console.log(a)
    let a = 777
}
test()

let 與 const 其實是有 hoisting 的,但不是像 var 一樣,初始化時會是 undefined,並且在賦值以前如果去存取,會拋出錯誤。

在 hoisting 至 賦值以前 這段時間,我們稱它為 TDZ,在這段時間內存取該變數,會拋出錯誤!

let a = 666
function test(){
    // ======= a TDZ 開始
    console.log(a)
    .
    .
    .
    let a = 777 // ======= a TDZ 結束
}
test()
let a = 666
function test(){
    test2() // ======= a TDZ 開始 
    let a = 777 // ======= a TDZ 結束
    funciton test2(){
        console.log(a)
    }
}
test()

總之,就是記得賦值以前去存取 let 與 const 宣告的變數,會發生錯誤就對了。


#js #Scope Chain #變數宣告







Related Posts

[Leetcode in Java] 209. Minimum Size Subarray Sum

[Leetcode in Java] 209. Minimum Size Subarray Sum

Python基礎入門 | 命名空間 (Namespace)

Python基礎入門 | 命名空間 (Namespace)

CSS

CSS


Comments