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

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

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

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

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

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

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

標識符解析是沿着做用域鏈一級一級地搜索標識符的過程。搜索過程始終從做用域鏈的前端開始,而後逐級地向後回溯,直至找到標識符爲止(若是找不到標識符,一般會致使錯誤發生)。編程

---- 摘自 JavaScript高級程序設計瀏覽器

注意: 除了全局做用域以外,每一個函數都會建立本身的做用域,做用域在函數定義時就已經肯定了。而不是在函數調用時肯定。
做用域只是一個「地盤」,一個抽象的概念,其中沒有變量。要經過做用域對應的執行上下文環境來獲取變量的值。同一個做用域下,不一樣的調用會產生不一樣的執行上下文環境,繼而產生不一樣的變量的值。因此,做用域中變量的值是在執行過程當中產生的肯定的,而做用域倒是在函數建立時就肯定了。閉包

---- 摘自 https://www.cnblogs.com/wangf...模塊化

理論說完,直接上代碼。函數

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

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

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

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

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

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

三、手動執行點擊事件。

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

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

閉包

垃圾回收機制

先介紹下垃圾回收機制。

離開做用域的值將被自動標記爲能夠回收,所以將在垃圾收集期間被刪除。

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

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

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

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

如今就解釋下爲何會有不一樣的結果。

  • 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函數。

閉包的用途

一、經過做用域訪問外層函數的私有變量/方法,而且使這些私有變量/方法保留再內存中

  • 在這裏補充一道閉包的面試題,固然還涉及到了遞歸。編寫一個add函數,使得add(1)(2)(3)(4)...()返回1+2+3+4+...的值。
function add(num) {
  var count = num
  function addTemp(otherNum) {
    if (!otherNum) return count
    count += otherNum
    return addTemp  
  }
  return addTemp 
}

二、避免全局變量的污染
三、代碼模塊化 / 面向對象編程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()
相關文章
相關標籤/搜索