常規(guī)方式定義函數(shù)
定義函數(shù)有兩種方式,第一種方式為常規(guī)聲明方式,該方式下函數(shù)可以先使用,后聲明,即"函數(shù)聲明提升"特性。
//函數(shù)提升
sayHi();
function sayHi(){
alert("Hi!");
}
常規(guī)聲明方式方式定義的函數(shù)有 name 屬性,可以獲取函數(shù)的名字
// name 屬性
function functionName(arg0, arg1, arg2) {
//function body
}
alert(functionName.name); //"functionName"
匿名方式定義函數(shù)
第二種方式為匿名函數(shù)方式,即函數(shù)沒(méi)有名字。但是函數(shù)對(duì)象可以賦值給變量,可以作為參數(shù),也可以作為返回值
var functionName = function(arg0, arg1, arg2){
//function body
};
7.1 遞歸
遞歸就是函數(shù)自己調(diào)用自己,下面求階乘的函數(shù)是個(gè)典型的遞歸函數(shù)
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * factorial(num-1);
}
}
但是上面這種寫法在 Javascirpt 中可能會(huì)出現(xiàn)錯(cuò)誤,因?yàn)樵?Javascript 中,函數(shù)的名字可能發(fā)生變化
var anotherFactorial = factorial;
factorial = null;//這個(gè)名字不再指向函數(shù)對(duì)象
alert(anotherFactorial(4)); //出錯(cuò)
可以用 arguments.callee 來(lái)解決這個(gè)問(wèn)題,arguments.callee 始終指向當(dāng)前正在執(zhí)行的函數(shù)對(duì)象
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * arguments.callee(num-1);
}
}
在有些情況下,arguments.callee 并不存在,可以用命名函數(shù)表達(dá)式來(lái)實(shí)現(xiàn)遞歸
//在函數(shù)定義外部包裹括號(hào),可以獲取函數(shù)對(duì)象
var factorial = (function f(num){
if (num <= 1){
return 1;
} else {
return num * f(num-1);
}
});
7.2 閉包
閉包也是一個(gè)函數(shù),但是這個(gè)函數(shù)保存了另外一個(gè)函數(shù)作用域中的變量。通常創(chuàng)建閉包的方式
- 在一個(gè)函數(shù)中創(chuàng)建另外一個(gè)函數(shù)
- 內(nèi)部函數(shù)訪問(wèn)了外部函數(shù)中的局部變量
- 外部函數(shù)執(zhí)行完畢后被銷毀,而內(nèi)部函數(shù)對(duì)象因?yàn)橛懈L(zhǎng)的生命周期而被保留,因而變成了閉包
function createComparisonFunction(propertyName) {
//定義內(nèi)部函數(shù)
return function(object1, object2){
//訪問(wèn)了外部函數(shù)的變量,該函數(shù)將變?yōu)殚]包
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
};
}
執(zhí)行環(huán)境
- 執(zhí)行環(huán)境是一組數(shù)據(jù),這組數(shù)據(jù)定義了代碼運(yùn)行的邊界,防止出現(xiàn)混亂。
- 執(zhí)行環(huán)境在函數(shù)執(zhí)行時(shí)候被創(chuàng)建,函數(shù)執(zhí)行完畢后,被銷毀。
-
window
對(duì)象是一個(gè)全局執(zhí)行環(huán)境,不會(huì)被銷毀。
變量對(duì)象
每一個(gè)執(zhí)行環(huán)境都有一個(gè)對(duì)應(yīng)的變量對(duì)象,變量對(duì)象中保存著函數(shù)能訪問(wèn)到的局部變量和函數(shù)的名字。
作用域鏈
每個(gè)執(zhí)行環(huán)境都會(huì)包含一個(gè)作用域鏈,作用域鏈的頂端指向當(dāng)前環(huán)境的變量對(duì)象,下一個(gè)指向外部環(huán)境的變量對(duì)象,最后指向全局環(huán)境的變量對(duì)象。作用域鏈保存在函數(shù)對(duì)象的 [[Scope]] 屬性中
查找變量
函數(shù)查找一個(gè)變量的時(shí)候,先從作用域鏈頂端的變量對(duì)象中查找,找不到則到下一個(gè)變量對(duì)象中查找,一直查找到最后一個(gè)全局環(huán)境的變量對(duì)象為止。
作用域鏈創(chuàng)建的流程
函數(shù)運(yùn)行時(shí),會(huì):
- 先創(chuàng)建函數(shù)自己的局部執(zhí)行環(huán)境
- 向執(zhí)行環(huán)境中加入一個(gè)默認(rèn)的作用域鏈,該作用域鏈只包含全局環(huán)境的變量對(duì)象
- 將函數(shù)自己的變量對(duì)象添加到作用域鏈的頂端
下面的代碼運(yùn)行后,會(huì)產(chǎn)生如圖的數(shù)據(jù)結(jié)構(gòu)
function compare(value1, value2){
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
閉包為什么能保存外部環(huán)境中的變量
當(dāng)內(nèi)部環(huán)境訪問(wèn)外部環(huán)境的時(shí)候,外部環(huán)境的變量對(duì)象會(huì)被添加到內(nèi)部環(huán)境的作用域鏈中,因此內(nèi)部環(huán)境可以訪問(wèn)外部環(huán)境。
一般情況下,內(nèi)部環(huán)境總是先于外部環(huán)境執(zhí)行完畢,因此先被銷毀
特殊的情況下,外部環(huán)境先被銷毀,而內(nèi)部環(huán)境被長(zhǎng)期保留。這時(shí)候,由于內(nèi)部環(huán)境的作用域鏈中引用了外部環(huán)境的變量對(duì)象,因此外部環(huán)境的變量對(duì)象會(huì)被保留下來(lái),被銷毀的只有外部環(huán)境本身和他的作用域鏈。
適度使用閉包并及時(shí)釋放閉包占用的內(nèi)存
- 因?yàn)殚]包會(huì)阻止外部環(huán)境銷毀其變量對(duì)象,因此閉包會(huì)占用額外內(nèi)存,過(guò)度的閉包會(huì)導(dǎo)致性能下降
- 可以閉包使用完畢后,及時(shí)將其設(shè)置為 null ,手動(dòng)釋放內(nèi)存
7.2.1 閉包與變量的狀態(tài)
閉包中引用的外部變量,其值可能會(huì)在閉包創(chuàng)建以后繼續(xù)發(fā)生變化,而閉包并不能記住創(chuàng)建時(shí)外部變量的狀態(tài)。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
//所有閉包引用的都是同一個(gè)外部變量對(duì)象中的同一個(gè) i ,因此只能得到最后的 i 的狀態(tài)
result[i] = function(){
return i;
};
}
return result;
}
要想保持閉包創(chuàng)建時(shí)外部變量的狀態(tài),可以將外部變量作為閉包的參數(shù)傳進(jìn)去,這樣閉包中只有對(duì)本地變量使用,不受外部變量變化的影響
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i);//將狀態(tài)作為參數(shù)穿進(jìn)去
}
return result;
}
7.2.2 關(guān)于 this 指針
- 在全局作用于下,this 指向 window 對(duì)象
- 在方法中,this 指向方法所在的對(duì)象
- 在閉包中,this 指向 window 對(duì)象
在下面的例子中,object.getNameFunc()()
將返回一個(gè)閉包。這是因?yàn)椋罱K返回的匿名函數(shù)的外部環(huán)境已經(jīng)不存在了,因此匿名函數(shù)變成了閉包。所以,其中的 this 將指向 window 對(duì)象
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window" (in non-strict mode)
要想在這種情況下讓 this 指向?qū)ο蟊旧恚梢韵扔靡粋€(gè)臨時(shí)變量保存 this ,然后利用閉包的特性引用臨時(shí)變量,而不是直接使用 this
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
//用一個(gè)臨時(shí)變量保存 this
var that = this;
return function(){
//在內(nèi)部引用臨時(shí)變量
return that.name;
};
}
};
alert(object.getNameFunc()()); //"My Object"
7.2.3 閉包的內(nèi)存泄漏:
一般情況下 HTML 對(duì)象的事件屬性會(huì)指向一個(gè)函數(shù)對(duì)象, HTML 對(duì)象被銷毀,函數(shù)對(duì)象也會(huì)被銷毀。
某些特殊情況下,事件處理函數(shù)中,又反過(guò)來(lái)引用了 HTML 對(duì)象,事件處理函數(shù)就變成了一個(gè)閉包。這是一種循環(huán)引用,導(dǎo)致 HTML 對(duì)象將不會(huì)被銷毀
function assignHandler(){
var element = document.getElementById("someElement");
//事件處理函數(shù)引用了外部變量,且生存周期比外部環(huán)境長(zhǎng),因此變成了閉包
//閉包中引用的對(duì)象將不會(huì)被釋放
element.onclick = function(){
alert(element.id);
};
}
解決辦法:可以先將對(duì)象的屬性賦值給外部函數(shù)的一個(gè)局部變量,閉包中引用這個(gè)局部變量而不引用 HTML 對(duì)象。 HTML 對(duì)象使用完成后應(yīng)該賦值 null
function assignHandler(){
var element = document.getElementById("someElement");
//將 HTML 對(duì)象復(fù)制給一個(gè)局部變量,閉包中引用這個(gè)局部變量
var id = element.id;
element.onclick = function(){
alert(id);
};
//釋放這個(gè) HTML 對(duì)象
element = null;
}
7.3 塊級(jí)作用域
Javascript 中沒(méi)有塊級(jí)作用域的概念。下面的代碼中,臨時(shí)變量 i 超出了作用于仍然存在
function outputNumbers(count){
for (var i=0; i < count; i++){
alert(i);
}
alert(i); //count
}
可以將使用臨時(shí)變量的代碼放到塊級(jí)作用域中,這樣可以防止命名空間污染,減少內(nèi)存占用
function outputNumbers(count){
(function () {
for (var i=0; i < count; i++){
alert(i);
}
})();
alert(i); //causes an error
}
7.4 私有變量
函數(shù)本身就具有封裝性,在函數(shù)內(nèi)部定義的變量和函數(shù)都是私有的,外部不能訪問(wèn)
///外部看不見(jiàn) num1 num2 等變量
function add(num1, num2){
var sum = num1 + num2;
return sum;
}
訪問(wèn)接口
完全私有的變量是沒(méi)有用處的,可以留一個(gè)公共接口供外部訪問(wèn)
function MyObject(){
//私有變量
var privateVariable = 10;
//私有函數(shù)
function privateFunction(){
return false;
}
//特權(quán)方法,外部可以訪問(wèn)
this.publicMethod = function (){
privateVariable++;
return privateFunction();
};
}
實(shí)例成員私有模式
這是最基本的模式,每個(gè)實(shí)例都會(huì)創(chuàng)建自己獨(dú)立的成員,不同實(shí)例之間互不影響;通過(guò)特權(quán)函數(shù)可以訪問(wèn)實(shí)例的私有成員。
缺點(diǎn)是:特權(quán)函數(shù)也會(huì)在每個(gè)實(shí)例創(chuàng)建一份,浪費(fèi)資源。
function Person(name){
this.getName = function(){
return name;
};
this.setName = function (value) {
name = value;
};
}
var person = new Person("Nicholas");
alert(person.getName()); //"Nicholas"
person.setName("Greg");
alert(person.getName()); //"Greg"
7.4.1 靜態(tài)私有變量
實(shí)現(xiàn)方式:
- 所有成員,包括私有成員、特權(quán)函數(shù)和構(gòu)造函數(shù)都在一個(gè)自調(diào)用的匿名函數(shù)中定義
- 通過(guò)匿名函數(shù)的自調(diào)用創(chuàng)建所有成員
- 其中構(gòu)造函數(shù)不使用 var 關(guān)鍵字,因此可以全局訪問(wèn)
- 訪問(wèn)接口在原型中定義
(function(){
//私有成員和私有方法
var privateVariable = 10;
function privateFunction(){
return false;
}
//構(gòu)造函數(shù),沒(méi)有用 var 關(guān)鍵字,所以可以全局調(diào)用
MyObject = function(){
};
//特權(quán)方法
MyObject.prototype.publicMethod = function(){
privateVariable++;
return privateFunction();
};
})();//通過(guò)自調(diào)用創(chuàng)建成員
模式特點(diǎn):
- 所有成員,包括私有成員、特權(quán)函數(shù)和構(gòu)造函數(shù)只在匿名函數(shù)自調(diào)用的時(shí)候創(chuàng)建一次
- 私有成員因?yàn)楸粯?gòu)造函數(shù)和特權(quán)函數(shù)引用,因此得以在匿名函數(shù)自調(diào)用完成以后,仍然保留在內(nèi)存中
- 構(gòu)造函數(shù)可以多次調(diào)用,創(chuàng)建多個(gè)實(shí)例,但是私有成員不會(huì)被再次創(chuàng)建
- 所有實(shí)例共享私有變量,就好像其他語(yǔ)言中的靜態(tài)變量
(function(){
//name 本來(lái)是匿名函數(shù)的私有變量,在匿名函數(shù)自調(diào)用的時(shí)候創(chuàng)建
var name = "";
//構(gòu)造函數(shù)閉包引用了外部變量,并且生命周期比外部環(huán)境長(zhǎng),因此變成了閉包
//因此,匿名函數(shù)自調(diào)用完畢后,私有變量不會(huì)被銷毀
Person = function(value){
name = value;
};
Person.prototype.getName = function(){
return name;
};
Person.prototype.setName = function (value){
name = value;
};
})();
var person1 = new Person("Nicholas");
alert(person1.getName()); //"Nicholas"
person1.setName("Greg");
alert(person1.getName()); //"Greg"
var person2 = new Person("Michael");
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"
7.4.2 模塊模式(單例模式)
在匿名函數(shù)中沒(méi)有定義構(gòu)造函數(shù),而是在匿名函數(shù)自調(diào)用以后返回一個(gè)對(duì)象,對(duì)象中包含了公共接口
var singleton = function(){
//私有成員
var privateVariable = 10;
function privateFunction(){
return false;
}
//返回對(duì)象,
return {
publicProperty: true,
publicMethod : function(){
privateVariable++;
return privateFunction();
}
};
}();
這種模式下,私有成員同樣只創(chuàng)建一次,通過(guò)公共接口訪問(wèn)
var application = function(){
//私有成員
var components = new Array();
//一些其他的初始化動(dòng)作
components.push(new BaseComponent());
//公共接口
return {
getComponentCount : function(){
return components.length;
},
registerComponent : function(component){
if (typeof component == "object"){
components.push(component);
}
}
};
}();
7.4.2 增強(qiáng)單例模式
和上面的單例模式差不多,只是將返回值由字面量對(duì)象改成普通對(duì)象
var singleton = function(){
//私有成員
var privateVariable = 10;
function privateFunction(){
return false;
}
//創(chuàng)建一個(gè)對(duì)象
var object = new CustomType();
//添加特權(quán)接口
object.publicProperty = true;
object.publicMethod = function(){
privateVariable++;
return privateFunction();
};
//返回對(duì)象
return object;
}();
例程
var application = function(){
//private variables and functions
var components = new Array();
//initialization
components.push(new BaseComponent());
//create a local copy of application
var app = new BaseComponent();
//public interface
app.getComponentCount = function(){
return components.length;
};
app.registerComponent = function(component){
if (typeof component == "object"){
components.push(component);
}
};
//return it
return app;
}();