什麼是閉包 closure?


Posted by YongChenSu on 2020-12-09

閉包是在 function 裡面 return function

當 function 執行結束之後,會將資源釋放,但以下面例子而言,在閉包裡面依舊存取得到 a

function test(){
  var a = 10
  function inner() {
    a++
    console.log(a)
  }
  return inner
}
var func = test()

func() // inner()
func()
func()

閉包的實際應用

共用同一個 ans 並把值記住

function complex(num) {
  console.log('calc')
  return num * num * num
}

function cache(func){
  var ans = {}
  return function(num) {
    if (ans[num]) {
      return ans[num]
    }
    ans[num] = func(num)
    return ans[num]
  }
}

const cacheComplex = cache(complex)
console.log(cacheComplex(20)) // 計算
console.log(cacheComplex(30)) // 計算
console.log(cacheComplex(20)) // 不用再次計算,已有結果
console.log(cacheComplex(30)) // 不用再次計算,已有結果

// calc
// 8000
// calc
// 27000 
// 8000
// 27000


從 ECMAScript 來解釋 VO, AO

  • 每一個 Excution Context 都有一個 scope chain
  • 當進入新的 Excution Context,scope chain 就會被建立並初始化。
  • 當進入 function code 的時候,scope chain 會新增 activation object (AO),還有會新增預設的屬性 arguments。AO 可當作 variable object(VO) 來用。
  • global EC 裡面有 VO。

    global EC: {
    VO: {
    
    }
    }
    
  • function EC 裡面有 AO
    function EC: {
    AO: {
      a: undefined,
      func: func,
    }
    scopeChain: [function EC.AO, [scope]]
    // 在 function 被宣告的時候決定 scopeChain
    // scope chain 是自己的 AO 加上 scope 的 property
    }
    

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

範例

程式碼

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

scope chain 解釋

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

testEC: {
  AO: {
    b: 2,
    inner: function
  },
  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

閉包是 scope chain 有 reference 其他 excution context 的 VO、AO

因 return 之後,只要 scope chain 有連結到,還是可能用到 AO 或 VO,所以 JS 的垃圾回收機制就不會將其回收。

閉包的優點

  1. 變數隱藏在裡面讓外部存取不到
  2. 當輸入的資料曾經計算過,不須重新計算一次。

閉包的問題

有可能保留到不想保留的值

var v1 = 10
function test() {
  var vTest = 20
  var obj = { hugeObject: 'huge object' }
  function inner() {
    console.log(v1, vTest, obj.hugeObject)
  }
  return inner
}
var inner = test()
inner()


不同角度看閉包

閉包:「會記住週邊資訊的 funciton」

這樣在 JS 裡面,所有 function 都是閉包,,因都會把上層的資訊記起來,但通常不會這樣定義閉包。

閉包:function 裡面會 return function

這才是一般所定義的閉包

閉包:作用域陷阱

這樣等於在全域宣告 i,當 for 迴圈裡面的 function 往上找 i 時,迴圈已經跑完了,i 已經等於 5。

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

解決方法(一)

用新的 funciton 代替,因建立新的 function 故有新的作用域記住 i 值

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

function logN(n) {
  return function() {
    console.log(n)
  }
}
arr[1]() // 1

解決方法(二):IIFE

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


arr[1]()

解決方法(三):let

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

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

相當於以下程式碼:

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

closure 可避免變數被外部修改

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

參考資源


#程式導師實驗計畫第四期 #前端 #closure







Related Posts

4. Builder

4. Builder

變成rule的形狀(4) - ESLint + Prettier + Stylelint 問題集

變成rule的形狀(4) - ESLint + Prettier + Stylelint 問題集

AI輔導室|施博瀚的以拉拉拉系列EP1-EP20

AI輔導室|施博瀚的以拉拉拉系列EP1-EP20


Comments