作用域鏈、垃圾回收機制、閉包及其應用(oop)

執行環境、變量對象 / 活動對象、作用域鏈

執行環境(executioncontext,爲簡單起見,有時也稱爲“環境”)是JavaScript中最爲重要的一個概念。執行環境定義了變量或函數有權訪問的其他數據,決定了它們各自的行爲。每個執行環境都有一個與之關聯的變量對象(variableobject),環境中定義的所有變量和函數都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象,但解析器在處理數據時會在後臺使用它。

全局執行環境是最外圍的一個執行環境。根據ECMAScript實現所在的宿主環境不同,表示執行環境的對象也不一樣。在Web瀏覽器中,全局執行環境被認爲是window對象,因此所有全局變量和函數都是作爲window對象的屬性和方法創建的。某個執行環境中的所有代碼執行完畢後,該環境被銷燬,保存在其中的所有變量和函數定義也隨之銷燬(全局執行環境直到應用程序退出——例如關閉網頁或瀏覽器——時纔會被銷燬)。

每個函數都有自己的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行之後,棧將其環境彈出,把控制權返回返回給之前的執行環境。ECMAScript程序中的執行流正是由這個方便的機制控制着。當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈(scopechain)。

作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數的有序訪問。作用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。如果這個環境是函數,則將其活動對象(activationobject)作爲變量對象。

活動對象在最開始時只包含一個變量,即arguments對象(這個對象在全局環境中是不存在的)。作用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境;全局執行環境的變量對象始終都是作用域鏈中的最後一個對象。

標識符解析是沿着作用域鏈一級一級地搜索標識符的過程。搜索過程始終從作用域鏈的前端開始,然後逐級地向後回溯,直至找到標識符爲止(如果找不到標識符,通常會導致錯誤發生)。

---- 摘自 JavaScript高級程序設計

理論說完,直接上代碼。

function Fn() {
  var count = 0
  function innerFn() {
    count ++
    console.log('inner', count)
  }
  return innerFn
}

var fn = Fn()
document.querySelector('#btn').addEventListener('click', ()=> {
  fn()
  Fn()()
})

1、 瀏覽器打開,進入全局執行環境,也就是window對象,對應的變量對象就是全局變量對象。

  • 在全局變量對象裏定義了兩個變量:Fn和fn。

2、當代碼執行到fn的賦值時,執行流進入Fn函數,Fn的執行環境被創建並推入環境棧,與之對應的變量對象也被創建,當Fn的代碼在執行環境中執行時,會創建變量對象的一個作用域鏈,這個作用域鏈首先可以訪問本地的變量對象(當前執行的代碼所在環境的變量對象),往上可以訪問來自包含環境的變量對象,如此一層層往上直到全局環境。

  • Fn的變量對象裏有兩個變量:count和innerFn,其實還有arguments和this,這裏先忽略。然後函數返回了innerFn函數出去賦給了fn。

3、手動執行點擊事件。

  • 首先,執行流進入了fn函數,實際上是進入了innerFn函數,innerFn的執行環境被創建並推入環境棧,執行innerFn代碼,通過作用域鏈對Fn的活動對象中的count進行了+1,並且打印。執行完畢,環境出棧。
  • 然後,執行流進入了Fn函數,Fn的執行跟第2步的一樣,返回了innerFn。接着執行了innerFn函數,innerFn的執行跟前面的一樣。
  • 每一次點擊都執行了fn, Fn, innerFn,而fn和innerFn其實是一樣邏輯的函數,但控制檯打印出來的結果卻有所不同。

clipboard.png
點擊了3次的結果,接下來進入閉包環節。

閉包

垃圾回收機制

先介紹下垃圾回收機制。

離開作用域的值將被自動標記爲可以回收,因此將在垃圾收集期間被刪除。

“標記清除”是目前主流的垃圾收集算法,這種算法的思想是給當前不使用的值加上標記,然後再回收其內存。

---- 摘自 JavaScript高級程序設計

通俗點說就是:
1、函數執行完了,其執行環境會出棧,其變量對象自然就離開了作用域,面臨着被銷燬的命運。但是如果其中的某個變量被其他作用域引用着,那麼這個變量將繼續保持在內存當中。
2、全局變量對象在瀏覽器關閉時纔會被銷燬。

接下來看看上面的代碼。
對了先畫張圖。
圖片描述

現在就解釋下爲什麼會有不同的結果。

  • Fn()() --- 執行Fn函數,return了innerFn函數並立即執行了innerFn函數,因爲innerFn函數引用了Fn變量對象中的count變量,所以即使Fn函數執行完了,count變量還是保留在內存中。等innerFn執行完了,引用也隨之消失,此時count變量被回收。所以每次運行Fn()(),count變量的值都是1。
  • fn() --- 從fn的賦值開始說起,Fn函數執行後return了innerFn函數賦值給了fn。從這個時候開始Fn的變量對象中的count變量就被innerFn引用着,而innerFn被fn引用着,被引用的都存在於內存中。然後執行了fn函數,實際上執行了存在於內存中的innerFn函數,存在於內存中的count++。執行完成後,innerFn還是被fn引用着,由於fn是全局變量除了瀏覽器關閉外不會被銷燬,以至於這個innerFn函數沒有被銷燬,再延申就是innerFn引用的count變量也不會被銷燬。所以每次運行fn函數實際上執行的還是那個存在於內存中的innerFn函數,自然引用的也是那個存在於內存中的count變量。不像Fn()(),每次的執行實際上都開闢了一個新的內存空間,執行的也是新的Fn函數和innerFn函數。

閉包的用途

1、通過作用域訪問外層函數的私有變量/方法,並且使這些私有變量/方法保留再內存中
2、避免全局變量的污染
3、代碼模塊化 / 面向對象編程oop

  • 舉個例子
function Animal() {
  var hobbies = []
  return {
    addHobby: name => {hobbies.push(name)},
    showHobbies: () => {console.log(hobbies)}
  }
}
var dog = Animal()
dog.addHobby('eat')
dog.addHobby('sleep')
dog.showHobbies()

定義了一個Animal的方法,裏面有一個私有變量hobbies,這個私有變量外部無法訪問。全局定義了dog的變量,並且把Animal執行後的對象賦值給了dog(其實dog就是Animal的實例化對象),通過dog對象裏的方法就可以訪問Animal中的私有屬性hobbies。這麼做可以保證私有屬性只能被其實例化對象訪問,並且一直保留在內存中。當然還可以實例化多個對象,每個實例對象所引用的私有屬性也互不相干。

當然還可以寫成構造函數(類)的方式

function Animal() {
  var hobbies = []
  this.addHobby = name => {hobbies.push(name)},
  this.showHobbies = () => {console.log(hobbies)}
}
var dog = new Animal()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章