javascript 函數和作用域(閉包、作用域)(七)


一、閉包

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、函數參數(若未傳入,初始化該參數值為undefined2、函數聲明(若發生命名沖突,會覆蓋)
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有問題歡迎與我討論,共同進步。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM