[進階 js 09] Closure & Scope Chain


Posted by tzutzu858 on 2021-03-25

從 ECMAScript 看作用域

• Every execution context has associated with it a scope chain.
每個 Excution Context 都有一個 Scope Chain。

• When control enters an execution context, a scope chain is
created and populated with an initial set of objects

• When control enters an execution context, the scope chain is created and initialised, variable instantiation is performed, and the this value is determined.
當進入該 Excution Context,就會建立 Scope Chain

來自 ECMAScript 3rd的 10.1.4 和 10.2

Function Code
• The scope chain is initialised to contain the activation object followed by the objects in the scope chain stored in the [[Scope]] property of the Function object.

來自 ECMAScript 3rd的 10.2.3

上述得知

enter function EC =>
scope chain : [AO, [[Scope]]]

AO 就是 Activation Object

When control enters an execution context for function code, an object called the activation object is created and associated with the execution context. The activation object is initialised with a property
with name arguments and attributes { DontDelete }. The initial value of this property is the arguments object described below.
The activation object is then used as the variable object for the purposes of variable instantiation.
來自 ECMAScript 3rd的 10.1.6

所以 AO 和 VO 只有很微妙差別

global EC: {
    VO: {

    }
}

function EC: {
    AO: {
        a: undefined,
        func: func,
        //跟 VO 做一樣的事情
    },
    scopeChain: [function EC.AO, ]

}


模擬 js 引擎

範例

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

step 1 進入 Global EC

Global EC: {
    VO: {
        a : undefined,
        test: func
    },
    scopeChain: [globalEC.VO]
}

test.[[Scope]] = globalEC.scopeChain // 也等於 globalEC.VO

賦值 var a = 1 a 從 undefined 變成 1


step 2 進入 test function

testEC: {
  AO: {
    b: undefined,
    inner: func
  },
  scopeChain: [testEC.AO, test.[[Scope]]] 
  => [testEC.AO, globalEC.VO]
}

inner[[Scope]] = testEC.scopeChain
= [testEC.AO, globalEC.VO]

globalEC: {
  VO: {
    a: 1,
    test: func
  },
  scopeChain: [globalEC.VO]
}

test.[[Scope]] = globalEC.scopeChain

賦值 var b = 2 b 從 undefined 變成 2


step 3 進入 inner function

innerEC: {
  AO: {
    c: undefined,
  },
  scopeChain: [innerEC.AO, inner.[[Scope]]]
  = [innerEC.AO, test.[[Scope]]]
  = [innerEC.AO, testEC.AO, globalEC.VO]
}

testEC: {
  AO: {
    b: 2,
    inner: func
  },
  scopeChain: [testEC.AO, test.[[Scope]]] 
  => [testEC.AO, globalEC.VO]
}

inner[[Scope]] = testEC.scopeChain
= [testEC.AO, globalEC.VO]

globalEC: {
  VO: {
    a: 1,
    test: func
  },
  scopeChain: [globalEC.VO]
}

test.[[Scope]] = globalEC.scopeChain

賦值 var c = 3 c 從 undefined 變成 3
往下 console.log(b) ,從 innerEC.AO 開始找,沒有
沒有往上 testEC.AO 找到 b : 2,所以 log 出來就是 2
往下 console.log(a) ,從 innerEC.AO 開始找,沒有
沒有往上 testEC.AO ,沒有
沒有往上 globalEC.VO ,找到 a : 1,所以 log 出來就是 1


再次 cosplay JS 引擎

範例

var v1 = 10
function test() {
  var vTest = 20
  function inner() {
    console.log(v1, vTest)
  }
  return inner
}
var inner = test()
inner()

多開一個視窗看範例去對照再繼續往下看比較清楚,不然要一直拉回來好累


step 1 進入 Global EC

globalEC: {
    VO: {
        v1: undefined,
        inner: undefined,
        test: func
    },
    scopeChain: [globalEC.VO]
}

test.[[Scope]] = [globalEC.VO]

開始執行: v1 從 undefined 變成 10


step 2 進入 test EC

testEC: {
    AO: {
        vTest: undefined,
        inner: func
    },
    scopeChain: [testEC.AO, globalEC.VO]
}

inner.[[Scope]] = [testEC.AO, globalEC.VO]

globalEC: {
    VO: {
        v1: 10,
        inner: undefined,
        test: func
    },
    scopeChain: [globalEC.VO]
}

test.[[Scope]] = [globalEC.VO]

開始執行: vTest 從 undefined 變成 20
return inner 完後,照理來說 testEC: { AO: {...}} 要資源釋放掉
但因為 inner.[[Scope]] = [testEC.AO, globalEC.VO] 還會用到 testEC.AO
所以不能被底層機制給回收掉,所以這也就是閉包的原理
不過 testEC 可以被回收掉,保留 testEC.AO 就好

執行到 var inner = test() 這行
globalEC 裡的 vo 裡的 inner: undefined, 也會改成 inner: func,

step 3 進入 inner EC

inner.[[Scope]] = [testEC.AO, globalEC.VO]

innerEC: {
    AO: {

    },
    scopeChain: [innerEC.AO, testEC.AO, globalEC.VO]
}

globalEC: {
    VO: {
        v1: 10,
        inner: func
        test: func
    },
    scopeChain: [globalEC.VO]
}

test.[[Scope]] = [globalEC.VO]

testEC.AO: {
        vTest: 20,
        inner: func
    }

執行 console.log(v1, vTest)
v1 去 innerEC.AO 找不到,testEC.AO 也找不到,要到 globalEC.VO 才找到 v1: 10
vTest 去 innerEC.AO 找不到,testEC.AO 找到 vTest: 20



閉包的作用域陷阱

範例

var arr = []
for (var i=0; i<5; i++) {
  arr[i] = function() {
    console.log(i) 
  }
}
arr[0]() 

//以為 arr[0]() 要印出 0 結果是印出 5

因為會先 hoisting 把 for 裡面的 var i 提上去

var arr = []
var i
for (i=0; i<5; i++) {
  arr[i] = function() {
    console.log(i) 
  }
}
arr[0]()

實際執行

arr[0] = function() {
    console.log(i)
}

arr[1] = function() {
    console.log(i)
}

...以此類推

那它 console.log(i) i 要去哪裡找
往 global 找,但 i 已經是 5 了
因為迴圈每一圈都產生一個 function 但還沒執行
跑完 i < 5 不符合跳出迴圈,才執行 arr[0]() 結果才是 5


解決辦法

那我想要 arr[0]() 輸出 0 , arr[5]() 輸出 5 怎麼辦

方法 1 用新 function 來代替

var arr = []
for (var i=0; i<5; i++) {
  arr[i] = logN(i)
}

function logN(n) {
  return function() {
    console.log(n)
  }
}

arr[0]()

這樣執行就會是 arr[0] = logN(0)arr[1] = logN(1) ......
出來便是 arr[0]() 印出 0 、arr[1]() 印出 1 ......


方法 2 IIFE

IIFE (Immediately Invoked Function Expression) 是一個定義完馬上就執行的 JavaScript function。
把要執行的 function 用 () 包起來後面再加 ()
例如 :

(function (){
    console.log('123')
})()

這個語法可以把他想成把一個 function 包起來後面加 () 就是執行的意思

var arr = []
for (var i=0; i<5; i++) {
  arr[i] = (function() {
    return function() {
        console.log(number)
    }
  })(i)
}
arr[0]()

好處是不用額外宣告一個 function 但可讀性比較差


方法 3 let

比較簡單的解決辦法,把 var 改成 let

var arr = []
for (let i=0; i<5; i++) {
  arr[i] = function() {
    console.log(i)
  }
}
arr[0]()

因為 let 的變數生存範圍在 block 裡面
迴圈每跑一圈便會產生新的 scope

// 迴圈第一圈
{
    let i = 0 
    arr[0] = function(){
    console.log(i)
    }
}

// 迴圈第二圈
{
    let i = 1 
    arr[1] = function(){
    console.log(i)
    }
}

//......以此類推

Closure 可以應用在哪裡?

通常會用到 Closure 是想要隱藏住某些資訊

var money = 99

function add(num) {
    money += num
}

function deduct(num) {
    if (num >= 10) {
        money -= 10
    }else {
        money -=num
    }
}

add(1)
deduct(100)
console.log(money)
// money log 出會是 90

雖然寫了兩個 function 去操作 money
但可能跟別人合作,別人可以不透過 function 去調整 money
例如在 log 前直接加上 money = -1 ,那前面 fumction 再改值都沒有意義
所以這時就可以用閉包的好處

function createWallet(initMoney) {
  var money = initMoney
  return {
    add: function(num) {
      money += num
    },
    deduct: function(num) {
      if (num >= 10) {
          money -= 10 
      } else {
          money -= num
      }
    },
    getMoney() {
      return money
    }
  }
}
var myWallet = createWallet(99)
myWallet.add(1)
myWallet.deduct(10)
console.log(myWallet.getMoney())

這樣就沒辦法在 log 前去改變值,設 myWallet.money = 10 這種是沒有用的
沒辦法從外部去操控 money 的值
雖然自由度比較少,但比較安全


複習 Huli 文章 所有的函式都是閉包:談 JS 中的作用域與 Closure










Related Posts

SSR vs CSR

SSR vs CSR

React Styled-component 筆記 P1

React Styled-component 筆記 P1

git

git


Comments