關于JavaScript閉包的應用
什么是閉包(Closure)
簡單講,閉包就是指有權訪問另一個函數作用域中的變量的函數。
MDN 上面這么說:閉包是一種特殊的對象。它由兩部分構成:函數,以及創建該函數的環境。環境由閉包創建時在作用域中的任何局部變量組成。
但是,網上找了好多資料,它們對閉包的定義也各有各樣,搞得我也不知道怎么去定義閉包了,所以干脆不去定義了,意會吧。
道可道,非常道;名可名,非常名。
產生一個閉包
創建閉包最常見方式,就是在一個函數內部創建另一個函數。下面例子中的 closure 就是一個閉包:
- function func(){
- var a = 1,b = 2;
- function closure(){
- return a+b;
- }
- return closure;
- }
閉包的作用域鏈包含著它自己的作用域,以及包含它的函數的作用域和全局作用域。
閉包的注意事項
通常,函數的作用域及其所有變量都會在函數執行結束后被銷毀。但是,在創建了一個閉包以后,這個函數的作用域就會一直保存到閉包不存在為止。
- function makeAdder(x) {
- return function(y) {
- return x + y;
- };
- }
- var add5 = makeAdder(5);
- var add10 = makeAdder(10);
- console.log(add5(2)); // 7
- console.log(add10(2)); // 12
- // 釋放對閉包的引用
- add5 = null;
- add10 = null;
add5 和 add10 都是閉包。它們共享相同的函數定義,但是保存了不同的環境。在 add5 的環境中,x 為 5。而在 add10 中,x 則為 10。***通過 null 釋放了 add5 和 add10 對閉包的引用。
在javascript中,如果一個對象不再被引用,那么這個對象就會被垃圾回收機制回收;
如果兩個對象互相引用,而不再被第3者所引用,那么這兩個互相引用的對象也會被回收。
閉包只能取得包含函數中任何變量的***一個值,這是因為閉包所保存的是整個變量對象,而不是某個特殊的變量。
- function test(){
- var arr = [];
- for(var i = 0;i < 10;i++){
- arr[i] = function(){
- return i;
- };
- }
- for(var a = 0;a < 10;a++){
- console.log(arr[a]());
- }
- }
- test(); // 連續打印 10 個 10
對于上面的情況,如果我們改變代碼如下:
- function test(){
- var arr = [];
- for(let i = 0;i < 10;i++){ // 僅在這里作出了改動
- arr[i] = function(){
- return i;
- };
- }
- for(var a = 0;a < 10;a++){
- console.log(arr[a]());
- }
- }
- test(); // 打印 0 到 9
對于上面兩種代碼的解釋,請看我在 segmentfault 上面的提問:鏈接
閉包中的 this 對象
- var name = "The Window";
- var obj = {
- name: "My Object",
- getName: function(){
- return function(){
- return this.name;
- };
- }
- };
- console.log(obj.getName()()); // The Window
obj.getName()()實際上是在全局作用域中調用了匿名函數,this指向了window。這里要理解函數名與函數功能(或者稱函數值)是分割開的,不要認為函數在哪里,其內部的this就指向哪里。匿名函數的執行環境具有全局性,因此其 this 對象通常指向 window。
- var name = "The Window";
- var obj = {
- name: "My Object",
- getName: function(){
- var that = this;
- return function(){
- return that.name;
- };
- }
- };
- console.log(obj.getName()()); // My Object
閉包的應用
應用閉包的主要場合是:設計私有的方法和變量。
任何在函數中定義的變量,都可以認為是私有變量,因為不能在函數外部訪問這些變量。私有變量包括函數的參數、局部變量和函數內定義的其他函數。
把有權訪問私有變量的公有方法稱為特權方法(privileged method)。
- function Animal(){
- // 私有變量
- var series = "哺乳動物";
- function run(){
- console.log("Run!!!");
- }
- // 特權方法
- this.getSeries = function(){
- return series;
- };
- }
模塊模式(The Module Pattern):為單例創建私有變量和方法。
單例(singleton):指的是只有一個實例的對象。JavaScript 一般以對象字面量的方式來創建一個單例對象。
- var singleton = {
- name: "percy",
- speak:function(){
- console.log("speaking!!!");
- },
- getName: function(){
- return this.name;
- }
- };
上面是普通模式創建的單例,下面使用模塊模式創建單例:
- var singleton = (function(){
- // 私有變量
- var age = 22;
- var speak = function(){
- console.log("speaking!!!");
- };
- // 特權(或公有)屬性和方法
- return {
- name: "percy",
- getAge: function(){
- return age;
- }
- };
- })();
匿名函數***的用途是創建閉包,并且還可以構建命名空間,以減少全局變量的使用。從而使用閉包模塊化代碼,減少全局變量的污染。
- var objEvent = objEvent || {};
- (function(){
- var addEvent = function(){
- // some code
- };
- function removeEvent(){
- // some code
- }
- objEvent.addEvent = addEvent;
- objEvent.removeEvent = removeEvent;
- })();
在這段代碼中函數 addEvent 和 removeEvent 都是局部變量,但我們可以通過全局變量 objEvent 使用它,這就大大減少了全局變量的使用,增強了網頁的安全性。
一個閉包計數器
- var countNumber = (function(){
- var num = 0;
- return function(){
- return ++num;
- };
- })();
閉包的缺陷
- 閉包的缺點就是常駐內存會增大內存使用量,并且使用不當很容易造成內存泄露。
- 如果不是因為某些特殊任務而需要閉包,在沒有必要的情況下,在其它函數中創建函數是不明智的,因為閉包對腳本性能具有負面影響,包括處理速度和內存消耗。
***再來一些有關閉包的面試題
下面代碼中,標記 ? 的地方輸出分別是什么?
- function fun(n,o){
- console.log(o);
- return {
- fun: function(m){
- return fun(m,n);
- }
- };
- }
- var a = fun(0); // ?
- a.fun(1); // ?
- a.fun(2); // ?
- a.fun(3); // ?
- var b = fun(0).fun(1).fun(2).fun(3); // ?
- var c = fun(0).fun(1); // ?
- c.fun(2); // ?
- c.fun(3); // ?
- undefined
- 0
- 0
- 0
- undefined, 0, 1, 2
- undefined, 0
- 1
- 1
參考資料
- 【書】《JavaScript 高級程序設計(第三版)》
- 【文章】學習Javascript閉包(Closure)
- 【文章】【JavaScript】【函數】閉包閉包!