JavaScript高級之閉包的概念及其應用

主要內容:

什麼是閉包

閉包使用的一般模式

閉包都能做些什麼

 

  本文是我的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 }();

 

這段代碼會將任何一個字符串中的 &lt; 和 &gt; 都替換成尖括號<和>,對於頁面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);

發佈留言