《JavaScript高級程序設計》(第3版)讀書筆記 第7章 函數表達式

  • 定義函數表達式的方式有兩種:javascript

    • 函數聲明。它的重要特徵就是 函數聲明提高(function declaration hoisting) 即在執行代碼以前會先讀取函數聲明。這就意味着能夠把函數聲明放在調用它的語句後面。
    • 函數表達式。
// 函數聲明
function functionName(params) {
  ...
}
// 函數表達式有幾種不一樣的方式,下面是最多見的一種
var functionName = function(params) {
  ...
}
  • 上面這種形式看起來好像是常規的變量賦值語句。而右邊這種形式建立的函數叫作 匿名函數 (anonymous function)(有時候也叫 拉姆達函數 lambda),由於function關鍵字後面沒有標識符。
  • 函數表達式與其餘表達式同樣,在使用前必須先賦值,不然會致使出錯。
sayHi();         // 錯誤,函數還不存在
var sayHi = function () {
  console.log('Hi!');
};
  • 表面上看,下面的代碼沒有問題,conditiontrue時,使用一個定義,不然使用另外一個定義。實際上,在ECMAScript中屬於無效語法,JavaScript引擎會嘗試修正錯誤,將其轉換爲合理狀態。但問題是瀏覽器嘗試修正錯誤的作法並不一致。大多數瀏覽器會返回第二個聲明,忽略condition的值;Firefox會在condition爲true的時候返回第一個聲明。所以這種作法很危險,不該該出如今你的代碼中。
// 不要這樣作!
if (condition) {
  function sayHi() {
    console.log('Hi!');
  }
} else {
  function sayHi() {
    console.log('Yo!');
  }
}
  • 上述代碼改成函數表達式就沒有問題
// 能夠這樣作
var sayHi;

if (condition) {
  sayHi = function() {
    console.log('Hi!');
  }
} else {
  sayHi = function() {
    console.log('Yo!');
  }
}
  • 可以建立函數再賦值給變量,也就能把函數做爲其餘函數的返回值。createComparisonFunction() 就返回了一個匿名函數。
function createComparisonFunction (propertyName) {
  return function (object1, object2) {
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];

    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  }
}
  • 在把函數當成值來使用的狀況下,均可以使用匿名函數。不過,這並非匿名函數惟一的用途。

遞歸

  • 遞歸函數是在一個函數經過名字調用自身的狀況下構成的
// 經典的遞歸階乘函數
function factorial (num) {
  if (num <= -1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}
  • 雖然遞歸階乘函數表面看沒有什麼問題,但下面的代碼卻可能致使它出錯
// 把factorial() 函數保存在變量anotherFactorial中
var anotherFactorial = factorial;

// 將factorial設置爲null
// 如今指向原始函數的引用只剩下anotherFactorial
factorial = null;

// 原始函數必須執行factorial()
// 但factorial再也不是函數,因此致使出錯
anotherFactorial(4);      // throw error!
  • 可使用 arguments.callee (指向正在執行函數的指針)實現函數的遞歸調用
// 非嚴格模式
function factorial (num) {
  if (num <= -1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}
  • 但在嚴格模式下不能經過腳本訪問 arguments.callee,會致使出錯。可使用命名函數表達式來達成相同的結果
var factorial = (function f(num) {
  if (num <= -1) {
    return 1;
  } else {
    return num * f(num - 1);
  }
})

閉包

  • 匿名函數閉包 是兩個概念,容易混淆。 閉包 是指有權訪問另外一個函數做用域中的變量的函數。
  • 建立閉包的常見方式,就是在一個函數內部建立另外一個函數,仍之前面的 createComparisonFunction() 函數爲例
function createComparisonFunction (propertyName) {
  
  return function (object1, object2) {

    // 下面兩行代碼訪問了外部函數中的變量propertyName
    // 即便這個內部函數被返回了,並且是在其餘地方被調用了
    // 它仍然能夠訪問變量 propertyName
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];

    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  }
}
  • 上述例子,即便內部函數被返回了,在其餘地方調用,它仍然能夠訪問propertName。由於這個內部函數的做用域鏈中包含 createComparisonFunction() 的做用域。要搞清楚其中細節,必須從理解函數被調用的時候都會發生什麼入手。
  • 第4章介紹過 做用域鏈。當某個函數被 調用 時會發生下列事情:前端

    • 建立一個 執行環境(execution context) 及相應的 做用域鏈
    • 使用 arguments 和其餘命名參數的值來初始化函數的 活動對象(activation object)
    • 造成做用域鏈。外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位...直至做爲做用域終點的全局執行環境
    • 函數執行過程當中,爲讀寫變量的值,就須要在做用域鏈中查找變量。
function compare(value1, value2) {
  if (value1 < value2) {
    return -1;
  } else if (value1 > value2) {
    return 1;
  } else {
    return 0;
  }
}

var result = compare(5, 10);
  • 後臺的每一個執行環境都有一個表示變量的對象——變量對象。全局環境的變量對象始終存在,而像compare()函數這樣的局部環境的變量對象,則只在函數執行的過程當中存在。java

    • 在建立 compare() 函數時,會建立一個預先包含全局變量對象的做用域鏈,這個做用域鏈被保存在內部的[[scope]]屬性中。
    • 當調用 compare() 函數時,會爲函數建立一個執行環境,而後經過複製函數的[[scope]]屬性中的對象構建起執行環境的做用域鏈。
    • 此後,又有一個活動對象(在此做爲變量對象使用)被建立並被推入執行環境做用域鏈的前端。
  • 對於本例, compare() 函數的執行環境而言,其做用域鏈中包含兩個變量對象:數組

    • 本地活動對象
    • 全局活動對象
  • 做用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
  • 不管何時在函數中訪問一個變量時,就會從做用域鏈中搜索具備相應名字的變量。通常而言,當函數執行完畢後,局部活動對象就會被銷燬,內存中近保存全局做用域(全局執行環境的變量對象)。
  • 可是閉包的狀況有所不一樣
function createComparisonFunction (propertyName) {
  
  return function (object1, object2) {

    // 下面兩行代碼訪問了外部函數中的變量propertyName
    // 即便這個內部函數被返回了,並且是在其餘地方被調用了
    // 它仍然能夠訪問變量 propertyName
    // 即爲 createComparisonFunction 的活動對象
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];

    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  }
}

// 建立比較函數
// 調用了 createComparisonFunction() 方法
// 建立了 createComparisonFunction 的活動對象
// 返回內部的匿名函數 保存在 compareNames
// createComparisonFunction 執行完畢
// 但它的活動對象仍被 內部匿名函數引用,因此活動對象仍然存在,不會銷燬
var compareNames = createComparisonFunction("name");

// 此時result調用了 保存在 compareNames 的匿名函數
// 該匿名函數保持了對 createComparisonFunction 活動對象的引用
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });

// 即便 compareNames 執行完畢,createComparisonFunction 活動對象依然存在
// 須要手動解除對匿名函數的引用(以便釋放內存)
compareNames = null;
  • 首先,建立的比較函數被保存在變量compareNames中,而經過將其設置爲null解除引用,就等於通知垃圾回收例程將其消除。隨着匿名函數的做用域鏈被銷燬,其餘做用域鏈(除了全局做用域)也均可以安全地的銷燬了。圖7-2展現了調用compareNames()的過程當中產生的做用域鏈之間的關係

圖片描述

  • 因爲閉包會攜帶包含它的函數的做用域,所以會比其餘函數佔用更多的內存。過分使用閉包可能致使內存佔用過多,咱們建議讀者只在絕對必要時再考慮使用閉包。雖然像V8等優化後的JavaScript引擎會嘗試回收被閉包占用的內存,但請仍是要謹慎使用。

閉包與變量

  • 做用域鏈的這種配置機制,引出了一個值得注意的反作用,即閉包只能取得包含函數中任何變量的最後一值。別忘了閉包所保存的是整個變量對象,而不是某個特殊的變量。
function createFunctions() {
  var result = new Array();

  for (var i=0; i < 10; i++) {
    // 賦值給數組元素的是匿名函數自己,不是具體的值
    // 因此在 createFunctions() 執行完畢後,調用數組內的函數,返回的是變量i的值
    // 而變量i在執行完畢後,等於 10
    result[i] = function() {
      // 返回指向變量 i 的指針
      return i;
    };
  }

  return result;
}
  • 這個函數會返回一個 函數數組。表面上看result裏的每一項函數都應該返回本身的索引值。但實際上每個函數返回的都是10
  • 由於每一個函數的做用域中都保存着createFunctions()函數的活動對象,因此它們引用的都是同一個變量i。當createFunctions()函數返回後,變量i的值是10,此時每一個函數都引用着保存變量i的同一個變量對象,因此在每一個函數內部i的值都是10.
  • 能夠經過建立另外一個匿名函數強制讓閉包的行爲符合預期
function createFunctions() {
  var result = new Array();

  for (var i=0; i < 10; i++) {
    // 此時返回的裏層匿名函數調用了外層匿名函數的 num
    // 裏層匿名函數建立並返回了一個訪問 num 的閉包
    // 如此一來 result 數組中的每一個函數都有本身的num變量副本
    result[i] = function(num) {
      // 返回建立的另外一個匿名函數
      return function() {
        return num;
      };
    }(i);
  }

  return result;
}

關於this對象

  • 在閉包中使用this對象也可能會致使一些問題。this對象是在運行時基於函數的執行環境綁定的:瀏覽器

    • 在全局函數中,this等於window
    • 當函數被做爲某個對象的方法調用時,this等於那個對象。
    • 匿名函數的執行環境具備全局性,所以其this對象一般指向window(經過call() apply()改變函數的執行環境的狀況除外)。但有時候因爲變成寫閉包的方式不一樣,這一點可能不會那麼明顯
var name = "The Window";

var object = {
  name: "My Object",

  getNameFunc: function() {
    return function() {
      return this.name;
    };
  }
};

// 在非嚴格模式下
object.getNameFunc()();                    // "The Window"
  • 每一個函數在被調用時都會自動取得兩個特殊變量: thisarguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能直接訪問外部函數中的這兩個變量(這一點經過圖7-2能夠看的清楚)。
  • 不過,把外部做用域中的this對象保存在一個閉包可以訪問到的變量裏,就可讓閉包訪問該對象了。
var name = "The Window";

var object = {
  name: "My Object",

  getNameFunc: function() {
    var that = this;
    return function() {
      return that.name;
    };
  }
};

object.getNameFunc()();                    // "My Object"
  • 在幾種特殊狀況下,this的值可能會意外的改變。
var name = "The Window";

var object = {
  name: "My Object",

  getName: function() {
    return this.name;
  }
};

// this 指向 object
object.getName();          // "My Object"

// 加上了括號,看似在引用一個函數
// 但 (object.getName) 和 object.getName 的定義是相同的
// 因此這行代碼與上面的代碼無異
(object.getName)();      // "My Object"

// 非嚴格模式
// 賦值語句會返回 object.getName 的匿名函數
// 至關於將匿名函數在全局環境下運行
(object.getName = object.getName)();          // "The Window"
  • 第三行代碼先執行了一條賦值語句,而後再調用賦值後的結果。由於這個賦值表達式的值是函數自己,因此this的值不能獲得維持,結果就返回了 "The Window" 。

內存泄漏

  • 因爲IE9以前的版本對JScript對象和COM對象使用不一樣的垃圾收集例程(第4章介紹過),所以閉包在IE的這些版本中會致使一些特殊的問題。具體來講,若是閉包的做用域鏈中保存着一個HTML元素,那麼就意味着該元素將沒法被銷燬
function assignHandler() {
  var element = document.getElementById("someElement");
  element.onclick = function() {
    console.log(element.id);
  };
}
  • 以上代碼建立了一個做爲element元素處理程序的閉包,而這個閉包則又建立了一個循環引用(事件將在第13章討論)。因爲匿名函數保存了一個對assignHandler()的活動對象的引用,所以就會致使沒法減小element的引用數。只要匿名函數存在,element的引用數至少是1。
// 如下修改能夠避免這個問題
function assignHandler() {
  var element = document.getElementById("someElement");
  var id = element.id;

  element.onclick = function() {
    console.log(id);
  };

  element = null;
}
  • 閉包中引用包含函數的整個活動對象,而其中包含着element。即便閉包不直接引用element,包含函數的活動對象也仍然會保存一個引用。所以有必要把element變量設置爲null

模仿塊級做用域

  • JavaScript沒有塊級做用域。這意味着在塊語句中定義的變量,其實是在包含函數中而非語句中建立的。
function outputNumerbs(cout) {
  for (var i=0; i < cout; i++) {
    console.log(i);
  }
  console.log(i);    // 計數
}
  • 在Java, C++等語言中,變量i只會在for循環的語句塊中有定義,循環一旦結束,變量i就會被銷燬。但是在JavaScript中,變量i是定義在outputNumbers()的活動對象中的,所以從它有定義開始,就能夠在函數內部隨處訪問它。即便像下面這樣錯誤的從新聲明變量也不會改變值。
function outputNumerbs(cout) {
  for (var i=0; i < cout; i++) {
    console.log(i);
  }

  var i;             // 從新聲明變量
  console.log(i);    // 計數
}
  • JavaScript歷來不會告訴你是否屢次聲明瞭同一個變量,遇到這種狀況,它只會對後續的聲明視而不見(不過,它會執行後續聲明中的變量初始化)。
  • 匿名函數能夠用來模仿塊級做用域並避免這個問題。用做塊級做用域(一般稱爲 私有做用域 )的匿名函數的語法以下:
(function() {
  // 這裏是塊級做用域
  ...
})()
// 常見的代碼片斷
// 定義了一個函數,而後當即調用它
var someFunction = function() {
  // 這裏是塊級做用域
  ...
};
someFunction();
  • 那這裏若是將函數名也去掉呢?答案是不行,會致使出錯。由於JavaScriptfunction關鍵字當作一個函數聲明的開始,而函數聲明後面不能跟圓括號。(函數表達式能夠)
function() {
  // 這裏是塊級做用域
  ...
}()    // 出錯!
  • 要將函數聲明轉換成函數表達式,只要外面包裹圓括號便可
(function() {
  // 這裏是塊級做用域
  ...
}())
  • 不管在什麼地方,只要臨時須要一些變量,就可使用私有做用域
function outputNumbers(cout) {
  // 這裏是一個閉包,匿名函數能夠訪問 cout
  (function () {
    for (var i=0; i < cout; i++) {
      console.log(i);
    }
  })();

  // 在這裏調用變量 i 會報錯
  console.log(i);                  // throw error
}
  • 這種技術常常在全局做用域中被用在函數外部,從而限制向全局做用域中添加過多的變量和函數。
(function() {

  var now = new Date();
  if (now.getMonth() == 0 && now.gettDate() == 1) {
    console.log('Haapy new year!");
  }
})();
  • 這種作法能夠減小閉包占用的內存,由於沒有指向匿名函數的引用。只要函數執行完畢,就能夠當即銷燬其做用域鏈了。

私有變量

  • 嚴格來說,JavaScript中沒有私有成員的概念;全部對象屬性都是共有的。不過卻是有一個私有變量的概念。
  • 任何在函數中定義的變量,均可以認爲是私有變量,由於不能在函數的外部訪問這些變量。
  • 若是在函數內部建立一個閉包,那麼閉包經過本身的做用域鏈能夠訪問這些私有變量。利用這一點,咱們就能夠建立用於訪問私有變量的公有方法。
  • 有權訪問私有變量和私有函數的公有方法稱爲 特權方法(privileged method) 。有兩種建立特權方法的方式:安全

    • 在構造函數中定義特權方法(靜態私有變量)
    • 模塊模式
  • 構造函數中定義,基本的模式以下
// 構造函數Person
// 入參 name 是它的私有變量
function Person(name) {
  this.getName = function() {
    return name;
  };

  this.setName = function(value) {
    name = value;
  };
}

var person = new Person("Nicholas");
console.log(person.getName());              // "Nicholas"

person.setName("Greg");
console.log(person.getName());              // "Greg"
  • 這種模式有一個缺點,那就是必須使用構造函數來達到目的,而第6章討論過,構造函數模式的肯定是針對每個實例都會建立出一樣的一組新方法
  • 而使用靜態私有變量來實現特權方法就能夠避免這個問題。

靜態私有變量

(function() {

  // 私有變量
  var privateVariable = 10;

  // 私有函數
  function privateFunction() {
    return false;
  }

  // 構造函數
  // 這裏沒有使用var操做符,自動建立全局變量
  // 嚴格模式下不能使用
  MyObject = function() {};

  // 公有/特權方法
  MyObject.prototype.publicMethod = function() {
    privateVariable++;
    return privateFunction();
  };
})();
  • 公有方法是在原型上定義的,避免了重複建立方法的狀況。
  • 須要注意的是,這個模式在定義構造函數時沒有使用函數聲明,而是使用了函數表達式。函數聲明只能建立局部函數,但那並非咱們想要的。
  • 這個特權方法,做爲一個閉包,老是保存着對包含做用域的引用。
(function() {

  var name = "";

  Person = function(value) {
    name = value;
  };

  Person.prototype.getName = function() {
    return name;
  };

  Person.prototype.setName = function (value) {
    name = value;
  };
})();

var person1 = new Person("Nicholas");
console.log(person1.getName());                          // "Nicholas"
person1.setName("Greg");
console.log(person1.getName());                          // "Greg"

var person2 = new Person("Michael");
console.log(person1.getName());                          // "Michael"
console.log(person2.getName());                          // "Michael"
  • 這個例子中的Person構造函數與getName() setName() 方法同樣,都有權訪問私有變量name
  • name變成了一個靜態的、由全部實例共享的屬性。
  • 以這種方式建立靜態私有變量會由於使用原型而增進代碼複用,但每一個實例都沒有本身的私有變量。
  • 多查找做用域鏈中的一個層次,就會在必定程度上影響查找速度。而這正是使用閉包和私有變量的一個明顯的不足之處。

模塊模式

  • 模塊模式經過爲單例添加私有變量和特權方法使其獲得加強
  • 這種模式在須要對單例進行某些初始化,同時又須要維護其私有變量時很是有用
var application = function() {

  // 私有變量和函數
  var components = new Array();

  // 初始化
  components.push(new BaseComponent());

  // 公共
  return {
    getComponentCuont: function() {
      return components.length;
    },

    registerComponent: function(component) {
      if (typeof component == "object") {
        components.push(component)
      }
    }
  };
}();
  • 在WEB應用程序中,常常須要使用一個單例來管理應用程序級的信息。這個簡單的例子建立了一個用於管理組件的application對象。
  • 簡而言之,若是必須建立一個對象並以某些數據對其進行初始化,同時還要公開一些可以訪問這些私有數據的方法,那麼就可使用模塊模式。
  • 以模塊模式建立的每一個單例都是Object的實例,由於最終要經過一個對象字面量來表示他。

加強的模塊模式

  • 加強的模塊模式適合那些單例必須是某種類型的實例,同時還必須添加某些屬性和(或)方法對其加以加強的狀況。
  • 若是前述例子中的application對象必須是BaseComponent的實例,能夠以下代碼
var application = function() {

  // 私有變量和函數
  var components = new Array();

  // 初始化
  components.push(new BaseComponent());

  // 建立 application 的一個局部副本
  var app = new BaseComponent();

  // 公共接口
  app.getComponentCuont = function() {
    return components.length;
  };

  app.registerComponent = function(component) {
    if (typeof component == "object") {
      components.push(component);
    }
  };

  // 返回這個副本
  return app;
}();
相關文章
相關標籤/搜索