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


主要內容:

  1. 什么是閉包
  2. 閉包使用的一般模式
  3. 閉包都能做些什么


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

這段代碼會將任何一個字符串中的 &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);

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM