1. 程式人生 > >JavaScript閉包?看完這幾個例子你就完全搞懂了!

JavaScript閉包?看完這幾個例子你就完全搞懂了!

理解閉包,首先要知道作用域鏈是個什麼東西。

1.什麼是作用域鏈

在 JavaScript 的最頂層程式碼中(也就是不包含在任何函式定義內的程式碼),作用域鏈有一個全域性物件組成。在不包含巢狀的函式體內,作用域鏈上有兩個物件,第一個是定義函式引數和區域性變數的物件,第二個是全域性物件。在一個巢狀的函式體內,作用域鏈上至少有三個物件。

當定義一個函式時,它實際上儲存一個作用域鏈。

當呼叫這個函式時,它建立一個新的物件來儲存它的區域性變數,並將這個物件新增至儲存的那個作用域鏈上,同時建立一個新的更長的表示函式呼叫作用域的“鏈”。

對於巢狀函式來說,每次呼叫外部函式時,內部函式又會重新定義一次,因為每次呼叫外部函式的時候,作用域鏈都是不同的。

內部函式在每次定義的時候都有微妙的差別,即每次呼叫外部函式的時候,內部函式的程式碼都是相同的,而關聯這段程式碼的作用域鏈卻是不相同的

2.什麼是閉包

函式物件可以通過作用域鏈相互關聯起來,函式體內部的變數都可以儲存在函式作用域內,這種特性在電腦科學文獻中稱為“閉包”——是指函式變數可以被隱藏於作用域之內,因此看起來是函式將變數“包裹”起來了

例1

var scope = 'global scope';
function checkscope() {
    var scope = "local scope";
    function f() {
        console.log(scope);
    }
    return f();
}
checkscope();

執行函式之後列印

checkscope()函式聲明瞭一個區域性變數,並定義一個函式 f(), 函式 f() 返回了這個變數的值,最後將函式 f() 的執行結果返回。這個結果是顯而易見的返回函式內區域性變數,然後我們改動一下函式再看看。。。

var scope = 'global scope';
function checkscope() {
    var scope = "local scope";
    function f() {
         console.log(scope);
    }
    return f;
}
checkscope()();

上面例子也可以改為這樣

var scope = 'global scope';
function checkscope() {
    var scope = "local scope";
    function f() {
         console.log(scope);
    }
    return f;
}
var f = checkscope();
f();

結果並沒有改變,這是什麼原因呢?

會看一下作用域的基本規則:javascript 函式執行用到了作用域鏈,這個作用域鏈是函式定義的時候建立的。巢狀的函式 f() 定義在這個作用域裡,其中的變數scope一定是區域性變數,不管在何時何地執行函式f(),這種繫結在執行f()時依然有效。簡言之,閉包的這個特性就是:它們可以捕捉到區域性變數(和引數),並一直儲存下來,看起來像這些變數繫結到了在其中定義的外部函式。

3.利用閉包

例2

     function counter() {
            var n = 0;
            return {
                count: function() {
                    return n++;
                },
                reset: function() {
                    n = 0;
                }
            };
        }
        var c = counter(), d = counter();
        console.log(c.count());
        console.log(d.count());
        c.reset();
        console.log(c.count());
        console.log(d.count());

每次呼叫couter()都會建立一個新的作用域和一個新的私有變數。因此,變數c重置了n=0,所以第二次c,d執行count()的結果不一樣。

例3

      function counter(n) {
            return {
                get count() {return n++;},
                set count(m) {
                    if(m>=n) {
                        n = m;
                    }else {
                        throw Error("count can only be set to a larger value")
                    }
                }
            }
        }
        var c = counter(1000);
        console.log(c.count);
        console.log(c.count);
        console.log(c.count = 2000);
        console.log(c.count);
        console.log(c.count = 2000);

這個版本的counter()函式並未申明區域性變數,而只是使用引數n來儲存私有狀態,屬性儲存器方法可以訪問n,這樣的話,呼叫counter()的函式就可以指定私有變數的初始值了。

例4(利用閉包實現的私有屬性儲存器方法)

 function addPrivateProperty(o, name, predicate) {
            var value;
            o["get" + name] = function() { return value; };
            o["set" + name] = function(v) {
                if(predicate && !predicate(v)){
                    throw Error("set" + name + ": invalid value "+ v);
                }else {
                    value = v;
                }
            }
        }
        var o = {};
        addPrivateProperty(o, "Name", function(x) { return typeof x == "string"});
        o.setName("Frank");
        console.log(o.getName());
        o.setName(o);  //將丟擲異常

這裡除了呼叫o物件的setName()方法,且只能傳入字串對變數value進行賦值,同樣只能通過o物件的geiName()方法才能訪問到變數value的值。

4.閉包的危害

例5

        function constfunc(v) {return function() {return v;};}
        var funcs = [];
        for(var i = 0 ; i< 10; i++) {
            funcs[i] = constfunc(i);
        }
        console.log(funcs[5]());

        function constfuncs() {
            var funcs = [];
            for(var i = 0; i < 10; i++){
                funcs[i] = function() {
                    return i;
                }
            }
            return funcs;
        }
        var funcs2 = constfuncs();
        console.log(funcs2[5]());

第一個函式顯然使我們需要的結果,可以第二次為什麼會呼叫結果顯示為10呢?其實陣列funcs2儲存的所有函式執行結果都是10。

第二個函式中建立了是個閉包,並將它們儲存在一個數組中,這些閉包都是在同一個函式呼叫中定義的,因此它們可以共享變數i。當constfuncs()返回時,變數i的值是10,所有的閉包都共享這一個值,因此陣列中的函式的返回值都是同一個值。

故而我們需要像第一個函式那樣,不是直接將變數i返回而是通過傳入引數的方法,實現變數私有化的方法來實現想要的結果。