JS進擊之路:閉包

引言

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

定義

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

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)


上面是一個簡單的函數調用,以及在執行時的上下文環境,重點看執行時上下文環境,在創建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


接下來來分析下上面閉包的執行上下環境,在一個函數內部定義的函數會將包含函數的活動對象添加到它的作用域鏈中,因此,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
}


上面的代碼展示就是面試題裏面經常會碰到,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賦值給一個變量,內部函數是可以訪問外部函數變量的,這樣就解決了

總結

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章