1.作用域鏈
1.1.什么是作用域
談起作用域鏈,我們就不得不從作用域開始談起。因為所謂的作用域鏈就是由多個作用域組成的。那么, 什么是作用域呢?
1.1.1作用域是一個函數在執行時期的執行環境。
每一個函數在執行的時候都有着其特有的執行環境,ECMAScript標准規定,在javascript中只有函數才擁有作用域。換句話,也就是說,JS中不存在塊級作用域。比如下面這樣:
function getA() { if (false) { var a = 1; } console.log(a); //undefined } getA(); function getB() { console.log(b); } getB(); // ReferenceError: b is not defined
上面的兩段代碼,區別在於 :getA()函數中,有變量a的聲明,而getB()函數中沒有變量b的聲明。
另外還有一點,關於作用域中的聲明提前。
1.1.2.作用域中聲明提前
在上面的getA()函數中,或許你還存在着疑惑,為什么a="undefined"呢,具體原因就是因為作用域中的聲明提前:所以getA()函數和下面的寫法是等價的:
function getA(){ var a; if(false){ a=1 }; console.log(a); }
既然提到變量的聲明提前,那么只需要搞清楚三個問題即可:
1.什么是變量
2.什么是變量聲明
3.聲明提前到什么時候。
什么是變量?
變量包括兩種,普通變量和函數變量。
- 普通變量:凡是用var標識的都是普通變量。比如下面 :
var x=1; var object={}; var getA=function(){}; //以上三種均是普通變量,但是這三個等式都具有賦值操作。所以,要分清楚聲明和賦值。聲明是指 var x; 賦值是指 x=1;
- 函數變量:函數變量特指的是下面的這種,fun就是一個函數變量。
function fun(){} ;// 這是指函數變量. 函數變量一般也說成函數聲明。
類似下面這樣,不是函數聲明,而是函數表達式var getA=function(){} //這是函數表達式 var getA=function fun(){}; //這也是函數表達式,不存在函數聲明。關於函數聲明和函數表達式的區別,詳情見javascript系列---函數篇第二部分
什么是變量聲明?
變量有普通變量和函數變量,所以變量的聲明就有普通變量聲明和函數變量聲明。
- 普通變量聲明
var x=1; //聲明+賦值 var object={}; //聲明+賦值
上面的兩個變量執行的時候總是這樣的
var x = undefined; //聲明 var object = undefined; //聲明 x = 1; //賦值 object = {}; //賦值
關於聲明和賦值,請注意,聲明是在函數第一行代碼執行之前就已經完成,而賦值是在函數執行時期才開始賦值。所以,聲明總是存在於賦值之前。而且,普通變量的聲明時期總是等於undefined.
- 函數變量聲明
函數變量聲明指的是下面這樣的:
function getA(){}; //函數聲明
聲明提前到什么時候?
所有變量的聲明,在函數內部第一行代碼開始執行的時候就已經完成。-----聲明的順序見1.2作用域的組成
1.2.作用域的組成
函數的作用域,也就是函數的執行環境,所以函數作用域內肯定保存着函數內部聲明的所有的變量。
一個函數在執行時所用到的變量無外乎來源於下面三種:
1.函數的參數----來源於函數內部的作用域
2.在函數內部聲明的變量(普通變量和函數變量)----也來源於函數內部作用域
3.來源於函數的外部作用域的變量,放在1.3中講。
比如下面這樣:
var x = 1; function add(num) () { var y = 1; return x + num + y; //x來源於外部作用域,num來源於參數(參數也屬於內部作用域),y來源於內部作用域。 }
那么一個函數的作用域到底是什么呢?
在一個函數被調用的時候,函數的作用域才會存在。此時,在函數還沒有開始執行的時候,開始創建函數的作用域:
函數作用域的創建步驟:
1. 函數形參的聲明。
2.函數變量的聲明
3.普通變量的聲明。
4.函數內部的this指針賦值
......函數內部代碼開始執行!
所以,在這里也解釋了,為什么說函數被調用時,聲明提前,在創建函數作用域的時候就會先聲明各種變量。
關於變量的聲明,這里有幾點需要強調
1.函數形參在聲明的時候已經指定其形參的值。
function add(num) { var num; console.log(num); //1 } add(1);
2.在第二步函數變量的生命中,函數變量會覆蓋以前聲明過的同名聲明。
function add(num1, fun2) { function fun2() { var x = 2; } console.log(typeof num1); //function console.log(fun2.toString()) //functon fun2(){ var x=2;} } add(function () { }, function () { var x = 1 });
3. 在第三步中,普通變量的聲明,不會覆蓋以前的同名參數
function add(fun,num) { var fun,num; console.log(typeof fun) //function console.log(num); //1 } add(function(){},1);
在所有的聲明結束后,函數才開始執行代碼!!!
1.3.作用域鏈的組成
在JS中,函數的可以允許嵌套的。即,在一個函數的內部聲明另一個函數
類似這樣:
function A(){ var a=1; function B(){ //在A函數內部,聲明了函數B,這就是所謂的函數嵌套。 var b=2; } }
對於A來說,A函數在執行的時候,會創建其A函數的作用域, 那么函數B在創建的時候,會引用A的作用域,類似下面這樣
函數B在執行的時候,其作用域類似於下面這樣:
從上面的兩幅圖中可以看出,函數B在執行的時候,是會引用函數A的作用域的。所以,像這種函數作用域的嵌套就組成了所謂的函數作用域鏈。當在自身作用域內找不到該變量的時候,會沿着作用域鏈逐步向上查找,若在全局作用域內部仍找不到該變量,則會拋出異常。
2.什么是閉包
閉包的概念:有權訪問另一個作用域的函數。
這句話就告訴我們,第一,閉包是一個函數。第二,閉包是一個能夠訪問另一個函數作用域。
那么,類似下面這樣,
function A(){ var a=1; function B(){ //閉包函數,函數b能夠訪問函數a的作用域。所以,像類似這么樣的函數,我們就稱為閉包 } }
所以,創建閉包的方式就是在一個函數的內部,創建另外一個函數。那么,當外部函數被調用的時候,內部函數也就隨着創建,這樣就形成了閉包。比如下面。
var fun = undefined; function a() { var a = 1; fun = function () { } }
3.閉包所引起的問題
其實,理解什么是閉包並不難,難的是閉包很容易引起各種各樣的問題。
3.1.變量污染
看下面的這道例題:
var funB, funC; (function() { var a = 1; funB = function () { a = a + 1; console.log(a); } funC = function () { a = a + 1; console.log(a); } }()); funB(); //2 funC(); //3.
對於 funB和funC兩個閉包函數,無論是哪個函數在運行的時候,都會改變匿名函數中變量a的值,這種情況就會污染了a變量。
兩個函數的在運行的時候作用域如下圖:
這這幅圖中,變量a可以被函數funB和funC改變,就相當於外部作用域鏈上的變量對內部作用域來說都是靜態的變量,這樣,就很容易造成變量的污染。還有一道最經典的關於閉包的例題:
var array = [ ]; for (var i = 0; i < 10; i++) { var fun = function () { console.log(i); } array.push(fun); } var index = array.length; while (index > 0) { array[--index](); } //輸出結果 全是10;
想這種類似問題產生的根源就在於,沒有注意到外部作用域鏈上的所有變量均是靜態的。
所以,為了解決這種變量的污染問題---而引入的閉包的另外一種使用方式。
那種它是如何解決這種變量污染的呢? 思想就是: 既然外部作用域鏈上的變量時靜態的,那么將外部作用域鏈上的變量拷貝到內部作用域不就可以啦!! 具體怎么拷貝,當然是通過函數傳參的形式啊。
以第一道例題為例:
var funB,funC; (function () { var a = 1; (function () { funB = function () { a = a + 1; console.log(a); } }(a)); (function (a) { funC = function () { a = a + 1; console.log(a); } }(a)); }()); funB()||funC(); //輸出結果全是2 另外也沒有改變作用域鏈上a的值。
在函數執行時,內存的結構如圖所示:
由圖中內存結構示意圖可見,為了解決閉包的這種變量污染的問題,而加了一層函數嵌套(通過匿名函數自執行),這種方式延長了閉包函數的作用域鏈。
3.2.內存泄露
內存泄露其實嚴格來說,就是內存溢出了,所謂的內存溢出,當時就是內存空間不夠用了啊。
那么,閉包為什么會引起內存泄露呢?
var fun = undefined; function A() { var a = 1; fun = function () { } }
看上面的例題,只要函數fun存在,那么函數A中的變量a就會一直存在。也就是說,函數A的作用域一直得不到釋放,函數A的作用域鏈也不能得到釋放。如果,作用域鏈上沒有很多的變量,這種犧牲還可有可無,但是如果牽扯到DOM操作呢?
var element = document.getElementById('myButton'); (function () { var myDiv = document.getElementById('myDiv') element.onclick = function () { //處理程序 } }())
像這樣,變量myDiv如果是一個占用內存很大的DOM....如果持續這么下去,內存空間豈不是一直得不到釋放。久而久之,變引起了內存泄露(也是就內存空間不足)。