匿名函數 & 閉包 ( 7 章 )


   1:  function functionName(arg0,arg1){
   2:   
   3:   
   4:   
   5:  }
   6:   
   7:  var functionName = function( arg0 , arg1 ){
   8:   
   9:   
  10:   
  11:  }

匿名函數時一種強大的令人難以置信的工具

這兩種的區別,  前者會在代碼執行以前被加載到作用域中, 而后者則是在代碼執行到那一行的時候才會有定義.

另外的區別是, 函數聲明( 前者) 會給函數一個名字, 而函數表達式(后者) 則是創建一個匿名函數, 然后把這個匿名函數賦給一個變量 functionName.

關於函數聲明, 它的一個重要特征就是 函數聲明提升, 意思是在執行代碼之前會先讀取函數聲明, 這就意味着可以把函數聲明放在調用它的語句后面.

//匿名函數, 直接寫也是可以的,只不過沒有什么實際意義。

把函數想成值時, 都可以使用匿名函數 例如 :

在將函數作為參數傳入另一個函數,或者從一個函數中返回另一個函數時,通常都要使用以這種形式來定義匿名函數。

   1:  function createCompare(propertyName){
   2:   
   3:      return function( object1, object2 ){
   4:   
   5:          var value1 = object1[ propertyName] ;
   6:   
   7:          var value1 = object2[ propertyName] ;
   8:   
   9:          if ( value1 < value2 ){
  10:   
  11:              return 1 ;
  12:   
  13:           }else {
  14:   
  15:               return -1;
  16:   
  17:            }
  18:   
  19:         
  20:   
  21:      } ;
  22:   
  23:  }
遞歸 ( 用途 1 )
   1:  <SPAN style="COLOR: #000000">function factorial( num ){
   2:   
   3:    if(num <=1 ){
   4:   
   5:    return 1 ;
   6:   
   7:    } else {
   8:   
   9:      return num * factorial( num -1 ) ;
  10:   
  11:    }
  12:   
  13:  }
  14:   
  15:  var anotherFactorial = factorial ;
  16:   
  17:  factorial = null ;
  18:   
  19:  alert( anotherFactorial(4) ) ;      // 出錯, 因為此時 factorial = null , 已經不再可用, 所以第一次調用沒問題, 因為使用 anotherFactorial 指針, 可以找到對應的函數, 但是函數內部,
  20:   
  21:                                                         // 由於是遞歸函數, 所以函數內部的名字 factorial 失效 ( null ) , 所以就會出錯
  22:   
  23:        return arguments.callee( num - 1 ) ;   // 可以解決問題, 之前有提到過, 因此遞歸函數時, 使用 arguments.callee 總比使用函數名好
  24:  </SPAN>

使用 arguments.callee( num –1 );

   1:  function factorial( num ){
   2:   
   3:    if (num <= 1){
   4:   
   5:        return 1;
   6:   
   7:     } else (
   8:   
   9:        return num * arguments.callee( num -1 )          // 其中 arguments.callee 是一個指向正在執行的函數的指針
  10:   
  11:     )
  12:   
  13:  }
  14:   
  15:  var anotherFactorial = factorial ;
  16:   
  17:  factorial = null ;
  18:   
  19:  alert( anotherFactorial(4) ) ;
閉包

閉包是指 有權訪問另一個函數作用域中的變量的函數. 創建閉包的常見方式, 是在一個函數內部創建另一個函數,建議非必要時, 不要使用閉包,匿名函數也是差不多。

即,閉包是一個函數,是能夠訪問別的函數作用域變量的函數,那么內部函數肯定能訪問外部函數的變量, 所以,此時就用到了匿名函數 ( 閉包 ), 所以閉包和匿名函數是什么關系的,是包含關系,即閉包肯定是個匿名函數。例如:

   1:  function createComparisonFunction(propertyName){
   2:   
   3:    return function( object1, object2){
   4:   
   5:    var value1 = object1[propertyName];
   6:   
   7:    var value2 = object2[propertyName];
   8:   
   9:    if (value1 < value2){
  10:   
  11:      return -1;
  12:   
  13:    } else if ( value1 > value2){
  14:   
  15:      return 1;
  16:   
  17:    } else {
  18:   
  19:      return 0;
  20:   
  21:    }
  22:   
  23:   };
  24:   
  25:  }

內部函數訪問了外部函數變量 prototyName,即使這個內部函數被返回了,而且是在其他地方被調用了,它仍然可以訪問變量 prototyName,內部函數的作用域鏈包括外部函數,

當某個函數第一次被調用時, 會創建一個執行環境及相應的作用域鏈, 並把作用域鏈賦值給一個特殊的內部屬性[Scope] , 然后, this, arguments 和其他命名參數的值來初始化函數的活動對象.但在作用域鏈中, 內部函數-> 外部函數-> 更外部函數->......-> 最外部函數. ( 這種包含關系 )

   1:  function compare(value1, value2){
   2:      if(value1 < value2){
   3:          return -1;
   4:      }else if(value1 > value2){
   5:          return 1;
   6:      }else{
   7:          return 0;
   8:      }
   9:          
  10:  }
  11:   
  12:  var result = compare(5,10);

image

對於這個例子中compare()函數的執行環境,其作用域鏈中包含兩個變量對象,本地活動對象和全局變量對象,顯然,作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。

后台的每個執行環境都有一個表示變量的對象-變量對象, 全局環境的變量對象始終存在, 而像compare()函數這樣的局部環境變量對象, 則只在函數執行的過程中存在, 在創建compare()函數時, 會創建一個預先包含全局變量對象的作用域鏈, 這個作用域鏈保存在內部的[[Scope]]屬性中, 當調用 compare()函數時, 會為函數創建一個執行環境, 然后通過復制函數的[[Scope]]屬性中的對象構建起執行環境的作用於鏈. 此后, 又有一個活動對象(在此作為變量對象使用) 被創建並被推入執行環境作用域鏈的前端, 對於這個例子 compare() 函數的執行環境而言, 其作用域鏈中包含兩個變量對象, 本地活動對象和全局變量對象, 顯然作用域鏈本質上是一個指向變量對象的指針列表, 它只引用但不實際包含變量對象.

無論什么時候在函數中訪問一個變量時,就會從作用域鏈中搜索具有相應名字的變量,一般來講,當函數執行完畢后,局部活動對象就會被銷毀,內存僅保存全局作用域,但是閉包的情況不同.

在另一個函數內部定義的函數會將包含函數( 即 外部函數 )的活動對象添加到它的作用域鏈中。

 

承接以上代碼例子 :

var compare = createComparisonFunction(“name”);  //compare指針保存函數地址

var result = compare( {name : “Nicholas”}, {name : “Greg”});

在匿名函數從 createComparisonFunction()中被返回后,它的作用域鏈被初始化為包含createComparisonFunction()函數的活動對象和全局對象,這樣,匿名函數就可以訪問createComparisonFunction()中定義的所有變量, 更為重要的是, createComparisonFunction()函數執行完畢后,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。(那么匿名函數為什么沒有被釋放呢? 因為createComparisonFunction 這個函數本身返回的是一個函數, 所以compare=createComparisonFunction 這條語句的本質就是又做了一個指針指向了內存中的匿名函數, 所以在釋放空間時, 因為匿名函數在外圍函數的外圍還有一個指針指向, 所以它不能被釋放, 同樣它指向的外圍函數內部的變量也不能被釋放)換句話說,當createComparisonFunction()函數返回后,其執行環境的作用域鏈會被銷毀,但它的活動對象仍然會留在內存中,直到匿名函數被銷毀后,createComparisonFunction()的活動對象才會被銷毀。所以需要顯示解除 :  例如:

// 創建函數

var compareNames = createComparisonFunction("name"); // 這是 compareNames 已經是指向了函數內部的匿名函數

// 調用函數, // 注意, 這個函數執行完畢后, 函數的內存空間將被釋放, 但是... 匿名函數和匿名函數所指向的函數的內部變量不能被釋放

var result = comareNames({name: "Nicholas"}, {name: “Greg”});

// 解除對匿名函數的引用( 以便釋放內存 )

compareNames = null;

image

注意: ( anonymous ) Scope 中的 this 是 window, 這也就呼應了下邊的 this 的問題.

關於函數參數傳遞(特別是匿名函數 )

argument
1 /* 匿名函數傳參辦法 */
2 
3 /* 01. 普通函數 */
4 
5 var aa = createFunction( xx );    // xx為實參
6 
7 /* 02. 匿名函數 */
8 var aa = function(num){}(argu);    // num為形參, argu為實參

 

作用域鏈的副作用,即閉包只能取得包含函數中的任何變量的最后一個值,而不是某個特殊變量。

   1:  function createFunctions(){
   2:      var result = new Array();
   3:      for(var i=0; i<10; i++){
   4:          result[i] = function(){
   5:              return i;
   6:          };
   7:      }
   8:      return result;
   9:  }
  10:   
  11:  var funcs = createFunctions();
  12:  //每個函數都輸出10
  13:  for(var i=0; i<funcs.length; i++){
  14:      document.write(funcs[i]());
  15:  }

注意:

var funcs = createFunctions();  // 這個 createFunctions() 函數會執行, 並將返回結果 賦值給 funcs, 那么返回的是數組, funcs 就是數組, 返回的是函數 funcs 是函數.

var funcs = createFunctions;    // 這個 createFunctions 函數不會執行, 只是又定義了一個指針指向該函數.

首先, 來說createFunction函數的返回結果, 它所返回的不是一個數字, 而是一個函數, 所以, 當我們接到返回結果時, 因為是一個函數, 而funcs[i]() 表示執行這個函數, 而此時執行這個函數, 這個函數只有1條語句 return i, 而這時因為已經退出了 createFunction函數, 所以i的值是9. 所以最后顯示的結果全是9, 之前有誤區 :

誤區1 createFunctions()函數返回的數值, 首先 return result 是返回語句, 表示此函數返回的是一個數組, 那么數組中裝的是什么的, 很直接的是函數, 而並非是 i 的值, 如果是i的值, 那么后面輸出時也就不用寫成funcs[i]()了, 而直接寫成 funcs[i]就可以了. 注意閉包內部名沒有執行, 因為開始是個賦值語句, result[i]=function(){}; 這個賦值成功了就可以了, 函數內部並沒有執行, 函數必須后面跟()后才能執行, 剩下的就是單純的賦值, 不要想的那么復雜.

誤區2 以為result[i]=function(){}; 這一步會執行function里的內容, 其實直到funcs[i]() 這個語句的時候, 閉包里的內容才真正的執行, 包括 var funcs = createFunctions(); 這條語句, 閉包都沒有執行, 這條語句時 createFunction 這個函數執行了, 並把結果 包含有函數的數組返回給了funcs, 這個時候 funcs就是一個數組了, 並且數組元素是一個function, 所以才可以調用 funcs[i](), 來執行數組元素函數.

上例中,每個函數都返回10,而不是1,2,3,4,5,6,7,8,9,10,因為每個函數的作用域鏈中都保存着createFunctions()函數的活動對象,所以它們引用的都是 同一個變量 i , 當createFunctions()函數返回后,變量 i 的值是 10 , 此時每個函數都引用這保存變量 i 的同一個變量對象,(如上, var funcs = createFunctions() 這條語句會執行 createFunctions函數, 執行后, 函數內的 i 的值就是10了, 並且將數組返回給了 funcs, 此時 funcs 就是一個數組, 而數組元素是 函數, 注意, 此處的數組元素並非數字, 所以當下邊又執行 document.write(funcs[i]()))時, 實際上是在執行數組元素里的函數, 也就是匿名函數, 而此時, 匿名函數會去調用createFunctions 中的 i , 而此時, i 已經是10了, createFunction這個函數還在作用域鏈的原因就是因為有匿名函數還在引用它. 所以整個 createFunction函數的作用域都沒有被釋放. )

 

所以在每個函數內部 i 的值都是 10。改進如下 :

   1:  function createFunctions(){
   2:      var result = new Array();
   3:      for(var i=0; i<10; i++){
   4:          result[i] = function(num){
   5:              return function(){
   6:                  return num;
   7:              }(i);
   8:                          
   9:          };
  10:      }
  11:      return result;
  12:  }
  13:   
  14:  var funcs = createFunctions();
  15:   
  16:  for(var i=0; i<funcs.length; i++){
  17:      document.write(funcs[i]());
  18:  }

這里, 我們沒有直接把閉包賦值給數組, 而是定義了一個匿名函數, 並立即執行該匿名函數, 將結果賦值給數組, 這里匿名函數的參數是 num, 每次執行時會將 i 傳遞進去, 由於函數參數是按值傳遞的, 所以就會將變量 i 的當前值賦值給參數 num, 而在這個匿名函數內部, 又創建了一個訪問 num 的閉包, 這樣一來, result 數組中每個函數都有自己的 num 變量的一個副本, 因此就有各自不同的值了. 注意這里的匿名函數, 如下:

result[i] = function(num) {  // 匿名函數

  return function() {    // 閉包, 所有的閉包都是匿名函數

    return num;

  }

}(i);    // (i) 表示要執行

這個匿名函數之所以能夠立刻執行, 就是因為后邊的(i), 函數名稱后邊帶(), 就表示要執行函數, 正式因為要執行函數, 所以 var funcs = createFunctions(); 要執行函數 createFunctions, 而進入createFunction 后, 又要執行函數 function(num), 注意上邊例子的閉包是不需要執行這個的, 這里就有一個閉包, result[i] 里邊存放的也都是函數, result[i] 里的函數不需要執行, 但是此時的 num 所處的環境是在 function(num) 這里, 這里每個 num 的值已經不同, 當再執行 document.write(funcs[i]()); 調用閉包的匿名函數時, 同樣此時在執行 result[i] 里的函數, 而這時的函數所返回的內容是 num, 而 num 因為每個所處的環境不同, 都在各自的 function(num) 環境中, 所以它們之間沒有影響, 所以最后返回的結果是, 1,2,3,4,5,6,7,8,9,10.

 

function createFunction() {
    var result = new Array();
    for (var i=0; i<=9; i++) {
        result[i] = function() {
            return i;    
        }();
    }
    return result;
}

var funs = createFunction();
for(var i=0; i<funs.length; i++) {
    alert(funs[i]);
}

所以, 以上是更簡單的得到0,1,2,3,4,5,6,7,8,9的方法, 只要在原來的基礎上, result[i] = function(){return i;}(); 加上這一對小括號, 讓這個閉包在函數內部執行就可以了

 

 

this對象

在閉包中使用 this 對象也可能會有點問題,this對象是在運行時基於函數的執行環境綁定的,在全局函數中,this等於window,在函數被作為某個對象的方法調用時,this等於那個對象。不過, 匿名函數的執行環境具有全局性, 因此 this 對象通常指向 windows.

   1:  var name = "The window";
   2:   
   3:  var object = {
   4:      name : "My Object",
   5:      getNameFunc : function(){
   6:          return function(){
   7:              return this.name;
   8:          }
   9:      }
  10:  };
  11:   
  12:  alert(object.getNameFunc());    //"The window"

以上代碼先創建了全局變量 name, 又創建了一個包含 name 屬性的對象, 這個對象還包含一個方法-getNameFunc(), 它返回一個匿名函數, 而匿名函數又返回 this.name,

為什么上邊例子執行結果是 The window ?

前面曾經提過, 每個函數在被調用時, 其活動對象都會自動取得兩個特殊變量 this 和 arguments, 內部函數在搜索這兩個變量時, 只會搜索到活動對象為止, 因此永遠不可能訪問到外部函數中的這兩個變量, 不過, 把外部作用域中的 this對象保存在一個閉包能夠訪問的到的變量里, 就可以讓閉包訪問到該變量了,

個人總結, 因為閉包會將它所在的函數的作用域鏈全部保存起來, 所有就有類似的事情, 閉包->外部函數->再外部函數->全局, 這是閉包的作用域鏈, 因為閉包全部保存了, 而又由於閉包只能取得以上作用域鏈的最后值, 例如上邊例子 i, 都是顯示10 一樣, 所以當你定義的變量名字 name 與環境變量同名時, 閉包要搜索到作用域鏈最后的這個 name, 所以就后這個例子就返回了 全局 name 屬性.

 

匿名函數的執行環境具有全局性,所以它的 this 屬性都是 window, 並且 this 是關鍵字。看下邊例子 :

this
 1 var name = "The window";
 2 
 3 var object = {
 4     name : "My object",
 5     getNameFunc : function(){
 6         var that = this;    // 此時是可以訪問this的, 因為this是
 7                         // 這個對象的一個屬性, 
 8         return function() {  // 閉包 
 9             return that.name;    // that 作為外層函數的活動對象,
10                                           // 會被引用(注意不是this, this不會
11                              // 被引用, 
12         };
13     }
14 };

之所以這個會顯示我們想要的結果, 是因為在這個作用域鏈上沒有 that 同名的屬性, 所以當閉包搜索 that 屬性時, 這個 Object 里的 that 就是最頂端了, 換句話說是最后了, 所以自然就要顯示這個 that 的結果了.

模仿塊級作用域

JavaScript中沒有塊級作用域的概念, 例如 for 循環中的 i , 當離開 for 循環時, 也還繼續存在, 而在 C 和 Java 這些語言中就不一樣了,

function outputNumbers(count) {

  for (var i=0; i<count; i++) {

    alert(i);

  }

  alert(i);  // 此處的 i, 仍然可以調用, 因為沒有塊級別作用域

}

 

一般來說, 我們應該盡量少向全局作用域中添加變量和函數. 通過這種創建私有作用域, 每個開發人員既可以使用自己的變量, 又不必擔心搞亂全局作用域.

   1:  (                               //此處的 (
   2:   
   3:    function(){
   4:   
   5:      //這里是塊級作用域
   6:   
   7:    }
   8:   
   9:  ) ( ) ;                        // 此處的 ) ( ) ;

以上代碼定義並立即調用了一個匿名函數, 將函數聲明包含在一對圓括號中, 表示它實際上是一個函數表達式, 而緊隨其后的另一對圓括號會立即調用這個函數.

如果只是單純的寫 function(){

                        }();

這樣會出現錯誤,因為 Javascript 將 function 關鍵字當做一個函數聲明的開始,而函數聲明后面不能跟圓括號,然而,函數表達式后面可以跟圓括號(表示執行函數),要將函數聲明轉換成函數表達式,只要在function前后加一個圓括號就可以了, 即

( function {

}) ();

無論什么地方,只要臨時需要一些變量,就可以使用似有作用域,例如 :

似有作用域
1 function outputNumbers(count) {
2     (function() {
3         for (var i=0; i<count; i++) {
4             alert(i);
5         }
6     })();
7     alert( i );    //導致一個錯誤
8 }

 

   1:  var someFunction = function () {
   2:   
   3:      // 這里是塊級別作用域
   4:   
   5:  } ; 
   6:   
   7:  someFunction();      //調用函數

調用函數的方式是在函數名后邊加一對圓括號 (), 即 someFunction()

臨時使用一些變量時, 可以使用這種塊級作用域

   1:  (
   2:   
   3:    function(){
   4:   
   5:      for ( var i = 0 ; i < count ; i++ ) {
   6:   
   7:               alert( i ) ;
   8:   
   9:      }
  10:   
  11:    }
  12:   
  13:  ) () ;
  14:   
  15:    alert ( i ) ;     // 導致一個錯誤
私有變量

JavaScript中沒有私有成員概念, 不過, 有一個私有變量的概念, 任何函數中定義的變量, 都可以認為是私有變量, 外部不能訪問函數內部的私有變量, 但是, 可以通過在函數內部創建一個閉包( 即內部函數) , 那么閉包通過自己的作用域鏈也可以訪問這些變量, 而利用這一點, 就可以創建用於訪問私有變量的公有方法.

我們把有權訪問私有變量和私有方法的公有方法稱為 特權方法.

第一種,構造函數中定義的特權方法:

   1:  function MyObject(){
   2:   
   3:          var privateVariable = 10 ;         //私有變量
   4:   
   5:          function privateFunction() {
   6:   
   7:             return false ;
   8:   
   9:          }
  10:   
  11:          this.publicMethod = function() {       // 特權方法
  12:   
  13:                     privateVariable++;
  14:   
  15:                     return PrivateFunction();
  16:   
  17:          } ;
  18:   
  19:  }

另一種方法:

   1:  ( function(){
   2:      //似有變量和私有函數
   3:      var privateVariable = 10;
   4:      function privateFunction(){
   5:          return false;
   6:      }
   7:   
   8:      //構造函數
   9:      MyObject = function(){};
  10:   
  11:      //公有特權方法
  12:      MyObject.prototype.publicMethod = function(){
  13:          privateVariable++;
  14:          return privateFunction();
  15:      };
  16:    }
  17:   
  18:  )();

Good
 1 ( function() {
 2     var name = "";    //私有變量
 3    Person = function(value) {
 4         name = value;
 5     }; 
 6    Person.prototype.getName = function() {
 7         return name;
 8     };
 9     Person.prototype.setName = function(value) {
10         name = value;
11     };
12 
13      
14 } ) ();
15 
16 var person = new Person("Nicholas");
17 alert( person.getName() );    // "Nicholas"
18 person.setName("Greg");
19 alert( person.getName() );    // "Greg"


免責聲明!

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



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