主要內容:
- 什么是閉包
- 閉包使用的一般模式
- 閉包都能做些什么
本文是我的JavaScript高級這個系列中的第二篇文章. 在這個系列中,我計划分析說明 一下JavaScript中的一些常用的而又神秘的高級內容,包括:作用域鏈、閉包、函數調用形
式、面向對象等內容. 本文就閉包做個說明. 一說到JavaScript,就能想到閉包是個神奇的東西. 到底閉包是什么,以及怎么使用? 今天我們來分析一下!
同樣,這個也屬於JavaScript的高級的部分,對於JavaScript而言基礎非常重要,對於 基本語法,動態語言的基本特征希望不太了解的朋友,找本書或一些系統點的資料看看. 這
樣有助於對后文的理解. 當然,也可以到http://net.itcast.cn中去下載一下東西看看.
下面正式進入今天的主題.
一、何為閉包
"閉包"這個詞並非是JavaScript特有的,實際上閉包是一個特有的概念. 至於概念本身 我不過多介紹,百度一下什么都有. 我主要說說JavaScript中閉包是什么.
在JavaScript中閉包就是函數
閉包就是函數,這個概念似乎感覺有點迷惑. 實際上很簡單,閉包就是一個封閉包裹的 范圍. 前文咱們提到過,函數可以限定變量的作用域. 一個變量在函數內部聲明,那么在函
數外部是無法訪問的. 那么這個就是一個封閉的范圍. 廣義上說就是一個閉包了!
那么這個樣子其實沒有什么意義. 因為沒有什么特別的地方, 但是如果函數中又定義了 函數,並將這個函數以返回值的形式返回,那么,在JavaScript中"子域訪問父域"的規則就
會打破了. 因為這個時候,在函數外就可以訪問函數內的變量. 看下面代碼:
1 var func = function() { 2 var num = 10; 3 return function() { 4 alert(num); 5 }; 6 }; 7 var foo = func(); 8 foo();
這段代碼中,函數foo是0級鏈,而變量num是在1級鏈中,這個時候,0級鏈的函數就訪問了1級
鏈中的變量num,這段代碼運行結果是打印出10. 這樣就實現了JavaScript中的閉包.
小結一下,JavaScript中的閉包就是在函數中定義變量,然后通過返回值,將可以訪問這 個變量的函數返回,這樣在函數外就可以訪問函數內的變量了. 這樣就形成了閉包.
二、閉包的使用案例及其說明
閉包的案例非常的多. 在JavaScript中,使用閉包就像C語言中使用指針一樣. 其基本語法 簡單,但是使用靈活多變,使用其靈活的語法與特征就能實現許多非常強大的功能. 在此不能闡
述閉包的所有用法,但是對於剛剛接觸閉包的朋友,下面的案例足夠理解一段時間了.
2.1 模擬私有成員
這個案例是JavaScript實現面向對象的基礎. 看下面代碼
1 var Person = function(name, age, gender) { 2 return { 3 get_name : function() { 4 return name; 5 }, 6 set_name : function(value) { 7 name = value; 8 }, 9 get_age : function(){ 10 return age; 11 }, 12 get_gender : function(){ 13 return gender; 14 } 15 }; 16 };
這段代碼就是一個函數,函數帶有三個參數,也就是說在函數內部有三個局部變量,分別表示姓
名(name)、年齡(age)和性別(gender). 然后在返回值中,返回一個對象,該對象提供四個方法.
分別給年齡提供讀寫方法,給性別與年齡提供讀取的方法. 這四個函數都是這個函數的子域. 因
此返回的這個對象就可以直接訪問這三個變量. 但是有了讀寫的訪問權限的限制.
2.2 Fibonacci數列
Fibonacci數列就是:1, 1, 2, 3, 5, 8, 13, ...
這個案例是面試題中經常考到的案例,也算是具有代表性的算法題. 看下面代碼:
1 // 為了簡單就不做n的判斷處理了 2 var Fib = (function() { 3 var fibArr = [1,1]; 4 return function( n ) { 5 var res = fibArr[n]; 6 if(res) { 7 return res; 8 } else { 9 res = arguments.callee(n - 1) + arguments.callee(n - 2); 10 fibArr.push(res);
// 這里掉了一句代碼
return res; 11 } 12 }; 13 })();
這個案例一般傳統的做法就是使用遞歸,但是遞歸的性能問題十分可怕,如果大家有興趣可以 計算一下這個數列的第20項結果是多少,並統計一下這個函數遞歸調用了多少次. 如下面代碼
1 var count = 0; 2 var fib = function(n) { 3 count++; 4 // 為了簡單就不做n的判斷處理了 5 if(n == 0 || n == 1) return 1; 6 return fib(n-1) + fib(n-2); 7 }; 8 var res = fib(20); 9 alert("fib(20)的結果為:" + res + ", 函數調用了 " + count + " 次");
然后再用新方法,計算同樣的結果,並統計一下次數.
1 var count = 0; // 為了簡單就不做n的判斷處理了 2 var Fib = (function() { 3 var fibArr = [1,1]; 4 return function( n ) { 5 count++; 6 var res = fibArr[n]; 7 if(res) { 8 return res; 9 } else { 10 res = arguments.callee(n - 1) + arguments.callee(n - 2); 11 fibArr.push(res); 12 return res; 13 } 14 }; 15 })(); 16 var res = Fib(20); 17 alert("Fib(20)的結果為:" + res + ", 函數調用了 " + count + " 次");
這個結果,我不在這里揭曉,請大家自己下去運行看看.
下面分析一下這段新方法的代碼. 在這段代碼中,綁定在Fib中的函數,實際上是后面函數運 行的返回結果. 后面這個函數有一個私有變量,是一個數組. 保存着第0項和第1項數組的值. 然后
返回一個函數. 在調用 Fib(20) 的時候就是在執行這個被返回的函數.
這個函數中,首先訪問數組的第n項值,如果數組中有這個數據,就直接返回,否則實現遞歸 計算這個值,並將值加到數組中,最后返回計算的結果. 在JavaScript中,遞歸使用
arguments.callee()表示當前調用函數(即遞歸函數).
那么這么做最直接的結果是,存在一個緩存,將計算得到的結果保存在緩存中,並且實現所有 的計算只計算一次,那么可以大大的提高性能.
2.3 html字符串案例
這個是許多js庫使用的辦法,在很多js庫中需要使用正則表達式處理一些數據,而如果每次執 行都在方法中保存需要處理匹配的字符串,那么會大量的消耗內存,影響性能. 因此可以將重復使
用的表達式都保存在閉包中,每次使用都是訪問的這個字符串. 例如:
1 String.prototype.deentityify = function() { 2 var entity = { 3 lt : '<', 4 gt : '>' 5 }; 6 return function() { 7 return this.replace(/&([^;]+);/g, function(a,b) { 8 var r = entity[b]; 9 return typeof r === 'string' ? r : a; 10 }); 11 }; 12 }();
這段代碼會將任何一個字符串中的 < 和 > 都替換成尖括號<和>,對於頁面html代碼的復制
非常好用.
2.4 事件處理方法的追加與移除
在JavaScript中並不支持事件處理函數的追加. 大師 Jeremy Keith 給出了一個辦法:
1 var loadEvent = function( fn ) { 2 var oldFn = window.onload; 3 if( typeof oldFn === "function" ) { 4 window.onload = function() { 5 oldFn(); 6 fn(); 7 }; 8 } else { 9 window.onload = fn; 10 } 11 };
不過這段代碼沒有辦法移除已經追加的方法,那么使用閉包的緩存功能就可以輕易的實現.
1 var jkLoad = (function() { 2 var events = {}; 3 var func = function() { 4 window.onload = function() { 5 for(var i in events) { 6 events[i](); 7 } 8 }; 9 }; 10 return { 11 add : function(name, fn) { 12 events[name] = fn; 13 func(); 14 }, 15 remove : function(name) { 16 delete events[name]; 17 func(); 18 } 19 }; 20 })();
這段代碼就是得到用來追加和移出load事件的對象. 如果要追加事件,可以使用
1 jkLoad.add("f1", function() { 2 // 執行代碼1 3 });
如果要移除事件處理函數,就是用代碼
1 jkLoad.remove("f1");
那么這個案例還可以擴展到對應以對象追加指定的事件,那么怎么實現,請大家
自己考慮吧!!!
三、小結
到此,我們已經分析了閉包是什么,以及閉包的實現一般方式,最后又分析了 幾個閉包的案例. 我想大家應該對閉包有了更為深刻的理解. 那么在后面的面向對
象等高級內容中,我們將再次看到閉包的強大之處.
下面對前面問題做個解答:
第一個問題:
1 var func = function() { 2 alert("調用外面的函數"); 3 }; 4 var foo = function() { 5 func(); 6 7 var func = function() { 8 alert("調用內部的函數"); 9 }; 10 11 func(); 12 };
這段代碼在IE下會報錯,而在FF和Chrome中會沒有任何效果,因為在foo中第一個函
數的調用func()就會報錯,出現異常,因此后面代碼不在執行. 如果需要修改,只需
要try-catch一下就好. 如:
1 var func = function() { 2 alert("調用外面的函數"); 3 }; 4 var foo = function() { 5 try { 6 func(); 7 } catch ( e ) { 8 alert( e ); 9 } 10 var func = function() { 11 alert("調用內部的函數"); 12 }; 13 14 func(); 15 };
第二個問題:
1 if(! "a" in window) { 2 var a = "定義變量"; 3 } 4 alert(a);
這段代碼會返回 undefined.
首先,這段代碼中沒有函數,因此在if中定義的變量會提前,即等價於
1 var a; 2 if(! "a" in window) { 3 var a = "定義變量"; 4 } 5 alert(a);
而 in 運算符是用來判斷左邊的字符串表示的屬性是否是右邊對象的成員. 在瀏覽器
中JavaScript的全局對象就是window,而直接定義的變量實際上就是全局對象的一個
屬性,因此如果已經定義了變量a,那么 "a" in window 就返回true,然后取反,即
為false,所以if中的代碼不會執行,就不會給a賦值,所以打印結果為 undefined.
上面代碼就等價於:
1 var a; 2 if( false ) { 3 a = "定義變量"; 4 } 5 alert(a);