7 函數表達式

本章內容

  • 函數表達式的特征
  • 使用函數實現遞歸
  • 使用閉包定義私有變量

定義函數的方式有兩種:一種是函數聲明,另一種就是函數表達式。函數聲明的語法是這樣的。

function functionName(arg0, arg1, arg2) {
//函數體
}

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

sayHi ();
function sayHi () {
  alert("Hi!");
}

第二種創建函數的方式是使用函數表達式。函數表達式有幾種不同的語法形式。最常見的是

var functionName = function(arg0, arg1, arg2) {
//函數體
};

這種情況下創建的函數叫匿名函數,因為 function關鍵字后面沒有標識符。
函數表達式與其他表達式一樣,在使用前必須先賦值。

7.1 遞歸

遞歸函數是在一個函數通過名字調用自身的情況下構成的。

function factorial (num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num-1);
  }
}

表面上沒有問題,下面的代碼卻可能導致出錯

var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial (4));  //出錯!

因為在調用anotherFactorial()時,由于必須執行 factorial(),而它已不是函數,所以導致了錯誤。使用arguments.callee可以解決這個問題。

function factorial (num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num-1);
  }
}

在編寫遞歸函數時,使用arguments.callee總比使用函數名更保險。
但在嚴格模式下,不能通過腳本訪問arguments.callee。不過,可以使用命名函數表達式達到相同的結果。

var factorial = (function f(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * f(num-1);
  }
});

7.2 閉包

不少開發人員總是搞不清匿名函數和閉包這兩個概念,因此經常混用。閉包是指有權訪問另一個函數作用域中的變量的函數。創建閉包的常見方式,就是在一個函數內部創建另一個函數。

function createComparisonFunction(propertyName) {
  return function(object1, object2) {
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];
    // if 判斷 value1 value2
  }
}

內部函數中的代碼訪問了外部函數中的變量propertyName。之所以還能夠訪問這個變量,是因為內部函數的作用域鏈中包含createComparisonFunction()的作用域。
有關如何創建作用域鏈和作用域鏈有什么作用的細節,對徹底理解閉包至關重要。當某個函數第一次被調用時,會創建一個執行環境及相應的作用域鏈,并把作用域鏈賦值給一個特殊的內部屬性(即[[Scope]])。然后,使用this, arguments和其他命名參數的值來初始化函數的活動對象。但在作用域鏈中,外部函數的活動對象始終處于第二位,外部函數的外部函數的活動對象處于第三位,。。。直至作為作用域鏈終點的全局執行環境。
一般來講,當函數執行完畢后,局部活動對象就會被銷毀,內存中僅保存全局作用域。但閉包的情況又有所不同。
createComparisonFunction()函數返回后,其執行環境的作用域鏈會被銷毀,但他的活動對象仍然留在內存中,直到匿名函數被銷毀后,createComparisonFunction()的活動對象才會被銷毀。

//創建函數
var compareNames = createComparisonFunction('name');
//調用函數
var result = compareNames({name: 'nic'}, {name: 'Greg'});
//解除對匿名函數的引用
compareNames = null;

占用更多的內存,慎重使用閉包

7.2.1 閉包與變量

作用域鏈的這種配置機制引出了一個值得注意的副作用,即閉包只能取得包含函數中任何變量的最后一個值。閉包所保存的是整個變量對象,而不是某個特殊的變量。

function createFunctions() {
  var result = new Array();

  for (var i = 0; i < 10; i++) {
    result[i] = function() {return i};
  }
  return result;
}

實際上,函數數組中的每個函數都返回 10。因為每個函數的作用域保存的活動對象引用的都是同一個變量 i 。但是,我們可以通過創建另一個匿名函數強制讓閉包的行為符合預期。

function createFunctions() {
  var result = new Array();
  for (var i=0; i < 10; i++) {
    result[i] = function(num){
      return function() {
        return num;
      }
    }(i);
  }
  return result;
}

7.2.2 關于 this 對象

this對象是在運行時基于函數的執行環境綁定的:在全局函數中,this等于window,而當函數被作為某個對象的方法調用時,this等于那個對象。不過,匿名函數的執行環境具有全局性,因此其this對象通常指向window。但有時候由于編寫閉包的方式不同,這一點可能不會那么明顯。

thisarguments也存在同樣的問題。如果想訪問作用域中的arguments對象,必須將對該對象的引用保存到另一個閉包能夠訪問的變量中。

7.2.3 內存泄漏

7.3 模仿塊級作用域

JavaScript 沒有塊級作用域的概念。這意味著在塊語句中定義的變量,實際上是在包含函數中而非語句中創建的。

function outputNumbers(count) {
  for (var i = 0; i< count; i++) {
    alert(i);
  }
  var i;   //重新聲明變量
  alert(i);  //計數
}

JavaScript 從來不會告訴你是否多次聲明了同一個變量;它只會對后續的聲明視而不見(不過,它會執行后續聲明中的變量初始化)。匿名函數可以用來模仿塊級作用域并避免這個問題。

(function() {
  //這里是塊級作用域
}) ();

該代碼定義并立即調用了一個匿名函數。將函數聲明包含在一對圓括號中,表示它實際上是一個函數表達式。而緊隨其后的另一對圓括號會立即調用這個函數。
無論在什么地方,只要臨時需要一些變量,就可以使用私有作用域。

function outputNumbers(count) {
  (function () {
    for (var i = 0; i < count; i++) {
      alert(i);
    }
  }) ();
  alert(i);  //導致一個錯誤!
}

在匿名函數中定義的任何變量,都會在執行結束時被銷毀。
通過創建私有作用域,每個開發人員既可以使用自己的變量,又不必擔心搞亂全局作用域。

這種做法可以減少閉包占用的內存問題,因為沒有指向匿名函數的引用。只要函數執行完畢,就可以立即銷毀其作用域鏈了。

7.4 私有變量

任何在函數中定義的變量,都可以認為是私有變量,因為不能在函數的外部訪問這些變量。私有變量包括函數的參數,局部變量和在函數內部定義的其他函數。

function add(num1, num2) {
  var sum = num1 + num2;
  return sum;
}

在函數內部可以訪問這幾個變量,但在外部不能訪問。如果在這個函數內部創建一個閉包,那么閉包通過自己的作用域鏈也可以訪問這些變量。而利用這一點,就可以創建用于訪問私有變量的共有方法。
有兩種在對象上創建特權方法的方式。第一種是在構造函數中定義特權方法,基本模式如下。

function MyObject() {
  //私有變量和私有函數
  var privateVariable = 10;
  function privateFunction () {
    return false;
  }
  //特權方法
  this.publicMethod = function () {
    privateVariable++;
    return privateFunction();
  };
}

構造函數模式的缺點是針對每個實例都會創建同樣一組新方法,而使用靜態私有變量來實現特權方法就可以避免這個問題。

7.4.1 靜態私有變量

通過在私有作用域中定義私有變量或函數,同樣也可以創建特權方法,其基本模式如下所示。

(function () {
  //私有變量和私有函數
  var privateValue = 10;
  function privateFunction () {
    return false;
  }
  //構造函數
  MyObject = function () {};

  //共有/特權方法
  MyObject.prototype.publicMethod = function () {
    privateVariable++;
    return privateFunction();
  };
}) ();

注意,這里MyObject是全局變量,能夠在私有作用域之外被訪問到。在嚴格模式下會出錯。

多查找作用域鏈中的一個層次,就會在一定程度上影響查找速度。而這正是使用閉包和私有變量的一個明顯的不足之處。

7.4.2 模塊模式

道格拉斯所說的模塊模式則是為單例創建私有變量和特權方法。所謂單例,就是只有一個實例的對象。按照慣例,JavaScript 是以對象字面量的方式來創建單例對象的。

var singleton = {
  name: value,
  method: function () {
    //這里是方法的代碼
  }
};

模塊模式通過為單例添加私有變量和特權方法能夠使其得到增強,其語法形式如下:

var singleton = function () {
  //私有變量和私有函數
  var privateVariable = 10;
  function privateFunction () {
    return false;
  }
  //特權方法和屬性
  return {
    publicProperty: true,
    publicMethod: function () {
      privateVariable++;
      return privateFunction();
    }
  };
}();

返回的對象字面量中只包含可以公開的屬性和方法。

7.4.3 增強的模塊模式

在返回對象之前假如對其增強的代碼。這種增強的模塊模式適合那些單例必須是某種類型的實例,同時還必須添加某些屬性或方法對其加以增強的情況。

var object = new CustomType();
object.publicProperty = true;
object.publicMethod = function () {
  privateVariable++;
  return privateFunction();
};
return object;

7.5 小結

  • 函數表達式不同于函數聲明。函數聲明要求有名字,但函數表達式不需要。沒有名字的函數表達式也叫做匿名函數。
  • 在無法確定如何引用函數的情況下,遞歸函數就會變得比較復雜;
  • 遞歸函數應該始終使用 arguments.callee來遞歸地調用自身,不要使用函數名--函數名可能會發生變化。

當在函數內部定義了其他函數時,就創建了閉包。閉包有權訪問包含函數內部的所有變量,原理如下。

  • 在后臺執行環境中,閉包的作用域鏈包含著它自己的作用域,包含函數的作用域和全局作用域。
  • 通常,函數的作用域及其所有變量都會在函數執行結束后被銷毀。
  • 但是,當函數返回了一個閉包時,這個函數的作用域將會一直在內存中保存到閉包不存在為止。

使用閉包可以模仿塊級作用域。

  • 創建并立即調用一個函數,這樣既可以執行其中的代碼,又不會在內存中留下對該函數的引用。
  • 結果就是函數內部的所有變量被立即銷毀--除非將某些變量賦值給了包含作用域(即外部作用域)中的變量。

閉包還可以用于在對象中創建私有變量。

  • 可以使用閉包來實現公有方法,而通過公有方法可以訪問在包含作用域中定義的變量。
  • 有權訪問私有變量的公有方法叫做特權方法。
  • 可以使用構造函數模式,原型模式來實現自定義類型的特權方法,也可以使用模塊模式,增強的模塊模式來實現單例的特權方法。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 定義函數的方式有兩種:函數聲明和函數表達式。 函數聲明的一個重要特征就是函數聲明提升,意思是在執行代碼前會先讀取函...
    oWSQo閱讀 686評論 0 0
  • 定義函數的方法有兩種:函數聲明和函數表達式。 函數聲明 使用函數聲明時,函數聲明會被提升至當前作用域最前面。 但是...
    exialym閱讀 377評論 0 3
  • 常規方式定義函數 定義函數有兩種方式,第一種方式為常規聲明方式,該方式下函數可以先使用,后聲明,即"函數聲明提升"...
    勤勞的悄悄閱讀 271評論 0 0
  • 幾天前,思修課老師要求同學們以愛國與青年為主題進行演講。談到了關于樂天超市和一些愛國行為的看法。很多人爭執不下,于...
    所溺閱讀 359評論 0 0
  • 今天整天都在陪老家上來的姐妹和她女兒,帶著她們看了兩場電影,小朋友想一次就看夠,剛好時間合適,就滿足下她的心愿,今...
    卓彤的美好時光閱讀 184評論 0 0