Javascript深入之作用域與閉包

前端南玖發表於2021-10-20

相信絕大多數同學都聽過閉包這個概念,但閉包具體是什麼估計很少有人能夠說的很詳細。說實話閉包在我們平時開發中應該是很常見的,並且在前端面試中閉包也是常見的重要考點,在學習閉包之前我們先來看看作用域與作用域鏈,因為這是閉包的關鍵。

作用域

簡單來說,作用域是指程式中定義變數的區域,它決定了當前執行程式碼對變數的訪問許可權

在ES5中,一般只有兩種作用域型別:

  • 全域性作用域:全域性作用域作為程式的最外層作用域,一直存在
  • 函式作用域:函式作用域只有在函式被定義時才會被建立,包含在父級函式作用域或全域性作用域中

說完概念,我們來看下面這段程式碼:

var a = 100
function test(){
    var b = a * 2
    var a = 200
    var c = a/2
    console.log(b)
    console.log(c)
}
test()      // 這裡會列印出什麼?

解析:

1.首先這段程式碼形成了全域性作用域與函式作用域

2.全域性作用域有一個變數a賦值為100

3.在test函式作用域中定義了區域性變數b,a,c

4.這裡又存在變數提升,在函式作用域內先進行變數提升var b; var a; var c;

5.再對b進行賦值,這時候a還沒有被賦值,所以a的值為undefined,再將a*2,所以b為NaN

6.再給a賦值為200,c賦值為a/2等於100

所以最終會列印出 NaN,100

在ES6中,新增了一種塊級作用域

簡單來說,花括號{...}內的區域就是塊級作用域,但Javascript並不是原生支援塊級作用域的,需要藉助ES6提出的letconst來建立塊級作用域

// ES5
if(true) {
  var name = '南玖'
}
console.log(name)  // 南玖

// ES6
if(true) {
  let age = 18
}
console.log(age)  // 這裡會報錯

作用域鏈

當可執行程式碼內部訪問變數時,會先查詢當前作用域下有無該變數,有則立即返回,沒有的話則會去父級作用域中查詢...一直找到全域性作用域。我們把這種作用域的巢狀機制稱為作用域鏈

詞法作用域

詞法作用域是作用域的一種工作模型,詞法作用域是JavaScript中使用的一種作用域型別,詞法作用域也可以被叫做靜態作用域

所謂的詞法作用域就是在你寫程式碼時將變數和作用域寫在哪裡來決定的,也就是詞法作用域是靜態的作用域,在你寫程式碼時就決定了。函式作用域取決於它申明的位置,與實際呼叫的位置無關

MDN對閉包的定義:

一個函式和對其周圍(詞法環境)的引用捆綁在一起(或者說函式被引用包圍),這樣一個組合就是閉包(closure

也就是說,閉包讓你可以在一個內層函式中訪問到其外層函式的作用域。在JavaScript中,每當建立一個函式,閉包就會在函式建立的同時被建立出來。

我們可以得出:

閉包 = 函式 + 外層作用域

我們先來看段程式碼:

var name = '前端南玖'

function say() {
  console.log(name)
}
say()

解析:say函式可以訪問到外層作用域的變數a,那麼這樣不就是形成了一個閉包嗎?

在《Javascript權威指南》書中有這樣一句話:嚴格來講,所以JavaScript函式都是閉包

但這只是理論上的閉包,與我們平時使用的不太一樣。上面這個例子只是一個簡單的閉包。

ECMAScript對閉包的定義:

  • 從理論上來講:所有函式都是閉包。因為它們在建立的時候就已經上層上下文的資料儲存起來了。
  • 從實踐上來講:閉包應該滿足兩個條件:1.在程式碼中引用了外層作用域的變數;2.即使建立它的上下文已經銷燬,它仍然存在;

我們再看一段《JavaScript權威指南》上的程式碼:

let scope = 'global scope'
function checkscope(){
  let scope = 'local scope'
  function f(){
    return scope
  }
  return f
}

let s = checkscope()   
s()   // 這裡返回什麼?

很多同學可能覺得是global scope,但真的是這樣嗎,我們一起來看下它的執行過程:

1.首先執行全域性程式碼,建立全域性執行上下文,定義全域性變數scope並賦值

2.申明checkscope函式,並建立該函式的執行上下文,建立區域性變數scope並賦值

3.申明f函式,建立該函式的執行上下文

4.執行checkscope函式,該函式又返回了一個f函式賦值給了變數s

5.執行s函式,相當於執行了f函式。這裡返回的scope是local scope。至於為什麼是local scope,我們上面講到了詞法作用的基本規則:JavaScript函式是使用定義它們的作用域來執行的。在定義f函式的作用域中,變數scope的值為local scope

閉包的應用

閉包的應用,絕大多是都是在維護內部變數的場景下使用

閉包的缺陷

  • 由於閉包的存在可能會造成變數常駐記憶體,使用不當會造成記憶體洩漏
  • 記憶體洩漏可能會導致應用程式卡頓或崩潰

高頻閉包面試題

var arr = []
for(var i=0;i<3;i++){
    arr[i] = function(){
        console.log(i)
    } 
}
arr[0]()  // 3
arr[1]()  // 3
arr[2]()  // 3
// 這裡在執行的時候i已經變成了3

// 使用閉包解決
var arr = []
for(var i=0;i<3;i++){
    arr[i] = (function(i){
        return function(){
            console.log(i)
        } 
    })(i)
    
}
arr[0]()  // 0
arr[1]()  // 1
arr[2]()  // 2

相關文章