1,閉包是什麼
(百度百科定義)--閉包是可以包含自由(未綁定到特定對象)變數的代碼塊;這些變數不是在這個代碼塊內或者任何全域上下文中定義的,而是在定義代碼塊的環境中定義(局部變 量)。“閉包” 一詞來源於以下兩者的結合:要執行的代碼塊(由於自由變數被包含在代碼塊中,這些自由變數以及它們引用的對象沒有被釋放)和為自由變數提供綁定的計算環境 (範圍)。
W3C ECMAScript在閉包的解釋中,有這麼一句話:閉包,指的是詞法表示包括不被計算的變數的函數,也就是說,函數可以使用函數之外定義的變數就稱為閉包。
要瞭解閉包,在javascript王者歸來中,提出了文法域和執行域,來協助我們理解閉包。
文法域:定義某個程式段落時的地區。執行域:實際調用某個程式段落時所影響的地區()
這裡我的理解是:文法域,比如一個變數在函數中的範圍就是在函數內,不能在函數外訪問。執行域:通過一些運算式的改變,訪問到範圍外的內容,或被範圍外所訪問。
所謂閉包:是指文法域位於某個特定的地區,具有持續參照(讀寫)位於該地區內自身範圍之外的執行域上的非持久型變數值能力的段落。這些外部執行域的非持久型變數神奇的保留它們在閉包最初定義(或建立)時的值。
總結閉包的定義:它是某些可以訪問外部執行域的段落,在javascript 中,通過定義在函數內部的function來實現
閉包是指有權訪問另一個函數範圍中的變數的函數,建立閉包的常見方式,就是在一個函數內部建立另一個函數。
警告:不要對閉包和匿名函數充滿了疑惑,匿名函數就是一個沒有名字的函數而已,而閉包是訪問函數之外定義的變數。如果這個匿名函數能夠訪問函數之外定義的變數,那麼它就是一個閉包。
這上面的總結是一個星期前寫的,今天看了看,又有新的收穫。想想之前去面試,必問一個問題:什麼是閉包,現在才知道人家為什麼要問你對閉包的理解,因為要完全理解閉包,需要瞭解 範圍鏈,執行環境,這樣你才能在使用中遊刃有餘。
2,執行環境
因為閉包也是一個函數,所以要理解閉包,就要理解函數第一次被調用的時候發生了什嗎?下面來看看javascript的執行環境,就是上面提到的執行域
執行環境定義了變數或函數有權訪問的其他資料,決定了它們各自的行為。每個執行環境都有一個與之關聯的變數對象,環境中定義的所有變數和函數都儲存在這個對象中,雖然我們編寫的代碼無法訪問這個對象,但解析器在處理資料時會在後台使用它。
全域執行環境是最外圍的一個執行環境
在瀏覽器中,全域執行環境被認為是 window對象,因此所有全域變數和函數都是作為window對象的屬性和方法建立的
某個執行環境中的所有代碼執行完畢後,該環境被銷毀。儲存在其中的變數和函數定義也隨之銷毀(全域執行環境直到應用程式退出--例如關閉網頁或瀏覽器--時才會被銷毀)
舉個例子:就像我們一直呆在地球上,而地球就像我們的文法域,而我們的執行域 就像神六發射到月球一樣,楊利偉可以在月球行走,這裡的地球和月球的空間就是我們所說的執行域,在沒有發射神六時,楊利偉也只能在地球上,就像我們函數,沒有執行時,它就是被{ }這兩個符號包圍著,當神六到了月球時,楊利偉就能在月球走動,就想函數運行時,它可能就引用它以外的變數,這就是我們所說的執行環境,我個人理解就是 在執行的瞬間,它所包含的變數對象。
執行環境,故名思議,在執行過程中產生的,執行完畢後這個就消失了,所以你是不可訪問,就向神六上月球一樣,一旦返回地球,你就沒有對訪問月球的執行環境了
每個函數都有自己的執行環境,當執行流進入一個函數時,函數的環境會被推入一個環境棧中。而在函數執行完畢後,棧將其環境彈出,把控制權返回給之前的執行環境,ECMAScript程式中的執行流正是由這個機制控制著。
我對上面這句話的理解:當函數執行完畢後,然後開始執行包含它的變數對象,這樣的一直去執行,直到全域環境。
在javascript中,分為全域和局部(函數)範圍,這裡的局部指函數內,說到這,就得提一下變數聲明了,使用var在函式宣告變數為局部變數,如果在函數內沒有使用var 聲明變數,將預設為全域變數。
var a = 1;
function A(){
alert(a);
}
A();
一個簡單的閉包,你可能會疑問,這麼簡單的一個函數也是閉包嗎? 是的,只要使用函數之外定義的變數就是閉包。
最簡單的例子,上面的代碼中,A() 的範圍鏈包含兩個對象,它自身的變數對象(其中定義著arguments對象)、和全域環境的變數對象,可以在函數內部訪問 變數a,是因為可以在範圍鏈中找到它。
那麼它是怎麼搜尋的呢,首先它會從範圍鏈的前端開始(也就是當前執行的代碼所在環境的變數對象)、也就是A() 函數內,在A函數沒有找到,於是到它的包含環境(全域執行環境的變數對象)中尋找,發現有a變數,停止搜尋。
變數查詢不是沒有代價的,很明顯,訪問局部變數要比訪問全域變數更快,因為不用向上搜尋範圍鏈。
下面看一個複雜的例子:
var global = "global";
function A(){
var a = "a";
function B(){
var b = "b";
alert(global +","+ a);
}
B();
}
A();
這幅圖,大概就能看出什麼意思了,參考上面那個簡單的代碼,如果有什麼不明白的,希望留言,我再來補充。
3,範圍鏈
當代碼在一個環境中執行時,會建立變數對象的一個範圍鏈。範圍鏈的用途,是保證對執行環境有權訪問的所有變數和函數的有序訪問。範圍鏈的前端,始終都是當前執行的代碼所在的環境的變數對象,
如果這個環境是函數,則將其使用中的物件作為變數對象,使用中的物件在最開始時只包含一個變數,即arguments 對象(這個對象在全域環境中是不存在的)。
範圍鏈中的下一個變數對象來自包含(外部)環境,而再下一個變數對象則來自下一個包含環境,這樣,一直延續到全域變數,全域執行環境的變數對象始終都是範圍鏈中的最後一個對象。
標識符解析是沿著範圍鏈一級一級的搜尋標識符的過程,搜尋過程始終從範圍鏈的前端開始,然後逐級的向後回溯,直到找到標識符為止(如果找不到標識符,通常會導致錯誤發生)。
用大白話來理解,當程式中的函數執行時,會建立一個範圍鏈,你可以把他當做搜尋範圍,這裡有一個範圍鏈前端,這個指什麼呢?我的理解就是當前函數,例如在上面的代碼中 範圍鏈的前端就是B 函數,然後下一個變數對象A 函數,然後全域的。
延長範圍鏈(這個知識瞭解就行,如需深入瞭解,自行尋找)
通過其他辦法來延長範圍鏈,因為有些語句可以在範圍鏈的前端臨時增加一個變數對象,該變數對象會在代碼執行後被移除,
那麼函數是怎麼釋放的呢?來看看記憶體回收(更多瞭解請自行尋找,這個瞭解就行)
分析函數中局部變數的正常聲明周期,局部變數只在函數執行的過程中存在,在這個過程中,會為局部變數在記憶體上分配相應的空間,以便儲存他們,然後在函數中使用這些變數,直至函數執行結束,此時,局部變數就沒有存在的必要了,因此可以釋放它們的記憶體供將來使用
4,閉包的用法
學習了執行環境、範圍鏈、閉包的概念,你是不是不知道怎麼使用呢?(把下面的代碼拷貝下來,自己執行一下)
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8" />
<title>閉包示範</title>
<style type="text/css">
p {background:green;}
</style>
<script type="text/javascript">
function init() {
var pAry = document.getElementsByTagName("p");
for( var i=0; i<pAry.length; i++ ) {
pAry[i].onclick = function(){ // 點擊事件哦
alert(i);
}
}
}
</script>
</head>
<body onload="init();">
<p>產品 0</p>
<p>產品 1</p>
<p>產品 2</p>
<p>產品 3</p>
<p>產品 4</p>
</body>
</html>
上面的代碼執行過程中,你會發現,每次彈出都是4,是不是 不和你想象的一樣,這是為什麼呢?
因為onclick 這個函數運算式,使用了閉包(使用了函數外的變數 i ),根據執行環境中的範圍鏈,因為每個函數的範圍中都儲存著 init 函數的使用中的物件,所以它們引用的都是同一個變數i,當 init 函數返回後,變數 i 的值是4,此時每個函數都引用著儲存變數 i 的同一個變數對象,所以在每個函數內部 i 的值都是4
簡單的 說:這個變數,總能訪問到全域範圍的i
解決辦法:不要讓它總是訪問到全域範圍的 i,讓它在調用匿名函數時,給它傳入一個參數,因為函數是按值傳遞的(對象也是按值傳遞哦),所以就會將變數i的值賦給參數_i,因為是按值傳遞,所以你懂得。
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8" />
<title>閉包示範</title>
<style type="text/css">
p {background:green;}
</style>
<script type="text/javascript">
function init() {
var pAry = document.getElementsByTagName("p");
for( var i=0; i<pAry.length; i++ ) {
(function(_i){
pAry[_i].onclick = function(){
alert(_i);
}
})(i);
}
}
</script>
</head>
<body onload="init();">
<p>產品 0</p>
<p>產品 1</p>
<p>產品 2</p>
<p>產品 3</p>
<p>產品 4</p>
</body>
</html>
上面那種方法,我認為是最好理解的,對於按值傳遞啊,範圍鏈
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8" />
<title>閉包示範</title>
<style type="text/css">
p {background:green;}
</style>
<script type="text/javascript">
function init() {
var pAry = document.getElementsByTagName("p");
for( var i=0; i<pAry.length; i++ ) {
pAry[i].onclick = function(_i){
return function(){
alert(_i);
}
}(i);
}
}
</script>
</head>
<body onload="init();">
<p>產品 0</p>
<p>產品 1</p>
<p>產品 2</p>
<p>產品 3</p>
<p>產品 4</p>
</body>
</html>
上面那種方法是別人的,總之重在理解,下次遇到能夠很快的去解決