定義函數(shù)的方法有兩種:函數(shù)聲明和函數(shù)表達式。
函數(shù)聲明
使用函數(shù)聲明時,函數(shù)聲明會被提升至當(dāng)前作用域最前面。
//這樣也不會報錯
sayHi();
function sayHi(){
alert("Hi!");
}
但是這個特性也就造成了這樣的使用是不可預(yù)測的:
if(condition){
function sayHi(){
alert("Hi!");
}
} else {
function sayHi(){
alert("Yo!");
}
}
這段代碼本來的目的是在兩個if的情況下使用不同的函數(shù)定義,但是在函數(shù)聲明被提前的情況下,這顯然是做不到的。所以在JS里永遠(yuǎn)也不要這么做。
函數(shù)表達式
函數(shù)表達式就是將一個匿名函數(shù)賦值給一個變量,所以直到執(zhí)行到這句代碼之前,這個函數(shù)都是不存在的。
sayHi(); // 報錯函數(shù)不存在
var sayHi = function(){
alert("Hi!");
};
不過這樣的特性就可以實現(xiàn)剛才的目的咯~
var sayHi;
if(condition){
sayHi = function(){
alert("Hi!");
};
} else {
sayHi = function(){
alert("Yo!");
};
}
遞歸
就是函數(shù)自己調(diào)用自己咯
最初級的版本
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * factorial(num-1);
}
}
這個看起來并沒有什么問題呀,在函數(shù)定義里調(diào)用了自己。但是由于有函數(shù)表達式的存在,這樣就會出錯了:
//將anotherFactorial指向函數(shù)
var anotherFactorial = factorial;
//將factorial指向空
factorial = null;
//再調(diào)用這個函數(shù)時,里面的factorial(num-1)就無法執(zhí)行了
alert(anotherFactorial(4));
第二個版本
使用arguments.callee這個指向正在執(zhí)行函數(shù)的指針可以解決這個問題。
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * arguments.callee(num-1);
}
}
這樣就解決了函數(shù)名在遞歸函數(shù)里被寫死的問題。但是在嚴(yán)格模式下訪問不到這個指針哦,所以。。。。還得想個辦法。
第三個版本
這時函數(shù)表達式就上場了。
var factorial = (function f(num){
if (num <= 1){
return 1;
} else {
return num * f(num-1);
}
});
這樣寫即便把函數(shù)再賦值給別的變量f()還是可以保留。其實這個和第一種方法的本質(zhì)是一樣的,而且有個大問題就是這個函數(shù)表達式要放在調(diào)用前面。
閉包
閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。創(chuàng)建閉包的常見方式就是在一個函數(shù)中創(chuàng)建另一個函數(shù)。就像這樣:
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;
}
};
}
var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });
在內(nèi)部定義的匿名函數(shù)中,訪問了外部函數(shù)中的變量propertyName。即使這個匿名函數(shù)被返回賦值給另一個變量,在其他地方通過這個變量調(diào)用了這個匿名函數(shù),它仍然可以訪問propertyName這個變量。這是因為這個匿名函數(shù)的作用域鏈里包含著createComparisonFunction()的作用域。
普通函數(shù)的作用域鏈
unction compare(value1, value2){
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
在這里我們先定義了compare()函數(shù),然后又在全局作用域中調(diào)用了它。
當(dāng)某個函數(shù)被調(diào)用時,會創(chuàng)建一個執(zhí)行環(huán)境及相應(yīng)的作用域鏈。每一個執(zhí)行環(huán)境都有一個表示變量的對象(變量對象),全局的變量對象始終存在,而像函數(shù)這樣的局部環(huán)境的變量對象則只存在于函數(shù)執(zhí)行的過程中。
在創(chuàng)建compare()時,會預(yù)先創(chuàng)建一個包含全局變量對象的的作用域鏈,這個作用域鏈被保存在內(nèi)部的[[Scope]]屬性中。
當(dāng)調(diào)用compare()時,函數(shù)的執(zhí)行環(huán)境就被創(chuàng)建了,并通過復(fù)制函數(shù)[[Scope]]的對象構(gòu)建起該執(zhí)行環(huán)境的作用域鏈。接下來會創(chuàng)建這個函數(shù)的活動對象,這個對象包括arguments和其他命名參數(shù),活動對象在此作為這個局部執(zhí)行環(huán)境的變量對象被推入作用域鏈的最前端。作用域鏈本質(zhì)上就是一個指向變量對象的指針列表。
在函數(shù)中訪問一個變量時,就會在作用域鏈中搜索具有相應(yīng)名字的變量。對于一般的函數(shù),在執(zhí)行完畢后活動對象(局部變量對象)就會被銷毀。內(nèi)存中僅保留全局執(zhí)行環(huán)境的變量對象。
閉包的作用域鏈
在一個函數(shù)內(nèi)部定義的函數(shù)會將外部函數(shù)的活動對象添加到它的作用域鏈中。
以上面的例子來說
var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });
當(dāng)匿名函數(shù)從createComparisonFunction()返回到compare變量中時同樣要創(chuàng)建一個作用域鏈,這個作用域鏈里不僅包含全局執(zhí)行環(huán)境的變量對象,還包含createComparisonFunction()的活動對象。
這就意味著匿名函數(shù)可以訪問createComparisonFunction()中定義的所有變量。且在createComparisonFunction()執(zhí)行完之后,其執(zhí)行環(huán)境的作用域鏈被銷毀了,但是其活動對象并不像往常一樣會被銷毀,而是還留存在內(nèi)存中。因為這時匿名函數(shù)的作用域鏈仍在引用這個對象。所以直到匿名函數(shù)被銷毀,createComparisonFunction()的活動對象才會被消滅。
compareNames = null;
閉包與變量
閉包所保存的是外層函數(shù)的整個變量對象,也就是說,閉包只能外面的變量的最終一個值。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
var a = createFunctions();
alert(a[1]()); //10
按理說每個函數(shù)都應(yīng)該返回自己的索引數(shù)呀。但是對不起,所有匿名函數(shù)中保存的createFunctions的活動對象都是一個哦,所以i也是一個哦,最后i變?yōu)榱?0.所以所有匿名函數(shù)訪問i時都會得到10。
你可以這樣強制:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
return result;
}
var a = createFunctions();
alert(a[1]()); //1
由于i到num是值傳遞的,所以對于每一個result[i],num是不同的。這樣每次調(diào)用時訪問的就是對于每一個元素都不同的num變量了。
關(guān)于this對象
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"
每個函數(shù)在被調(diào)用時都會自動取得兩個特殊變量,this和arguments。函數(shù)在搜索這兩個變量的時候只搜索到自己的活動對象為止,并不在作用域鏈中搜索(其實這很好想,就相當(dāng)于自己的執(zhí)行環(huán)境里有這兩個變量,為啥還要往上找。)匿名函數(shù)最后是在全局變量中執(zhí)行的,所以this指向全局環(huán)境。
var name = "The Window";
var object = {
name : "My Object",
getName: function(){
return this.name;
};
}
object.getName(); //"My Object"
(object.getName)(); //"My Object"
(object.getName = object.getName)(); //"The Window"
有時即使是細(xì)微的變化也會引起this的值的變化。這里后兩個例子不太明白。
內(nèi)存泄漏
這個主要是因為IE9之前對于JS對象和BOM/DOM對象采用不同的垃圾回收機制造成的。對DOM元素使用的引用計數(shù)方法無法應(yīng)對循環(huán)引用的情況。
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}
在這里element對匿名函數(shù)有引用,匿名函數(shù)引用著assignHandler()的活動對象,其中就包含著element,啊哦。
所以。。。。。拋棄IE吧!
好吧。。。。可以這樣改。。。
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
};
element = null;
}
僅僅把閉包中的element移出去是木有用噠,閉包包含著外面函數(shù)的活動對象哦,element的引用還在,所以要手工減掉element對DOM的引用,對匿名函數(shù)的引用也就沒了。
模仿塊級作用域
對于多次聲明同一個變量,JS不會阻止你這樣做,而且如果你在多次的聲明中執(zhí)行了變量初始化,JS會照做不誤。這就帶來了很大的問題,你自己的代碼可能比較了解,但是當(dāng)你使用了框架或別人的JS時,天知道你們的變量名有沒有重復(fù)的!
所以,模仿塊級作用域的需求就來了。
看看是怎么演變來的
var someFunction = function(){
//塊級作用域
};
someFunction();
我們是可以用時機的值替換變量名的
function(){
//塊級作用域
}(); //報錯
這里會報錯是因為JS將function視作一個函數(shù)聲明的開始,函數(shù)聲明后面不能跟圓括號。那么我們將它轉(zhuǎn)換為函數(shù)表達式吧
(function(){
//塊級作用域
})();
私有變量
任何在函數(shù)中定義的變量就是私有變量,因為外部訪問不到?,F(xiàn)在我們有了閉包,閉包可以被返回到作用域外面,而通過閉包又可以訪問作用域里的變量和方法。這樣我們就可以來模仿私有變量和公有方法了。
function MyObject(){
var privateVariable = 10;
function privateFunction(){
return false;
}
this.publicMethod = function (){
privateVariable++;
return privateFunction();
};
}
privateVariable和privateFunction()只有通過publicMethod方法才能訪問。
使用這樣的方法,我們可以隱藏那些不能被修改的數(shù)據(jù)。
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"
這里如果我們只提供get方法,name就不會被改變。都不提供那name對于對象的使用者來說就是透明的。
但是在構(gòu)造函數(shù)中定義特權(quán)方法有個問題,那就是你必須使用構(gòu)造函數(shù)模式來達到這個目的,也就是說對于每一個實例都會創(chuàng)造一組新的方法。
靜態(tài)私有變量
(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");
alert(person1.getName()); //"Nicholas"
person1.setName("Greg");
alert(person1.getName()); //"Greg"
var person2 = new Person("Michael");
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"
靜態(tài)私有變量創(chuàng)建了一個私有作用域,其中Person這個變量定義時沒有通過var聲明,這樣它就是個全局變量。于是在它原型里的方法在私有作用域外也可以訪問到。但這里name屬性是所有實例共享的。
模塊模式
為單例創(chuàng)建私有變量和特權(quán)方法的模式,單例是只有一個實例的對象:
var singleton = {
name : value,
method : function () {
}
};
模塊模式為單例添加私有變量和特權(quán)方法:
var application = function(){
var components = new Array();
components.push(new BaseComponent());
return {
getComponentCount : function(){
return components.length;
},
registerComponent : function(component){
if (typeof component == "object"){
components.push(component);
}
}
};
}();
在這里,最后返回的單例對象是在匿名函數(shù)里創(chuàng)建的,所以這里面的方法可以訪問到匿名函數(shù)里的私有變量。外界只能通過著兩個方法有限的修改components,只能獲取長度和添加。但是這里的單例是Object對象。
增強的模塊模式
這種模式適合要返回的單例是特定類型的,本質(zhì)上和普通的模塊模式?jīng)]啥區(qū)別。
var application = function(){
var components = new Array();
components.push(new BaseComponent());
var app = new BaseComponent();
app.getComponentCount = function(){
return components.length;
};
app.registerComponent = function(component){
if (typeof component == "object"){
components.push(component);
}
};
return app;
}();
這樣app對象就滿足了必須是某個類型的單例的要求。