本章內容
- 函數表達式的特征
- 使用函數實現遞歸
- 使用閉包定義私有變量
定義函數的方式有兩種:一種是函數聲明,另一種就是函數表達式。函數聲明的語法是這樣的。
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
。但有時候由于編寫閉包的方式不同,這一點可能不會那么明顯。
this
和arguments
也存在同樣的問題。如果想訪問作用域中的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
來遞歸地調用自身,不要使用函數名--函數名可能會發生變化。
當在函數內部定義了其他函數時,就創建了閉包。閉包有權訪問包含函數內部的所有變量,原理如下。
- 在后臺執行環境中,閉包的作用域鏈包含著它自己的作用域,包含函數的作用域和全局作用域。
- 通常,函數的作用域及其所有變量都會在函數執行結束后被銷毀。
- 但是,當函數返回了一個閉包時,這個函數的作用域將會一直在內存中保存到閉包不存在為止。
使用閉包可以模仿塊級作用域。
- 創建并立即調用一個函數,這樣既可以執行其中的代碼,又不會在內存中留下對該函數的引用。
- 結果就是函數內部的所有變量被立即銷毀--除非將某些變量賦值給了包含作用域(即外部作用域)中的變量。
閉包還可以用于在對象中創建私有變量。
- 可以使用閉包來實現公有方法,而通過公有方法可以訪問在包含作用域中定義的變量。
- 有權訪問私有變量的公有方法叫做特權方法。
- 可以使用構造函數模式,原型模式來實現自定義類型的特權方法,也可以使用模塊模式,增強的模塊模式來實現單例的特權方法。