JS作用域與閉包

豆豆打醜小鴨發表於2022-05-17

?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個特性:

  1. 函式巢狀函式
  2. 在函式內部可以引用外部的引數和變數
  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到控制檯。

相關文章