JavaScript中的閉包(Closure)


在上一篇介紹JavaScript中的this關鍵字的文章中我們提到了閉包這個概念。閉包是指有權訪問另一個函數作用域中的變量的函數。從函數對象中能夠對外部變量進行訪問(引用、更新),是構成閉包的條件之一。創建閉包的常見方式,就是在一個函數內部創建另一個函數。爲了理解閉包,先來看一下什麼是變量的生命週期。

變量的聲明週期,就是變量的壽命,相對於表示程序中變量可見範圍的作用域來說,生命週期這個概念指的是一個變量可以在多長的週期範圍內存在並能夠被訪問。看下面一個例子:

function extent() {
    var n = 0;
    return function() {
        n++;
        console.log("n=" + n);
    };
}
var returnFun = extent();
returnFun();  //n=1
returnFun();  //n=2

局部變量n是在extent函數中聲明的,這個從屬於外部作用域中的局部變量,被函數對象給封閉在裏面了。被封閉起來的變量的壽命,與封閉它的函數對象的生命週期相同,即閉包延長了局部變量的聲明週期。 閉包的概念比較抽象,來來回回反反覆覆就那麼幾句話,並不好理解,下面主要通過幾個例子來看一下如何使用閉包。

1. 在循環中使用閉包

在循環中使用閉包時,經常會出意想不到的代碼執行結果。下面給出五個代碼片段,目的都是想要返回一個函數數組,該數組中的每個函數的返回結果都是其在函數數組中的下標:

<!-- 代碼片段一 Begin -->
function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = function() {
            return i;  //閉包,該函數保有外部變量i的訪問權
        };
    }
    return result;
}
//調用
var result = createFunction();
result[0]();  //10
result[1]();  //10
... ... ...
result[9]();  //10
<!-- 代碼片段一 End -->

<!-- 代碼片段二 Begin -->
function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        (function(num) {
            result[i] = function() {  //這裏的result[i]可以替換爲result[num]
                return num;
            };
        })(i);  //和代碼片段三與代碼片段四的區別一樣,這裏的 })(i); 可以替換爲 }(i));
    }
    return result;
}
//調用
var result = createFunction();
result[0]();  //0
result[1]();  //1
... ... ...
result[9]();  //9
<!-- 代碼片段二 End -->

<!-- 代碼片段三 Begin -->
function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = (function(num) {  //這裏的result[i]可以替換爲result[num]
            return function() {
                return num;
            };
        })(i);
    }
    return result;
}
//調用
var result = createFunction();
result[0]();  //0
result[1]();  //1
... ... ...
result[9]();  //9
<!-- 代碼片段三 End -->

<!-- 代碼片段四 Begin -->
function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = (function(num) {  //這裏的result[i]可以替換爲result[num]
            return function() {
                return num;
            };
        }(i));  //與代碼片段三的區別是,(i)所在的位置。兩種方式效果相同。
    }
    return result;
}
//調用
var result = createFunction();
result[0]();  //0
result[1]();  //1
... ... ...
result[9]();  //9
<!-- 代碼片段四 End -->

<!-- 代碼片段五 Begin -->
function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = function(num) {  //這裏的result[i]可以替換爲result[num]
            return function() {
                return num;
            };
        }(i);  //與代碼片段三和代碼片段四的區別是,function(num)沒有被()包裹
    }
    return result;
}
//調用
var result = createFunction();
result[0]();  //0
result[1]();  //1
... ... ...
result[9]();  //9
<!-- 代碼片段五 End -->

通過代碼及其執行結果可以看到,代碼片段一與我們的目標不符,生成的函數數組中的每一個函數的返回值都爲10。這是因爲for循環在結束後,i的值爲10。因爲閉包的存在,i的聲明週期被延長,函數數組中的每一個函數都可以訪問createFunction函數中定義的的變量i,這些函數在運行時會返回i的值,因爲現在i = 10,因而出現了這種錯誤結果。

可以使用代碼片段二 ~ 代碼片段五實現需求,所有方法的原理其實相同,就是通過函數的立即執行將循環中的i保留下來。具體使用那種方式可以根據個人習慣來定,筆者更習慣代碼片段二和代碼片段三這種方式。

2. 結合例一再次理解一下閉包

<!-- HTML Begin-->
<pid="help">Helpful notes will appear here</p>
<p>E-mail:  <input type="text" id="email" name="email"></p>
<p>Name:    <input type="text" id="name" name="name"></p>
<p>Age:     <input type="text" id="age" name="age"></p>
<!-- HTML End-->

<!-- JavaScript通用代碼段 Begin -->
function showHelp(info) {
    document.getElementById('help').innerHTML = info;
}
//爲了之後調用
function closureFunc(info) {
    return function() {
        showHelp(info);
    };
}
function setupHelp() {
    var helpText = [
        {id: "email", info: "Please Input YourEmail Address"},
        {id: "name", info: "Please Input Your Name"},
        {id: "age", info: "Please Input Your Age"}
    ];
<!-- JavaScript通用代碼段 End -->
 
<!-- 代碼片段一 Begin -->
    for(var i = 0; i < helpText.length; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = function() {
            showHelp(item.info);
        }
    }
<!-- 代碼片段一 End -->
 
<!-- 代碼片段二 Begin -->
    for(var i = 0; i < helpText.length; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = closureFunc(item.info);
    }
<!-- 代碼片段二 End -->

<!-- 代碼片段三 Begin -->
    for(var i = 0; i < helpText.length; i++) {
        (function(num) {
            var item = helpText[num];
            document.getElementById(item.id).onfocus = function() {
                showHelp(item.info);
            };
        })(i);
    }
<!-- 代碼片段三 End -->

<!-- 代碼片段四 Begin -->
    for(var i = 0; i < helpText.length; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = (function(info) {
            return function() {
                showHelp(info);
            };
        })(item.info);
    }
<!-- 代碼片段四 End -->

<!-- 代碼片段五 Begin -->
    for(var i = 0; i < helpText.lenght; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = (function(info) {
            return function() {
                showHelp(info);
            };
        }(item.info));
    }
<!-- 代碼片段五 End -->

<!-- 代碼片段六 Begin -->
    for(var i = 0; i < helpText.length; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = function(info) {
            return function() {
                showHelp(info);
            };
        }(item.info);
    }
<!-- 代碼片段六 End -->

}
setupHelp();

通過代碼可以看到,代碼片段一與我們的目標不符,其餘代碼片段均可成功,原理與第一個例子完全相同,這裏不再贅述,有興趣的讀者可以嘗試一下代碼片段一與其他代碼在實現效果上的差別。

這裏注意一點,代碼片段六也能成功,但建議最好使用括號將其括起來,即推薦使用代碼片段四或代碼片段五的形式。如果直接在JavaScript中這樣寫:

function(info) {
    return function() {
        showHelp(info);
    };
}(item.info);

會導致錯誤,這是因爲JavaScript把function關鍵字當作一個函數聲明的開始,函數聲明後面不能跟圓括號直接執行,然而函數表達式後面可以跟圓括號直接執行。要想將函數聲明轉換爲函數表達式,只要像下面這樣給它加上一對圓括號:

(function(info){
    return function() {
        showHelp(info);
    };
})(item.info);

如果對函數聲明及函數表達式的概念不是很清楚,請參考這裏

3. 創建公有方法訪問私有變量

可以通過使用閉包模擬私有變量,實現和JavaBean類似的功能:

var Person = function(name, sex, age) {
    var getName = function() {
        return name;
    },
    setName = function(newName) {
        name = newName;
    },
    getSex = function() {
        return sex;
    }
    getAge = function() {
        return age;
    };
    return {
        getName: getName,
        setName: setName,
        getSex: getSex,
        getAge: getAge
    };
};

var person1 = Person("Lucy", "female", 22),
    person2 = Person("Tom", "male", 24);
person1.name;  //undefined  不能直接取私有變量的值
person1.getName();  //Lucy  通過公有方法取私有變量的值
person1.setName("Green");
person1.getName();  //Green  通過公有方法改變私有變量的值
person2.getName();  //Tom  通過公有方法取私有變量的值

上述Person方法有三個私有變量,name sex age,其中,name提供了取值和設值的方法,sexage只提供了取值方法,可以提高數據的安全性,這在之前的一篇博客中提到過,通常被稱爲穩妥構造函數模式

4. 模擬塊級作用域

Java C等許多編程語言中都有塊級作用域的概念,以Java爲例:

for(int i = 0; i< 10; i++) {
    //do something
}
System.out.println(i);  //i cannot be resolved to a variable

而 JavaScript中沒有塊級作用域的概念:

for(var i = 0; i< 10; i++) {
    //do something
}
var i;
console.log(i);  //10
var i = 5;
console.log(i);  //5

沒有塊級作用域的一個弊端是,一個項目的不同開發者之間定義的變量很有可能會重名,甚至同一個人也可能會有命名衝突,全局命名空間很有可能被污染。很多情況下,爲了不污染全局命名空間,可以使用閉包來模擬塊級作用域:

(function() {
    var just_a_number = 100;
    console.log(just_a_number); //100
})();
console.log(just_a_number);  //Uncaught ReferenceError: just_a_number is not defined

通過模擬塊級作用域,可以在很大程度上避免全局作用域被污染,建議在多人維護的JavaScript項目中使用模擬的塊級作用域。

關於閉包就說這麼多,相信讀者如果能夠把四個例子搞清楚,對閉包的理解就已經比較深了。閉包並非JavaScript獨有,Python、Ruby等許多語言都有閉包的概念,擅長其他語言的讀者可以通過自己的擅長語言來理解閉包,畢竟閉包的原理都是相通的。

完。


參考/擴展資料:

《JavaScript高級程序設計(第3版)》 作者:Nicholas C.Zakas 譯者:李鬆峯 曹力

JavaScript 裏的閉包是什麼?應用場景有哪些? -- 知乎

JavaScript 閉包究竟是什麼 -- 博客園

技術文檔:閉包 -- 火狐開發者

閉包和引用 -- JavaScript祕密花園

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