在前端開發必須知道的JS之原型和繼承一文中說過下面寫篇閉包,加之最近越來越發現需要加強我的閉包應用能力,所以此文不能再拖了。本文講的是函數閉包,不涉及對象閉包(如用with實現)。如果你覺得我說的有偏差,歡迎拍磚,歡迎指教。
一. 閉包的理論
首先必須瞭解以下幾個概念:
執行環境
每調用一個函數時(執行函數時),系統會為該函數建立一個封閉的局部的運行環境,即該函數的執行環境。函數總是在自己的執行環境中執行,如讀寫局部變數、函數參數、運行內部邏輯。建立執行環境的過程包含了建立函數的範圍,函數也是在自己的範圍下執行的。從另一個角度說,每個函數執行環境都有一個範圍鏈,子函數的範圍鏈包括它的父函數的範圍鏈。關於範圍、範圍鏈請看下面。
範圍、範圍鏈、調用對象
函數範圍分為詞法範圍和動態範圍。
詞法範圍是函數定義時的範圍,即靜態範圍。當一個函數定義時,他的詞法範圍就確定了,詞法範圍說明的是在函數結構的嵌套關係下,函數作用的範圍。這個時候也就形成了該函數的範圍鏈。範圍鏈就是把這些具有嵌套層級關係的範圍串聯起來。函數的內部[[scope]]屬性指向了該範圍鏈。
動態範圍是函數調用執行時的範圍。當一個函數被調用時,首先將函數內部[[scope]]屬性指向了函數的範圍鏈,然後會建立一個調用對象,並用該調用對象記錄函數參數和函數的局部變數,將其置於範圍鏈頂部。動態範圍就是通過把該調用對象加到範圍鏈的頂部來建立的,此時的[[scope]]除了具有定義時的範圍鏈,還具有了調用時建立的調用對象。換句話說,執行環境下的範圍等於該函數定義時就確定的範圍鏈加上該函數剛剛建立的調用對象,從而也形成了新的範圍鏈。所以說是動態範圍,並且範圍鏈也隨之發生了變化。再看這裡的範圍,其實是一個對象鏈,這些對象就是函數調用時建立的調用對象,以及他上面一層層的調用對象直到最上層的全域對象。
譬如全域環境下的函數A內嵌套了一個函數B,則該函數B的範圍鏈就是:函數B的範圍—>函數A的範圍—>全域window的範圍。當函數B調用時,尋找某標識符,會按函數B的範圍—>函數A的範圍—>全域window的範圍去尋找,實際上是按函數B的調用對象—>函數A的調用對象—>全域對象這個順序去尋找的。也就是說當函數調用時,函數的範圍鏈實際上是調用對象鏈。
閉包
在動態執行環境中,資料即時地發生變化,為了保持這些非持久型變數的值,我們用閉包這種載體來儲存這些動態資料(看完下面的應用就會很好的體會這句話)。閉包的定義:所謂“閉包”,指的是一個擁有許多變數和綁定了這些變數的環境的運算式(通常是一個函數),因而這些變數也是該運算式的一部分。
閉包就是嵌套在函數裡面的內建函式,並且該內建函式可以訪問外部函數中聲明的所有局部變數、參數和其他內建函式。當該內建函式在外部函數外被調用,就產生了閉包。(實際上任何函數都是全域範圍的內建函式,都能訪問全域變數,所以都是window的閉包)
譬如下面這個例子: 複製代碼 代碼如下:<script type="text/javascript">
function f(x) {
var a = 0;
a++;
x++;
var inner = function() {
return a + x;
}
return inner;
}
var test = f(1);
alert(test());
</script>
記憶體回收機制:如果某個對象不再被引用,該對象將被回收。
再結合前面所講的一些概念,在執行var test=f(1)時建立了f的調用對象,這裡暫且記作obj,執行完後雖然退出了外部執行環境,但內建函式inner被外部函數f外面的一個變數test引用。由於外部函數建立的調用對象obj有一個屬性指向此內建函式,而現在這個內建函式又被引用,所以調用對象obj會繼續存在,不會被記憶體回收行程回收,其函數參數x和局部變數a都會在這個調用對象中得以維持。雖然調用對象不能被直接存取,但是該調用對象已成為內建函式範圍鏈中的一部分,可以被內建函式訪問並修改,所以執行test()時,可以正確訪問x和a。所以說, 當執行了外部函數時,產生了閉包,被引用的外部函數的變數將繼續存在。
二. 閉包的應用
應用1:
這個是我在用js類比排序演算法過程遇到的問題。我要輸出每一次插入排序後的數組,如果在迴圈中寫成
setTimeout(function() { $("proc").innerHTML += arr + "<br/>"; }, i * 500);
會發現每次輸出的都是最終排好序的數組,因為arr數組不會為你保留每次排序的狀態值。為了儲存會不斷髮生變化的數組值,我們用外麵包裹一層函數來實現閉包,用閉包儲存這個動態資料。下面用了2種方式實現閉包,一種是用參數儲存數組的值,一種是用臨時變數儲存,後者必須要深拷貝。所有要通過閉包儲存非持久型變數,均可以用臨時變數或參數兩種方式實現。 xmlns="http://www.w3.org/1999/xhtml">
var a = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0];
Proc: