Javascript This.作用域.閉包

JavaScript是個有點神奇的語言,不過它的一些獨有的特性往往讓我們初學者感到費解。ThisJavascript語言的一個關鍵詞。不過它到底是指什麼呢?很多人都會認爲this指的是當前對象。當然,這樣理解是沒錯的,但是在有些情況下仍然會有些問題。在此,我搜集了一些資料,重新學習並整理一下,希望能借此來更好的理解this在JS中的工作方式和使用方法。

  1.  

    var test = function(){
        alert(this);
    }
    
    test();
    new test();

運行以上代碼,你會發現test()和new test()的運行結果是不一樣的,test()指向的是Windows對象而new test()纔是指向test對象,爲什麼會有兩種不同的運行結果?其實這裏就涉及到一個變量作用域的問題,而變量作用域同時又牽涉到閉包(Closure)這個JS特性了,正因爲閉包的存在,理解變量作用域就顯得非常重要。

關於變量作用域

接着先來介紹一下所謂的變量作用域,概念非常簡單,每個變量都有自己的作用域,即變量在這麼一個區域中可以被識別,而出了此區域就沒有任何作用了。

作用域就兩種:全局作用域和局部作用域。
全局變量在JavaScript中處處都有定義,它貫穿在一個全局對象中,因此可以在任何地方使用。而局部變量則只在一個函數中有定義。其中要注意到的一點就是:JavaScript無塊級作用域,這有別於C++和Java。因此任何變量在定義它的整個函數體中都能被識別。

  1.  

    var a = 123;
    function fun1() {
        alert(a);
    }
    
    fun1();//123

很簡單,函數內部可以直接讀取全局變量;

  1.  

    function fun2() {
        var a =123;
    }
    
    alert(a);//error

     

在函數外部當然無法讀取函數內的局部變量。

var s = "oo";
function fun3() {
    alert(s);// undefined,他認爲此時的s並沒有被初始化
    var s ="xx";
    alert(s);// xx因爲javascript無塊級作用域,s在此被初始化,整個函數中都有定義
}

fun3();

以上代碼其實就相當於

  1.  

    function fun3(){
        var s;
        alert(s);
        s = "xx";
        alert(s);
    }
    
    fun3();

     

另外大家要注意的一點就是,在函數內部聲明變量的時候,如果不用var命令,這樣其實就相當聲明瞭一個全局變量。

關於閉包

瞭解過變量作用域之後,對於理解閉包就有很大的幫助了。當我們需要得到函數內的局部變量的時候,就需要在函數的內部再定義一個函數。

  1.  

    function fun1(){
        var a = 123;
    
        function fun2(){
            alert(a);//123
        }
    }

fun2可以訪問fun1所有的局部變量,因此我們只要把fun2作爲返回值,就可以在fun1外部讀取到它的內部變量了。

  1.  

    function fun1(){
        var a = 123;
    
        function fun2(){
            alert(a);//123
        }
    
        return fun2;
    }
    
    var result = fun1();
    result();//因爲調用fun1返回的是一個函數fun2,所以調用result是要加上()

因此,有了閉包將聯繫函數內外聯繫起來,我們就可以從函數外讀取到函數內部的變量了,另外由於JavaScript作用域的機制,閉包只能讀取到包含函數中任何變量的最後一個值。
這裏引用一個例子:

  1.  

    function createFunctions() {
        var result = new Array();
        for(var i = 0; i < 10; i++) {
            result[i] = function(){
                return i;
            };
        }
    
        return result;
    }
    
    var funcs = createFunctions();
    
    for(var i = 0; i < funcs.length; i++) {
        alert(funcs[i]());//每次都顯示10
    }

因爲每個函數作用域中都保存着createFunctions()函數的活動對象,所以它們引用的都是同一個變量i,當createFunctions()函數返回後,變量i的值是10,因此每個函數都引用着保存變量i的同一個變量對象,所以每個函數內部i的值都是10。我們要稍微改寫一下函數,讓它符合我們的預期要求。

  1.  

    function createFunctions() {
        var result = new Array();
        for( var i = 0; i < 10; i++) {
            result[i] = function(num){
                return num;
            }(i);
        }
    
        return result;
    }
    
    var funcs = createFunctions();
    for( var i = 0; i < funcs.length; i++) {
        alert(funcs[i]);//顯式0,1,2,3,4,5,6,7,8,9
    }

這裏,我們通過定義一個匿名函數,並立即執行該匿名函數的結果賦給數組,這裏的匿名函數只有一個參數num,也就是最終要返回的值。在調用每個匿名函數時,我們傳入了變量i,通過變量i的將當前值賦給參數num,這樣就獲得我們所要的結果。

清楚了作用域和閉包之後,對於this就能很快理解了。先來看下面這段代碼:

  1. function test1() {
        this.x = 123;
        alert(this.x);
    }
    
    test1();//123

當前的this就是全局對象,稍作變化,效果一樣。

  1. var x = 1;//x表示的是window.x
    function test() {
        this.x= 0;
    }
    
    alert(x);//1
    test();//此時的this就是window,所以執行過test方法之後,this.x= 0 就表示window.x= 0;他修改了原來的x=1
    
    //所以最終的結果爲0
    alert(x);//0

再看如下代碼:

  1. var oo = new Object;
    oo.x = 1;
    function test2() {
        alert(this.x);
    }
    
    oo.fun = test2;
    oo.fun();//1,因爲是oo對象調用的fun方法,所以此時的this.x表示的就是oo.x

這裏的this就指當前調用該方法的對象。
對於開頭這段代碼來說,test()函數是在全局作用域下的(在這裏其實就是window對象),所以this的值是當前的window對象。而通過 new test()其實是作爲構造函數來調用的,就是通過這個函數來生成一個新的對象,所以這裏的this就指的是這個新對象。

  1. var xx = 2;
    function test3() {
        this.xx= 1;
    }
    
    var oo = new test3();
    alert(oo.xx);//1 oo對象引用了test方法,此時test方法中的this就表示的是oo對象
    alert(xx);//2

以上例子可證明this並不是全局對象,因爲xx的值並沒有改變。最後在網上看到兩段例子,對於理解this和閉包有很大的幫助,在此引用過來。

代碼一

var name = "The Window";
//此時的name的作用域爲window,所以等價於window.name= "The Window";

var object = {
    name:"My Object",
    getNameFunc:function(){
        return function(){
            return this.name;
            //此時的this在匿名函數中,這個匿名函數沒有調用的對象,因此它的作用域爲window,
            //此時的this.name等價於window.name
            //即等同於第一行的name
        };
    }
};

alert(object.getNameFunc()());//返回的結果爲The Window

代碼二

var name = "The Window";
var object = {
    name:"My Object",
    getNameFunc:function(){
        var that = this;
        //此時的this屬於getNameFunc函數,而getNameFunc函數只能被object調用,
        //即getNameFunc的調用者是object,所以此時的this表示object對象,
        return function(){
            return that.name;
            //由於getNameFunc中的this表示object對象,所以that也表示object對象,
            //所以that.name就等價於object.name 就是"My Object"
        };
    }
};

alert(object.getNameFunc()());//輸出結果爲My Object

三、閉包的概念

各種專業文獻上的“閉包”(closure)定義非常抽象,很難看懂。我的理解是,閉包就是能夠讀取其他函數內部變量的函數。由於在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成“定義在一個函數內部的函數”。所以,在本質上,閉包就是將函數內部和函數外部連接起來的一座橋樑。

四、閉包的用途

閉包可以用在許多地方。它的最大用處有兩個,個是前面提到的可以讀取函數內部的變量,另一個就是讓這些變量的值始終保持在內存中

怎麼來理解這句話呢?請看下面的代碼。
Js代碼

function f1(){
    var n = 999;
    nAdd = function(){
        n += 1
    }

    function f2(){ 
        alert(n);    
    }

    return f2;
}

var result = f1();
result(); // 999
nAdd();
result(); // 1000

 

在這段代碼中,result實際上就是閉包f2函數。它一共運行了兩次,第一次的值是999,第二次的值是1000。這證明了,函數f1中的局部變量n一直保存在內存中,並沒有在f1調用後被自動清除。

爲什麼會這樣呢?原因就在於f1是f2的父函數,而f2被賦給了一個全局變量,這導致f2始終在內存中,而f2的存在依賴於f1,因此f1也始終在內存中,不會在調用結束後,被垃圾回收機制(garbage collection)回收。

這段代碼中另一個值得注意的地方,就是“nAdd=function(){n+=1}”這一行,首先在nAdd前面沒有使用var關鍵字,因此 nAdd是一個全局變量,而不是局部變量。其次,nAdd的值是一個匿名函數(anonymous function),而這個匿名函數本身也是一個閉包,所以nAdd相當於是一個setter,可以在函數外部對函數內部的局部變量進行操作。

五、使用閉包的注意點

1)由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變量全部刪除。

2)閉包會在父函數外部,改變父函數內部變量的值。所以,如果你把父函數當作對象(object)使用,把閉包當作它的公用方法(Public Method),把內部變量當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數內部變量的值。

Js代碼

 

function outerFun(){ 
    var a = 0; 
    function innerFun() {  
        a++;  
        alert(a); 
    } 
	
    return innerFun;  //注意這裏
}

var obj = outerFun();
obj();  //結果爲1
obj();  //結果爲2
var obj2 = outerFun();
obj2();  //結果爲1
obj2();  //結果爲2

 

什麼是閉包:

當內部函數 在定義它的作用域 的外部 被引用時,就創建了該內部函數的閉包 ,如果內部函數引用了位於外部函數的變量,當外部函數調用完畢後,這些變量在內存不會被 釋放,因爲閉包需要它們.

再來看一個例子

Js代碼

 

function outerFun(){ 
    var a =0; 
    alert(a);  
}
var a = 4;
outerFun();
alert(a);

 

結果是 0,4 .  因爲在函數內部使用了var關鍵字 維護a的作用域在outFun()內部.

再看下面的代碼:

Js代碼 

function outerFun(){
    //沒有var  
    a = 0; 
    alert(a);  
}

var a = 4;
outerFun();
alert(a);

結果爲 0,0 真是奇怪,爲什麼呢?

 

作用域鏈是描述一種路徑的術語,沿着該路徑可以確定變量的值 .當執行a=0時,因爲沒有使用var關鍵字,因此賦值操作會沿着作用域鏈到var a=4;  並改變其值.

 

一、什麼是閉包?

官方”的解釋是:閉包是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。
相信很少有人能直接看懂這句話,因爲他描述的太學術。其實這句話通俗的來說就是:JavaScript中所有的function都是一個閉包。不過一般來說,嵌套的function所產生的閉包更爲強大,也是大部分時候我們所謂的“閉包”。看下面這段代碼:

 

function a() { 
    var i = 0; 
    function b() {
        alert(++i); 
    } 

    return b;
}

var c = a();
c();

這段代碼有兩個特點:

 

1、函數b嵌套在函數a內部;

 

2、函數a返回函數b。

引用關係如圖:

  這樣在執行完var c=a()後,變量c實際上是指向了函數b,再執行c()後就會彈出一個窗口顯示i的值(第一次爲1)。這段代碼其實就創建了一個閉包,爲什麼?因爲函數a外的變量c引用了函數a內的函數b,就是說:

  當函數a的內部函數b被函數a外的一個變量引用的時候,就創建了一個閉包。

  讓我們說的更透徹一些。所謂“閉包”,就是在構造函數體內定義另外的函數作爲目標對象的方法函數,而這個對象的方法函數反過來引用外層函數體中的臨時變量。這使得只要目標 對象在生存期內始終能保持其方法,就能間接保持原構造函數體當時用到的臨時變量值。儘管最開始的構造函數調用已經結束,臨時變量的名稱也都消失了,但在目 標對象的方法內卻始終能引用到該變量的值,而且該值只能通這種方法來訪問。即使再次調用相同的構造函數,但只會生成新對象和方法,新的臨時變量只是對應新 的值,和上次那次調用的是各自獨立的。

二、閉包有什麼作用?

  簡而言之,閉包的作用就是在a執行完並返回後,閉包使得Javascript的垃圾回收機制GC不會收回a所佔用的資源,因爲a的內部函數b的執行需要依賴a中的變量。這是對閉包作用的非常直白的描述,不專業也不嚴謹,但大概意思就是這樣,理解閉包需要循序漸進的過程。

在上面的例子中,由於閉包的存在使得函數a返回後,a中的i始終存在,這樣每次執行c(),i都是自加1後alert出i的值。

  那 麼我們來想象另一種情況,如果a返回的不是函數b,情況就完全不同了。因爲a執行完後,b沒有被返回給a的外界,只是被a所引用,而此時a也只會被b引 用,因此函數a和b互相引用但又不被外界打擾(被外界引用),函數a和b就會被GC回收。(關於Javascript的垃圾回收機制將在後面詳細介紹)

三、閉包內的微觀世界

  如果要更加深入的瞭解閉包以及函數a和嵌套函數b的關係,我們需要引入另外幾個概念:函數的執行環境(excution context)、活動對象(call object)、作用域(scope)、作用域鏈(scope chain)。以函數a從定義到執行的過程爲例闡述這幾個概念。

  1. 當定義函數a的時候,js解釋器會將函數a的作用域鏈(scope chain)設置爲定義a時a所在的“環境”,如果a是一個全局函數,則scope chain中只有window對象。
  2. 當執行函數a的時候,a會進入相應的執行環境(excution context)。
  3. 在創建執行環境的過程中,首先會爲a添加一個scope屬性,即a的作用域,其值就爲第1步中的scope chain。即a.scope=a的作用域鏈。
  4. 然後執行環境會創建一個活動對象(call object)。活動對象也是一個擁有屬性的對象,但它不具有原型而且不能通過JavaScript代碼直接訪問。創建完活動對象後,把活動對象添加到a的作用域鏈的最頂端。此時a的作用域鏈包含了兩個對象:a的活動對象和window對象。
  5. 下一步是在活動對象上添加一個arguments屬性,它保存着調用函數a時所傳遞的參數。
  6. 最後把所有函數a的形參和內部的函數b的引用也添加到a的活動對象上。在這一步中,完成了函數b的的定義,因此如同第3步,函數b的作用域鏈被設置爲b所被定義的環境,即a的作用域。

到此,整個函數a從定義到執行的步驟就完成了。此時a返回函數b的引用給c,又函數b的作用域鏈包含了對函數a的活動對象的引用,也就是說b可以訪問到a中定義的所有變量和函數。函數b被c引用,函數b又依賴函數a,因此函數a在返回後不會被GC回收。

當函數b執行的時候亦會像以上步驟一樣。因此,執行時b的作用域鏈包含了3個對象:b的活動對象、a的活動對象和window對象,如下圖所示:

如圖所示,當在函數b中訪問一個變量的時候,搜索順序是:

  1. 先搜索自身的活動對象,如果存在則返回,如果不存在將繼續搜索函數a的活動對象,依次查找,直到找到爲止。
  2. 如果函數b存在prototype原型對象,則在查找完自身的活動對象後先查找自身的原型對象,再繼續查找。這就是Javascript中的變量查找機制。
  3. 如果整個作用域鏈上都無法找到,則返回undefined。

小結,本段中提到了兩個重要的詞語:函數的定義與執行。文中提到函數的作用域是在定義函數時候就已經確定,而不是在執行的時候確定(參看步驟1和3)。用一段代碼來說明這個問題:

function f(x) { 
  var g = function () { return x; }
  return g;
}
var h = f(1);
alert(h()); 

這段代碼中變量h指向了f中的那個匿名函數(由g返回)。

  • 假設函數h的作用域是在執行alert(h())確定的,那麼此時h的作用域鏈是:h的活動對象->alert的活動對象->window對象。
  • 假設函數h的作用域是在定義時確定的,就是說h指向的那個匿名函數在定義的時候就已經確定了作用域。那麼在執行的時候,h的作用域鏈爲:h的活動對象->f的活動對象->window對象。

如果第一種假設成立,那輸出值就是undefined;如果第二種假設成立,輸出值則爲1。

運行結果證明了第2個假設是正確的,說明函數的作用域確實是在定義這個函數的時候就已經確定了。
 

四、閉包的應用場景
保護函數內的變量安全。以最開始的例子爲例,函數a中i只有函數b才能訪問,而無法通過其他途徑訪問到,因此保護了i的安全性。

  1. 在內存中維持一個變量。依然如前例,由於閉包,函數a中i的一直存在於內存中,因此每次執行c(),都會給i自加1。
  2. 通過保護變量的安全實現JS私有屬性和私有方法(不能被外部訪問)
    私有屬性和方法在Constructor外是無法被訪問的

    function Constructor(...) {  
      var that = this;  
      var membername = value; 
      function membername(...) {...}
    }

以上3點是閉包最基本的應用場景,很多經典案例都源於此。
 

五、Javascript的垃圾回收機制

在Javascript中,如果一個對象不再被引用,那麼這個對象就會被GC回收。如果兩個對象互相引用,而不再被第3者所引用,那麼這兩個互相引用的對象也會被回收。因爲函數a被b引用,b又被a外的c引用,這就是爲什麼函數a執行後不會被回收的原因。

 

六、結語

理解JavaScript的閉包是邁向高級JS程序員的必經之路,理解了其解釋和運行機制才能寫出更爲安全和優雅的代碼。

 

 

 

 

 

閉包是一個比較抽象的概念,尤其是對js新手來說.書上的解釋實在是比較晦澀,對我來說也是一樣.

  但是他也是js能力提升中無法繞過的一環,幾乎每次面試必問的問題,因爲在回答的時候.你的答案的深度,對術語的理解以及js內部解釋器的運作方式的描述,都是可以看出你js實際水平的.即使你沒答對,也能讓考官對你的水平有個評估.那麼我先來說說我對js中的閉包的理解.

  閉包是很多語言都具備的特性,在js中,閉包主要涉及到js的幾個其他的特性:作用域鏈,垃圾(內存)回收機制,函數嵌套,等等.

  在理解閉包以前.最好能先理解一下作用域鏈的含義,簡單來說,作用域鏈就是函數在定義的時候創建的,用於尋找使用到的變量的值的一個索引,而他內部的規則是,把函數自身的本地變量放在最前面,把自身的父級函數中的變量放在其次,把再高一級函數中的變量放在更後面,以此類推直至全局對象爲止.當函數中需要查詢一個變量的值的時候,js解釋器會去作用域鏈去查找,從最前面的本地變量中先找,如果沒有找到對應的變量,則到下一級的鏈上找,一旦找到了變量,則不再繼續.如果找到最後也沒找到需要的變量,則解釋器返回undefined.

  瞭解了作用域鏈,我們再來看看js的內存回收機制,一般來說,一個函數在執行開始的時候,會給其中定義的變量劃分內存空間保存,以備後面的語句所用,等到函數執行完畢返回了,這些變量就被認爲是無用的了.對應的內存空間也就被回收了.下次再執行此函數的時候,所有的變量又回到最初的狀態,重新賦值使用.但是如果這個函數內部又嵌套了另一個函數,而這個函數是有可能在外部被調用到的.並且這個內部函數又使用了外部函數的某些變量的話.這種內存回收機制就會出現問題.如果在外部函數返回後,又直接調用了內部函數,那麼內部函數就無法讀取到他所需要的外部函數中變量的值了.所以js解釋器在遇到函數定義的時候,會自動把函數和他可能使用的變量(包括本地變量和父級和祖先級函數的變量(自由變量))一起保存起來.也就是構建一個閉包,這些變量將不會被內存回收器所回收,只有當內部的函數不可能被調用以後(例如被刪除了,或者沒有了指針),纔會銷燬這個閉包,而沒有任何一個閉包引用的變量纔會被下一次內存回收啓動時所回收.

也就是說,有了閉包,嵌套的函數結構纔可以運作,這也是符合我們的預期的.然後,閉包還有一些特性,卻往往讓程序員覺得很難理解.

看看下面一段代碼.

var result=[];
function foo(){
    var i= 0;
    for (;i<3;i=i+1){
        result[i]=function(){
            alert(i)
        }
    }
};
foo();
result[0](); // 3
result[1](); // 3
result[2](); // 3

 

這段代碼中,程序員希望foo函數中的變量i被內部循環的函數使用,並且能分別獲得他們的索引,而實際上,只能獲得該變量最後保留的值,也就是說.閉包中所記錄的自由變量,只是對這個變量的一個引用,而非變量的值,當這個變量被改變了,閉包裏獲取到的變量值,也會被改變.

解決的方法之一,是讓內部函數在循環創建的時候立即執行,並且捕捉當前的索引值,然後記錄在自己的一個本地變量裏.然後利用返回函數的方法,重寫內部函數,讓下一次調用的時候,返回本地變量的值,改進後的代碼:

var result=[];
function foo(){
    var i= 0;
    for (;i<3;i=i+1){
        result[i]=(function(j){
            return function(){
                alert(j);
            };
        })(i);
    }
};
foo();
result[0](); // 0
result[1](); // 1
result[2](); // 2

在這裏我再解釋一下.這裏用到了另外2個技術,立即調用的匿名函數和返回函數.也是初學者比較難以理解的部分.
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章