主要內容:
什麼是閉包
閉包使用的一般模式
閉包都能做些什麼
本文是我的JavaScript高級這個系列中的第二篇文章. 在這個系列中,我計劃分析說明 一下JavaScript中的一些常用的而又神秘的高級內容,包括:作用域鏈、閉包、函數調用形
式、面向對象等內容. 本文就閉包做個說明. 一說到JavaScript,就能想到閉包是個神奇的東西. 到底閉包是什麼,以及怎麼使用? 今天我們來分析一下!
同樣,這個也屬於JavaScript的高級的部分,對於JavaScript而言基礎非常重要,對於 基本語法,動態語言的基本特征希望不太瞭解的朋友,找本書或一些系統點的資料看看. 這
樣有助於對後文的理解. 當然,也可以到https://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);