談起做用域鏈,咱們就不得不從做用域開始談起。由於所謂的做用域鏈就是由多個做用域組成的。那麼, 什麼是做用域呢?javascript
每個函數在執行的時候都有着其特有的執行環境,ECMAScript標準規定,在javascript中只有函數才擁有做用域。換句話,也就是說,JS中不存在塊級做用域。好比下面這樣:html
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的聲明。java
另外還有一點,關於做用域中的聲明提早。閉包
在上面的getA()函數中,或許你還存在着疑惑,爲何a="undefined"呢,具體緣由就是由於做用域中的聲明提早:因此getA()函數和下面的寫法是等價的:函數
function getA(){ var a; if(false){ a=1 }; console.log(a); }
既然提到變量的聲明提早,那麼只須要搞清楚三個問題便可:post
1.什麼是變量this
2.什麼是變量聲明spa
3.聲明提早到何時。指針
什麼是變量?code
變量包括兩種,普通變量和函數變量。
var x=1; var object={}; var getA=function(){}; //以上三種均是普通變量,可是這三個等式都具備賦值操做。因此,要分清楚聲明和賦值。聲明是指 var x; 賦值是指 x=1;
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.在函數內部聲明的變量(普通變量和函數變量)----也來源於函數內部做用域
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);
在全部的聲明結束後,函數纔開始執行代碼!!!
在JS中,函數的能夠容許嵌套的。即,在一個函數的內部聲明另外一個函數
相似這樣:
function A(){ var a=1; function B(){ //在A函數內部,聲明瞭函數B,這就是所謂的函數嵌套。 var b=2; } }
對於A來講,A函數在執行的時候,會建立其A函數的做用域, 那麼函數B在建立的時候,會引用A的做用域,相似下面這樣
函數B在執行的時候,其做用域相似於下面這樣:
從上面的兩幅圖中能夠看出,函數B在執行的時候,是會引用函數A的做用域的。因此,像這種函數做用域的嵌套就組成了所謂的函數做用域鏈。當在自身做用域內找不到該變量的時候,會沿着做用域鏈逐步向上查找,若在全局做用域內部仍找不到該變量,則會拋出異常。
閉包的概念:有權訪問另外一個做用域的函數。
這句話就告訴咱們,第一,閉包是一個函數。第二,閉包是一個可以訪問另外一個函數做用域。
那麼,相似下面這樣,
function A(){ var a=1; function B(){ //閉包函數,函數b可以訪問函數a的做用域。因此,像相似這麼樣的函數,咱們就稱爲閉包 } }
因此,建立閉包的方式就是在一個函數的內部,建立另一個函數。那麼,當外部函數被調用的時候,內部函數也就隨着建立,這樣就造成了閉包。好比下面。
var fun = undefined; function a() { var a = 1; fun = function () { } }
其實,理解什麼是閉包並不難,難的是閉包很容易引發各類各樣的問題。
看下面的這道例題:
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的值。
在函數執行時,內存的結構如圖所示:
由圖中內存結構示意圖可見,爲了解決閉包的這種變量污染的問題,而加了一層函數嵌套(經過匿名函數自執行),這種方式延長了閉包函數的做用域鏈。
內存泄露其實嚴格來講,就是內存溢出了,所謂的內存溢出,當時就是內存空間不夠用了啊。
那麼,閉包爲何會引發內存泄露呢?
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....若是持續這麼下去,內存空間豈不是一直得不到釋放。長此以往,變引發了內存泄露(也是就內存空間不足)。