JS進擊之路:閉包

william_li發表於2019-03-31

引言

閉包這個詞對很多前端開發人員來說既熟悉又陌生,熟悉是因為很多人都用過閉包,但是用的時候不知道閉包,陌生是因為並不理解閉包,接下來這篇文章將會從多方面介紹閉包

定義

閉包是怎麼定義的呢?當函式可以記住並訪問所在的詞法作用域時,就產生了閉包,即使函式在當前詞法作用域之外執行。來看一個具體例子:

function foo () {
  var a = 2
  function bar () {
    console.log(a)
  }
  return bar
}
var baz = foo()
baz() //2
複製程式碼

函式bar的詞法作用域可以訪問foo的內部作用域,並且bar在被作為返回值賦值給baz執行時,bar函式在定義時的詞法作用域以外的地方被呼叫,依然可以訪問foo函式的內部作用域變數a,這就是閉包

分析

現在讓我們來看為什麼閉包可以在定義的詞法作用域外記住並且訪問定義時的詞法作用域的變數,想要一探究竟,先來看一個簡單的例子來函式的執行過程:

function foo (a) {
  console.log(a)
}
foo (a)
複製程式碼

JS進擊之路:閉包
上面是一個簡單的函式呼叫,以及在執行時的上下文環境,重點看執行時上下文環境,在建立foo函式時,會建立一個預先包含全域性變數物件的作用域鏈,這個作用域鏈被儲存在內部的[[Scope]]屬性中,當呼叫foo()函式時,會為函式建立一個執行環境,然後通過複製函式的[[Scope]屬性中的物件構建起執行環境的作用域鏈。此後,又有一個活動物件(包含this、arguments、a)被建立並被推入執行環境作用域鏈的前端,對於foo函式來說,其作用域鏈包含兩個變數物件,一個時全域性的變數物件,一個是區域性的活動變數物件,一般來說當函式執行完成後,區域性的活動變數物件會被銷燬,只留全域性的,但是閉包執行過程有所不同,來看具體例子:

function foo () {
  var a = 2
  function bar (b) {
    console.log(a + b)
  }
  return bar
}
var baz = foo()
baz(3) //5
複製程式碼

JS進擊之路:閉包
接下來來分析下上面閉包的執行上下環境,在一個函式內部定義的函式會將包含函式的活動物件新增到它的作用域鏈中,因此,bar函式的作用域鏈中會包含foo函式的活動物件,在bar函式從foo中被返回後,它的作用域鏈條被初始化為全域性變數和foo中活動物件,因此,bar函式可以訪問foo函式中定義的所有變數,同時foo函式在執行完畢後,其活動物件也不會被銷燬,因為bar函式的作用域鏈仍然在引用這個活動物件。

常見問題

說到閉包相關的問題,最典型的就是變數和this指向這兩類問題。

變數

function test () {
  var result = new Array()
  for (var i = 0; i < 6; i++) {
    result[i] = function () {
      return i
    }
  }
  return result
}
複製程式碼

JS進擊之路:閉包
上面的程式碼展示就是面試題裡面經常會碰到,result的結果從上面截圖能看到,作用域中儲存的i都是6,這是為什麼呢?因為閉包儲存的是函式中的活動物件,因此它們引用的都是同一個變數,並且是變數的最後一個值,因此都是6,那這個問題怎麼解決呢?最常見的最簡單肯定是將var換成let,也可以像下面這樣:

function test () {
  var result = new Array()
  for (var i = 0; i < 6; i++) {
    result[i] = (function () {
      return i
    })()
  }
  return result
}
複製程式碼

將閉包直接改成一個自執行函式,自執行函式本身是沒有變數作用域的,因此會使用外層函式的變數作用域,這樣也能達到我們想要的效果

this指向

var name = "window"
var obj = {
  name: "object",
  getName: function () {
    return function () {
      return this.name
    }
  }
}
console.log(obj.getName()())
複製程式碼

上面這段js程式碼的this.name的返回值是window,這是為什麼呢?按照上面寫到的,此匿名函式在執行過程中,它的作用域會包含三部分:自身的活動物件、getName函式的活動物件和全域性的變數物件,同時每個活動物件自動取得兩個特殊的變數:this和arguments,但是內部函式在查詢this時是無法直接訪問外部函式的this變數,因此會沿著作用域鏈去查詢全域性變數中繼續查詢,如果想要取外部函式中的this取值也很簡單,只需要向下面程式碼這樣:

var name = "window"
var obj = {
  name: "object",
  getName: function () {
    var that = this
    return function () {
      return that.name
    }
  }
}
console.log(obj.getName()())
複製程式碼

將this賦值給一個變數,內部函式是可以訪問外部函式變數的,這樣就解決了

總結

閉包是一個容易混淆不清的概念,這篇文章對閉包的定義、執行、常見問題做了簡單的介紹,希望通過這篇能對大家理解和使用閉包有所幫助。如果有錯誤或不嚴謹的地方,歡迎批評指正,如果喜歡,歡迎點贊。