一、閉包
JavaScript中允許嵌套函數,允許函數用作數據(可以把函數賦值給變量,存儲在對象屬性中,存儲在數組元素中),並且使用詞法作用域,這些因素相互交互,創造了驚人的,強大的閉包效果。【update20170501】
閉包就是指有權訪問 另一個函數作用域 中的變量 的函數 !!!
好處:靈活方便,可封裝
缺點:空間浪費、內存泄露、性能消耗
由於閉包會攜帶包含它的函數的作用域,因此會比其他函數占用更多的內存。過度使用閉包可能會導致內存占用過多,建議只在絕對必要時再考慮使用閉包。雖然像V8等優化后的JavaScript引擎會嘗試回收被閉包占用的內存,還是要慎重使用閉包。
1、原理分析[update20170322]
無論什么時候在函數中訪問一個變量時,就會從作用域鏈中搜索具有相應名字的變量。一般來講,當函數執行完畢后,局部活動對象就會被銷毀,內存中僅保存全局作用域(全局執行環境的變量對象)。但是,閉包的情況有所不同。
例:以此為例說明閉包原理
function createComparisonFunction(propertyName){ return function(object1,object2){ //匿名函數中value1和value2訪問了外部函數中的變量propertyName var value1=object1[propertyName]; var value2=object2[propertyName]; if(value1<value2){ return -1; }else if(value1>value2){ return 1; }else{ return 0; } } } //創建函數 var compareNames=createComparisonFunction("name"); //調用函數 var result=compareNames({name:"Nicholas"},{name:"Gerg"}); //1 //解除對匿名函數的引用(以便釋放內存) compareNames=null;
即使內部函數(匿名函數)被返回了,而且在其他地方被調用了,它仍然可以訪問變量propertyName。之所以還能夠訪問這個變量,是因為內部函數的作用域鏈中包含外部函數createComparisonFunction()的作用域。
在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中。因此,在createComparisonFunction()函數內部定義的匿名函數的作用域鏈中,實際上將會包含外部函數createComparisonFunction()的活動對象。
在匿名函數從createComparisonFunction()中返回后,它的作用域鏈被初始化為包含createComparisonFunction()函數的活動對象和全局對象。
這樣匿名函數就可以訪問在createComparisonFunction()中定義的所有變量。更為重要的是,createComparisonFunction()函數在執行完畢后,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。
換句話說,當createComparisonFunction()函數返回后,其執行環境的作用域鏈會被銷毀,但它的活動對象仍然會留在內存中;直到匿名函數被銷毀后,createComparisonFunction()的活動對象才會被銷毀。
2、簡單例子
一般函數執行完后局部變量釋放,有閉包則局部變量不能在函數執行完釋放。
例1:
調用outer()返回匿名函數,這個匿名函數仍然可以訪問外部outer的局部變量localVal,所以outer執行完成后localVal不能被釋放。
outer()調用結束,func()再次調用的時候仍然能訪問到外層的outer()這個外函數的局部變量。這種情況就是通常所說的閉包。
例2:【update20170307】
//創建一個名為quo的構造函數 //它構造出帶有get_status方法和status私有屬性的一對象。 var quo=function(status){ return{ get_status:function(){ return status; } } } //構造一個quo實例 var myQuo=quo("amazed"); document.writeln(myQuo.get_status());//amazed
quo函數被設計成無須在前面加上new來使用,所以名字也沒有首字母大寫。調用quo時,它返回包含get_status方法的一個新對象。該對象的一個引用保存在myQuo中。即使quo已經返回了,但get_status方法仍然享有訪問quo對象的status屬性的特權。get_status方法並不是訪問該參數的一個副本,它訪問的就是該參數本身。這是可能的,因為該函數可以訪問它被創建時所處的上下文環境。這被稱為閉包。
3、前端閉包
例1:定義一個函數,它設置一個DOM節點為黃色,然后把它漸變為白色
var fade=function(node){ var level=1; var step=function(){ var hex=level.toString(16); node.style.backgroundColor='#FFFF'+hex+hex; if(level<15){ level+=1; setTimeout(step,100); } } setTimeout(step,100); } fade(document.body);//調用fade,把document.body作為參數傳遞給它(HTML<body>標簽所創建的節點)
fade函數設置level為1,。它定義了一step函數;接着調用setTimeout,並傳遞step函數和一個時間(100毫秒)給它。然后setTimeout返回,fade函數結束。
大於十分之一秒后,step函數被調用。它把fade函數的level變量轉化為10位字符。接着,它修改fade函數得到的節點的背景顏色。然后查看fade函數的level變量。如果背景色尚未變白色,那么它增大fade函數的level變量,接着用setTimeout預定它自己再次運行。
step函數很快再次被調用。但這次,fade函數的level變量值變成2。fade函數在之前已經返回了,但只要fade的內部函數需要,它的變量就會持續保留。
例2:
點擊事件里面用到外層的局部變量,有了閉包在數據的傳遞上更為靈活。
!function(){ var localData="localData here"; document.addEventListener('click', function(){ console.log(localData); }); }();
異步請求,用$.ajax()方法,在success回調中,用到外層的這些變量。在前端編程中,經常直接或間接,有意或無意用到閉包。
!function(){ var localData="localData here"; var url="http://www.baidu.com"; $.ajax({ url:url, success:function(){ //do sth console.log(localData); } }); }();
4、常見錯誤—循環閉包
閉包作用域鏈的機制引出的一個問題:閉包只能取得包含函數中任何變量的最后一個值。別忘了閉包所保存的是整個變量對象,而不是某個特殊的變量。
例1:
createFunctions()函數返回一個函數數組,表面看每個函數都返回自己的索引值。實際上,每個函數都返回10。
因為每個函數的作用域鏈中都保存着createFunctions()函數的活動對象,所以它們引用的都是同一個變量i。但createFunctions()函數返回后,變量i的值是10,此時每個函數都引用這保存變量i的同一個變量對象,所以每個函數內部i的值都是10。
正確方法:通過創建另一個你們函數強制讓閉包的行為符合預期。
function createFunctions(){ var result=new Array(); for(var i=0;i<10;i++){ result[i]=function(num){ return function(){ return num; } }(i); } return result; }
例2:
期望結果:點擊aaa彈出1,點擊bbb彈出2,點擊ccc彈出3。
<script> document.body.innerHTML="<div id=div1>aaa</div>"+"<div id=div2>bbb</div><div id=div3>ccc</div>"; for(var i=1;i<4;i++){ document.getElementById('div'+i).addEventListener('click',function(){ alert(i);//all are 4!!! }); } </script>
這段代碼執行后無論點擊哪個,彈出的永遠是4。
因為事件處理器函數綁定了變量i本身,而不是函數在構造時的變量i的值。
addEventListener里面是個回調函數, 當點擊的時候,這個回調函數才會動態的拿到i的值,在整個初始化完成之后i的值就已經是4了。
正確做法:
在每次循環的時候用一個立即執行的匿名函數包裝起來,每次循環的時候把i的值傳到匿名函數里面,在匿名函數里面再去引用i。這樣的話,在每次點擊alert的函數i會取自每一個閉包環境下的i,這個i來源於每次循環時的賦值i,這樣的話才能實現點擊彈出1,2,3的次序。
document.body.innerHTML="<div id=div1>aaa</div>"+"<div id=div2>bbb</div><div id=div3>ccc</div>"; for(var i=1;i<4;i++){ !function(i){ document.getElementById('div'+i).addEventListener('click',function(){ alert(i);//right }); }(i); }
5、閉包和this對象[update20170322]
在閉包中使用this對象也可能會導致一些問題。
this對象是在運行時基於函數的執行環境綁定的:
- 在全局函數中,this等於window
- 函數作為某個對象的方法調用時,this等於那個對象。
- 匿名函數的執行具有全局性,因此其this對象通常指向window。
有的時候,由於編寫閉包的方式不同,匿名函數的this指向window這一點可能不會那么明顯。
var name="The Window"; var object={ name:"My Object", getNameFunc:function(){ 'use strict'; return function(){ return this.name; } } } console.log(object.getNameFunc()());//The Window (非嚴格模式)
object包含一個name屬性,還包含一個方法—getNameFunc(),返回一個匿名函數,而匿名函數又返回this.name。
由於getNameFunc()返回一個函數,因此調用object.getNameFunc()()就會立即調用它返回的函數,結果就是返回一個字符串。
這個例子返回的字符串是“The Window”,即全局name變量的值。為什么匿名函數沒有取得其包含作用域(或者外部作用域)的this對象呢?
每個函數在被調用時,其活動對象都會自動取得兩個特殊變量:this和arguments。內部函數在搜索這兩個變量時,只會搜索到內部函數自己的活動對象為止,可以看上面的原理圖,因此永遠不可能直接訪問外部函數中的這兩個變量。
可以把外部作用域的this對象保存在一個閉包能夠訪問到的變量里,就可以讓閉包訪問該對象了。
var name="The Window"; var object={ name:"My Object", getNameFunc:function(){ var that=this; return function(){ return that.name; } } } console.log(object.getNameFunc()());//My Object
在幾種特殊情況下,this的值可能會意外地改變。下面是代碼修改前不同調用方式下的結果。
var name="The Window"; var object={ name:"My Object", getName:function(){//getName()方法簡單地返回this.name的值 return this.name; } } //幾種不同調用object.getName()的方式 console.log(object.getName());//My Object console.log((object.getName)());//My Object console.log((object.getName=object.getName)());//The Window
- object.getName()普通調用
-
(object.getName)()調用getName()方法前先給它加上了括號。雖然加上了括號之后,就好像只是在引用一個函數,但this的值得到了維持,因為object.getName和(object.getName)的定義是相同的。
-
(object.getName=object.getName)()先執行一條賦值語句,然后再調用賦值后的結果。因為這個賦值表達式的值是函數本身,所以this的值不能得到維持,結果就返回了“The Window”。
6、閉包的好處—封裝
封裝再具體一點:
- 模仿塊級作用域
- 私有變量
(function(){})() 里面定義一些想讓外部無法直接獲取的變量_userId,_typeId,最后通過window.export=export把最終想輸出的對象輸出出去。
<script> (function(){ var _userId=23492; var _typeId='item'; var myExport={}; function converter(userId){ return +userId; } myExport.getUserId=function(){ return converter(_userId); } myExport.getTypeId=function(){ return _typeId; } window.myExport=myExport; })(); console.log(myExport.getUserId()); //23492 console.log(myExport.getTypeId()); //item console.log(myExport._userId);//undefined console.log(myExport._typeId);//undefined console.log(myExport.converter);//undefined </script>
對應外部使用export對象上的getUserId()方法的人來說,只能通過export上提供的方法來間接訪問到具體的函數里面的變量,利用了閉包的特性,getUserId在函數執行完了后仍然能訪問到里面的自由變量。
在函數外面無法通過myExport._userId直接訪問變量,也沒法去改寫變量。
二、作用域
1、全局\函數\eval作用域
比較簡單。有時候也經常引起誤解。有哪幾種作用域:全局、函數和eval作用域。
2、作用域鏈
閉包outer1里可以訪問到自由變量local2也可以訪問到global3。
function outer2(){ var local2=1; function outer1(){ var local1=1; //可以訪問到 local1,local2 or global3 console.log(local1+','+local2+','+global3); } outer1(); } var global3=1; outer2();//1,1,1
3、利用函數作用域封裝
如果沒有一些模塊化的工具的話,經常看到很多類庫或者代碼最外層,去寫一個匿名函數如下:
(function(){ //do sth here var a,b; })();
或者
!function(){ //do sth here var a,b; }();
或者
+function(){ //do sth here var a,b; }();
好處:把函數內部的變量變成函數的局部變量,而不是全局變量,防止大量的全局變量和其他代碼或者類庫沖突。
用!或者+目的是把函數變成函數表達式而不是函數聲明。如果省略掉!,把一個完整的語句以function開頭的話,會被理解為函數聲明,會被前置處理掉,最后留下一對括號或者函數聲明省略了名字的話都會報語法錯誤。
三、ES3執行上下文(可選)【update20170321】
執行環境(execution context)是JavaScript中最為重要的一個概念。執行環境定義了變量或函數有權訪問的其他數據,決定了它們各自的行為。每個執行環境都有一個與之關聯的變量對象(variable object),執行環境中定義的所有變量和函數都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象,但解析器在處理數據時會在后台使用它。
每一次函數調用的時候,都有一套執行環境(execution context)。
某個執行環境中的所有代碼執行完畢后,該環境被銷毀,保存在其中的所有變量和函數定義也會隨之銷毀(全局執行環境直到應用程序退出—例如關閉網頁或瀏覽器—時才會被銷毀)。
抽象概念:執行上下文,變量對象
1、執行上下文
類似一個棧的概念。
函數調用1萬次就會有1萬個Execution context執行上下文。
每個函數都有自己的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行之后,棧將其環境彈出,把控制權返回給之前的執行環境。ECMAScript程序中的執行流正是由這個方便的機制控制着。
console.log('EC0'); function funcEC1(){ console.log('EC1'); var funcEC2=function(){ console.log('EC2'); var funcEC3=function(){ console.log('EC3'); } funcEC3(); } funcEC2(); } funcEC1(); //EC0 EC1 EC2 EC3
控制權從EC0到EC1到EC2到EC3,EC3執行完后控制權退回到EC2,EC2執行完之后控制權退回到EC1,EC1執行完后退回到EC0
2、變量對象
JavaScript解釋器如何找到我們定義的函數和變量?
需要引入一個抽象名詞:變量對象。
變量對象(Variable Object,縮寫為VO)是一個抽象概念中的“對象”,它用於存儲執行上下文中的:1、變量2、函數聲明3、函數參數。
即
例子:比如有一段javaScript代碼
var a=10; function test(x){ var b=20; } test(30);
全局作用域下的VO等於window,等於this。
當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈(scope chain)。作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數有序訪問。
作用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。如果這個環境是函數,則將其活動對象(activation object)作為變量對象。活動對象在最開始只包含一個變量,即arguments對象(這個對象在全局環境中是不存在的)。作用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境;全局執行環境的變量對象始終都是作用域鏈中的最后一個對象。
3、全局執行上下文(瀏覽器)
全局執行環境(執行上下文)是最外圍的一個執行環境。根據ECMAScript實現所在的宿主環境不同,表示執行環境的對象也不一樣。在web瀏覽器中,全局執行環境是window對象。因此所有全局變量和函數都是作為window對象的屬性和方法創建的。
在JavaScript第一行就可以調用Math,String,isNaN等方法,在瀏覽器里也可以拿到window,為什么?
因為在全局作用域下,背后就有一個變量對象VO(globalContext)===[[global]];
在第一行代碼執行之前,瀏覽器js引擎會把一些全局的東西初始化到VO里面,比如[[global]]里面有Math方法,String對象,isNaN函數,等,也會有一個window,這個window會指向它這個全局對象本身。
VO對象是一個標准抽象的概念,對應javascript語言本身,是不可見的,沒辦法直接訪問到,
比如函數對象的VO是沒任何辦法拿到的;但是在瀏覽器里面有一個全局的window會指向它自己,所以在控制台里用window.window.window.window...可以一直嵌套下去可以證明這是個無限循環。
String(10)背后就是會訪問對應的VO對象,也就是[[global]]對象,拿到[[global]]對象的屬性String。
4、函數中的激活對象
函數稍微特殊一點,函數中還有一個概念叫激活對象。
函數在執行的時候會把arguments放在AO激活對象中。
初始化auguments之后呢,這個AO對象又會被叫做VO對象。
和全局的VO一樣,進行其他一些初始化,比如說初始化函數的形參,初始化變量的聲明,或者是函數的聲明。
4.1、變量初始化階段
目的主要是理解一點:為什么函數和變量的聲明會被前置?為什么匿名函數表達式的名字不可以在外面調用?
對於函數對象的VO來說,分為2個階段,第一個階段為變量初始化階段。
上面說了全局作用域下VO變量初始化會把Math,String等一些全局的東西放進去。在第二個階段才能更好的執行代碼。
函數的變量初始化階段會把arguments的初始化,會把變量聲明和函數聲明放進去。
具體操作:
VO按照如下順序填充: 1、函數參數(若未傳入,初始化該參數值為undefined) 2、函數聲明(若發生命名沖突,會覆蓋) 3、變量聲明(初始化變量值為undefined,若發生命名沖突,會忽略)
注意一點:函數表達式不會影響VO
比如上面,var e=function _e(){};中_e是不會放到AO中的。這也是為什么在外面不能通過_e拿到函數對象。
函數變量初始化的階段把函數聲明d放到了AO中,這也就解釋了為什么函數聲明會被前置。
函數聲明沖突會覆蓋,變量什么沖突會忽略。
4.2代碼執行階段
這段代碼:
第一階段:變量初始化階段AO如下
第二階段:代碼執行階段
得到
5、測試一下
<script> console.log(x); //function x(){} var x=10; console.log(x);//10 x=20; function x(){} console.log(x); //20 if(true){ var a=1; }else{ var b=true; } console.log(a); //1 console.log(b); //undefined </script>
四、作用域鏈和執行環境的綜合例子
當函數第一次被調用時,會創建一個執行環境及相應的作用域鏈,並把作用域鏈賦值給一個特殊的內部屬性(即[[Scope]])。
例子:定義了compare()函數,並在全局作用域中調用它。
function compare(value1,value2){ if(value1<value2){ return -1; }else if(value1>value2){ return 1; }else{ return 0; } } var result=compare(5,10);
作用域鏈本質是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
第一次調用compare(),會創建一個包含this,arguments,value1和value2的活動對象。全局執行環境的變量對象(包含this,result,compare)在compare()執行環境的作用域鏈中則處於第二位。
全局環境的變量對象始終存在,而像compare()函數這樣的局部環境的變量對象,則只在函數執行的過程中存在。
在創建compare()函數時,會創建一個預先包含全局變量對象的作用域鏈,這個作用域鏈被保存在內部的[[Scope]]屬性中。當調用compare()函數時,會為函數創建一執行環境,然后通過賦值函數的[[Scope]]屬性中的對象構建起執行環境的作用域鏈。
此后,又有一個活動對象(在此作為變量對象使用)被創建並推入執行環境作用域鏈的前端。對於這個例子中的compare()函數的執行環境而言,其作用域鏈中包含兩個變量對象:本地活動對象和全局變量對象。
本文作者starof,因知識本身在變化,作者也在不斷學習成長,文章內容也不定時更新,為避免誤導讀者,方便追根溯源,請諸位轉載注明出處:http://www.cnblogs.com/starof/p/6400261.html有問題歡迎與我討論,共同進步。