?JS作用域與閉包
在JavaScript中,作用域是可訪問變數,物件,函式的集合。
變數分為全域性變數和區域性變數。全域性變數在函式外定義,HTML中全域性變數是window物件,所有資料物件都屬於window物件。區域性變數在函式內定義,只能在函式內部訪問,在函式開始執行時建立,在函式執行完之後會自動銷燬。
JS的作用域分為全域性作用域和函式作用域。
全域性作用域
全域性作用域在頁面開啟時建立,在頁面關閉時銷燬。在全域性作用域中,建立的變數都會作為window物件的屬性儲存;建立的函式都會作為window物件的方法儲存。
變數在函式外定義就是全域性變數,在全域性作用域中有一個全域性物件window,可以直接使用。
全域性作用域中的變數都是全域性變數,在頁面的任意部分都可以訪問到。
var a=10;
console.log(window.a); //相當於console.log(a);
function fun(){
console.log('我是fun函式');
}
window.fun();
變數宣告提前
使用var關鍵字宣告的變數,會在所有的程式碼執行之前被宣告,但是不會被賦值
但是如果宣告變數時不使用var關鍵字,則變數不會被宣告提前
console.log("a="+a); //a=undefined
var a=123;
如果是訪問了未宣告的變數,控制檯會報錯:xxx is not defined
但是在這個例子中,我們可以發現在變數a宣告之前,依然可以訪問到a這個變數,只是這個變數的型別是undefined,還沒有被賦值。
使用函式宣告形式建立的函式 function 函式(){} 會在所有的程式碼執行之前就被建立,所以可以在函式宣告前被呼叫。
使用函式表示式建立的函式,不會被宣告提前,所以不能在宣告前建立。
函式作用域
呼叫函式時建立函式作用域,函式執行完畢後,函式作用域銷燬。
每呼叫一次函式就會建立一個新的函式作用域,他們之間是互相獨立的。
在函式作用域中,可以訪問到全域性作用域的變數,在全域性作用域中無法訪問到函式作用域的變數。
var a=10
function fun(){
var b = 20
console.log("a="+a); //a=10
}
fun()
console.log("b="+b); //b is not defined
在這個例子中,a使全域性變數,可以在函式內部被訪問到;b是定義在函式內部的區域性變數,在函式執行完之後這個變數會被自動銷燬,所以在函式外訪問不到變數b。
當在函式作用域操作一個變數時,會現在自身作用域中尋找,如果有就直接使用,如果沒有則向上一級作用域中尋找
在函式作用域中也有宣告提前的特性
使用var關鍵字宣告的變數,會在函式中所有的程式碼執行之前被宣告
function fun3(){
console.log(a);
var a= 35;
}
fun3(); //undefined
在函式中,不使用var宣告的變數都會成為全域性變數。
下面的例子中,變數myName
在函式內沒有使用var關鍵字宣告,為全域性變數。
function fun4(){
myName = "xiaoming";
}
作用域鏈
在介紹作用域鏈之前我們需要了解以下幾個概念:
①執行環境(execution context):定義了變數或函式有權訪問的其他資料,決定了他們各自的行為
②每個執行環境都有一個與之關聯的變數物件(variable object),環境中所有定義的變數和函式都儲存在這個物件中(下面會用VO()
來表示一個變數物件)
③每個函式都有自己的執行環境,當執行流進入一個函式時,函式的環境就會被推入一個環境棧中。在函式執行之後,棧將其環境彈出,把控制權返回給之前的執行環境。
當程式碼在一個環境中執行時,會建立變數物件的一個作用域鏈(scope chain)。作用域鏈是由當前環境與上層環境的一系列變數物件組成,保證了當前執行環境對符合訪問許可權的變數和函式的有序訪問。
我們可以分析一個例子
var a = 30;
function test() {
var b = a + 10;
function innerTest() {
var c = 10;
return b + c;
}
return innerTest();
}
test();
這裡有三個變數物件,我們把全域性、test函式,innerTest()的變數物件分別用VO(global)、VO(test)、VO(innerTest)
。對於innerTest來說,他的作用域鏈包含了這三個變數物件,所以可以用陣列將innerTest的作用域鏈進行表示:
scopeChain:[VO(innerTest),VO(test),VO(global)]
陣列的第一項是作用域鏈的最前端,最後一項是作用域鏈的末端。
作用域鏈的前端,始終都是當前執行的程式碼所在的環境的變數物件。如果這個環境是函式,則將其活動物件(activation object)作為變數物件(下面會用AO()
來表示一個活動物件)。活動物件在其最開始時只包含一個變數,即arguments物件(這個物件在全域性環境中時不存在的)。作用域鏈的末端始終為全域性變數物件。
作用域鏈是由一系列變數物件組成,我們可以在這條鏈中,查詢變數物件中的識別符號,這樣就可以訪問到上一層作用域中的變數了。
在這個例子中,innerTest()內部能夠訪問到其他兩個環境中的所有變數,即能夠訪問到的變數有:變數c、變數b和變數a。因為這兩個環境是它的父執行環境。
圖中的矩形表示特定的執行環境。其中,內部環境可以通過作用域鏈訪問所有的外部環境,但外部環境不能訪問內部環境中的任何變數和函式。這些環境之間的聯絡是線性的、有次序的。每個環境都可以向上搜尋作用域鏈,以查詢變數和函式名;但任何環境都不能通過向下搜尋作用域鏈而進入另一個執行環境。例如:innerTest()的區域性環境開始時會先在自己的變數物件中搜尋變數和函式名,如果搜尋不到則再搜尋上一級作用域鏈。test()的作用域鏈中只包含兩個物件:自己的變數物件和全域性變數物件。也就是說,它不能訪問innerTest()的環境。
閉包
閉包是一個可以訪問外部(封閉)函式作用域鏈中變數的內部函式。閉包可以訪問3種範圍中的變數,這3個範圍具體如下:
- 自己範圍內的變數
- 封閉函式範圍內的變數
- 全域性變數
建立閉包的常見方式,就是在一個函式內部建立另一個函式。
function createComparisonFunction(propertyName) {
return function(object1, object2) {
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
在這個例子中內部函式(匿名函式)訪問了外部函式中的變數propertyName
。即使這個內部函式被返回了,而且是在其他地方被呼叫了,但它仍然可以訪問變數propertyName
。因為內部函式的作用域鏈中包含createComparisonFunction()
的作用域。
一般來講,當函式執行完畢後,區域性活動物件就會被銷燬,記憶體中僅儲存全域性作用域(全域性執行環境的變數物件)。但是閉包的情況又不同。
在匿名函式從createComparisonFunction()
中被返回後,它的作用域鏈被初始化為包含createComparisonFunction()
函式的活動物件和全域性變數物件。這樣匿名函式就可以訪問在createComparisonFunction()
中定義的所有變數。更為重要的是createComparisonFunction()
函式在執行完畢之後,其活動物件也不會被銷燬,因為匿名函式的作用域鏈仍然在引用這個活動物件。即:當createComparisonFunction()
函式返回後,其執行環境的作用域鏈會被銷燬,但它的活動物件仍然會留在記憶體中,直到匿名函式被銷燬後,createComparisonFunction()
的活動物件才會被銷燬。
//建立函式
var compareNames = createComparisonFunction("name");
//呼叫函式
var result = compareNames({name: "mike"}, {name: "lisa"});
//解除對匿名函式的引用(釋放記憶體)
compareNames = null;
首先,建立的比較函式被儲存在變數compareNames
中,通過將compareNames
設定為null解除該函式的引用,將其清除。隨著匿名函式的作用域鏈被銷燬,其他作用域(除了全域性作用域)也都可以安全的被銷燬了。下圖展示了呼叫compareNames()
的過程中產生的作用域鏈之間的關係。
作用域鏈的配置機制引出了一個問題,就是閉包只能取得包含函式中任何變數的最後一個值。閉包所儲存的是整個變數物件,而不是某個特殊的變數。
function createFunction() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i;
};
}
return result;
}
這個函式會返回一個函式陣列。表面上看,每個函式都應該返回自己的索引值。即位置0的函式返回0,位置1的函式返回1。但實際上,每個函式都會返回10。因為每個函式的作用域鏈中都儲存著ceateFunction()
函式的活動物件,所以他們引用的都是同一個變數 i 。當ceateFunction()
函式返回後,變數 i 的值是10,此時每個函式都引用著儲存變數 i 的同一個變數物件,所以在每個函式內部 i 的值都是10。
我們可以通過建立另一個匿名函式強制讓閉包的行為符合預期。
function createFunction() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function(num) {
return function() {
return num;
};
}(i);
}
return result;
}
我們沒有直接把閉包賦值給陣列,而是定義了一個匿名函式,並將立即執行該匿名函式的結果賦值給陣列。匿名函式有一個引數num,也就是最終的函式要返回的值。在呼叫每一個匿名函式時,傳入了變數 i 。由於函式引數時按值傳遞的,所以就會將變數 i 的值賦值給引數num。在這個匿名函式的內部,又建立並返回了一個返回num的閉包。這樣,result陣列中的每個函式都有自己num變數的一個副本,因此就可以返回各自不同的數值了。
閉包的優點:不產生全域性變數,可以避免全域性變數的汙染,實現屬性私有化
閉包的缺點:會常駐記憶體,增加記憶體使用量,使用不當很容易造成記憶體洩漏,在不用的時候需要刪除
閉包有3個特性:
- 函式巢狀函式
- 在函式內部可以引用外部的引數和變數
- 引數和變數不會以垃圾回收機制回收
最後來練習一下:
for(var i=0;i<5;i++){
(function(){
setTimeout(function(){
console.log(i);
},i*1000);
})();
}
上面的程式碼會顯示5 5 5 5 5。
原因是,在迴圈中執行的每個函式將整個迴圈完成之後執行,因此會引用儲存在i中的最後一個值——5
閉包可以為每次迭代建立一個唯一的作用域,儲存作用域內的迴圈變數。
for(var i=0;i<5;i++){
(function(x){
setTimeout(function(){
console.log(x);
},x*1000);
})(i);
}
上述寫法會按預期輸出0、1、2、3和4到控制檯。