函數(shù)名的提升
JavaScript引擎將函數(shù)名視同變量名,所以采用function命令聲明函數(shù)時(shí),整個(gè)函數(shù)會(huì)像變量聲明一樣,被提升到代碼頭部。所以,下面的代碼不會(huì)報(bào)錯(cuò)。
f();
function f() {}
表面上,上面代碼好像在聲明之前就調(diào)用了函數(shù)f。但是實(shí)際上,由于“變量提升”,函數(shù)f被提升到了代碼頭部,也就是在調(diào)用之前已經(jīng)聲明了。但是,如果采用賦值語句定義函數(shù),JavaScript就會(huì)報(bào)錯(cuò)。
f();
var f = function (){};
// TypeError: undefined is not a function
不能在條件語句中聲明函數(shù)
根據(jù)ECMAScript的規(guī)范,不得在非函數(shù)的代碼塊中聲明函數(shù),最常見的情況就是if和try語句。
if (foo) {
function x() {}
}
try {
function x() {}
} catch(e) {
console.log(e);
}
上面代碼分別在if代碼塊和try代碼塊中聲明了兩個(gè)函數(shù),按照語言規(guī)范,這是不合法的。但是,實(shí)際情況是各家瀏覽器往往并不報(bào)錯(cuò),能夠運(yùn)行。
函數(shù)作用域
作用域(scope)指的是變量存在的范圍。Javascript只有兩種作用域:
一種是全局作用域,變量在整個(gè)程序中一直存在,所有地方都可以讀??;
另一種是函數(shù)作用域,變量只在函數(shù)內(nèi)部存在。
在函數(shù)外部聲明的變量就是全局變量(global variable),它可以在函數(shù)內(nèi)部讀取。
在函數(shù)內(nèi)部定義的變量,外部無法讀取,稱為“局部變量”(local variable)。
函數(shù)內(nèi)部定義的變量,會(huì)在該作用域內(nèi)覆蓋同名全局變量。
對(duì)于var命令來說,局部變量只能在函數(shù)內(nèi)部聲明,在其他區(qū)塊中聲明,一律都是全局變量(例如if中)。
函數(shù)本身的作用域
函數(shù)本身也是一個(gè)值,也有自己的作用域。它的作用域與變量一樣,就是其聲明時(shí)所在的作用域,與其運(yùn)行時(shí)所在的作用域無關(guān)。
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
上面代碼中,函數(shù)x是在函數(shù)f的外部聲明的,所以它的作用域綁定外層,內(nèi)部變量a不會(huì)到函數(shù)f體內(nèi)取值,所以輸出1,而不是2。
總之,函數(shù)執(zhí)行時(shí)所在的作用域,是定義時(shí)的作用域,而不是調(diào)用時(shí)所在的作用域。
很容易犯錯(cuò)的一點(diǎn)是,如果函數(shù)A調(diào)用函數(shù)B,卻沒考慮到函數(shù)B不會(huì)引用函數(shù)A的內(nèi)部變量。
var x = function () {
console.log(a);
};
function y(f) {
var a = 2;
f();
}
y(x)
// ReferenceError: a is not defined
上面代碼將函數(shù)x作為參數(shù),傳入函數(shù)y。但是,函數(shù)x是在函數(shù)y體外聲明的,作用域綁定外層,因此找不到函數(shù)y的內(nèi)部變量a,導(dǎo)致報(bào)錯(cuò)。
同樣的,函數(shù)體內(nèi)部聲明的函數(shù),作用域綁定函數(shù)體內(nèi)部。
function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}
var x = 2;
var f = foo();
f() // 1
上面代碼中,函數(shù)foo內(nèi)部聲明了一個(gè)函數(shù)bar,bar的作用域綁定foo。當(dāng)我們?cè)趂oo外部取出bar執(zhí)行時(shí),變量x指向的是foo內(nèi)部的x,而不是foo外部的x。
參數(shù)傳遞方式
函數(shù)參數(shù)如果是原始類型的值(數(shù)值、字符串、布爾值),傳遞方式是傳值傳遞(passes by value)。這意味著,在函數(shù)體內(nèi)修改參數(shù)值,不會(huì)影響到函數(shù)外部。
var p = 2;
function f(p) {
p = 3;
}
f(p);
p // 2
上面代碼中,變量p是一個(gè)原始類型的值,傳入函數(shù)f的方式是傳值傳遞。因此,在函數(shù)內(nèi)部,p的值是原始值的拷貝,無論怎么修改,都不會(huì)影響到原始值。
但是,如果函數(shù)參數(shù)是復(fù)合類型的值(數(shù)組、對(duì)象、其他函數(shù)),傳遞方式是傳址傳遞(pass by reference)。也就是說,傳入函數(shù)的原始值的地址,因此在函數(shù)內(nèi)部修改參數(shù),將會(huì)影響到原始值。
var obj = {p: 1};
function f(o) {
o.p = 2;
}
f(obj);
obj.p // 2
上面代碼中,傳入函數(shù)f的是參數(shù)對(duì)象obj的地址。因此,在函數(shù)內(nèi)部修改obj的屬性p,會(huì)影響到原始值。
注意,如果函數(shù)內(nèi)部修改的,不是參數(shù)對(duì)象的某個(gè)屬性,而是替換掉整個(gè)參數(shù),這時(shí)不會(huì)影響到原始值。
var obj = [1, 2, 3];
function f(o){
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
上面代碼中,在函數(shù)f內(nèi)部,參數(shù)對(duì)象obj被整個(gè)替換成另一個(gè)值。這時(shí)不會(huì)影響到原始值。這是因?yàn)?,形式參?shù)(o)與實(shí)際參數(shù)obj存在一個(gè)賦值關(guān)系。
// 函數(shù)f內(nèi)部
o = obj;
上面代碼中,對(duì)o的修改都會(huì)反映在obj身上。但是,如果對(duì)o賦予一個(gè)新的值,就等于切斷了o與obj的聯(lián)系,導(dǎo)致此后的修改都不會(huì)影響到obj了。
某些情況下,如果需要對(duì)某個(gè)原始類型的變量,獲取傳址傳遞的效果,可以將它寫成全局對(duì)象的屬性。
var a = 1;
function f(p) {
window[p] = 2;
}
f('a');
a // 2
上面代碼中,變量a本來是傳值傳遞,但是寫成window對(duì)象的屬性,就達(dá)到了傳址傳遞的效果。
同名參數(shù)
如果有同名的參數(shù),則取最后出現(xiàn)的那個(gè)值。
閉包
函數(shù)內(nèi)部可以直接讀取全局變量。
但是,在函數(shù)外部無法讀取函數(shù)內(nèi)部聲明的變量。
如果出于種種原因,需要得到函數(shù)內(nèi)的局部變量。正常情況下,這是辦不到的,只有通過變通方法才能實(shí)現(xiàn)。那就是在函數(shù)的內(nèi)部,再定義一個(gè)函數(shù)。
function f1() {
var n = 999;
function f2() {
console.log(n); // 999
}
}
上面代碼中,函數(shù)f2就在函數(shù)f1內(nèi)部,這時(shí)f1內(nèi)部的所有局部變量,對(duì)f2都是可見的。但是反過來就不行,f2內(nèi)部的局部變量,對(duì)f1就是不可見的。這就是JavaScript語言特有的”鏈?zhǔn)阶饔糜颉苯Y(jié)構(gòu)(chain scope),子對(duì)象會(huì)一級(jí)一級(jí)地向上尋找所有父對(duì)象的變量。所以,父對(duì)象的所有變量,對(duì)子對(duì)象都是可見的,反之則不成立。
既然f2可以讀取f1的局部變量,那么只要把f2作為返回值,我們不就可以在f1外部讀取它的內(nèi)部變量了嗎!
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
上面代碼中,函數(shù)f1的返回值就是函數(shù)f2,由于f2可以讀取f1的內(nèi)部變量,所以就可以在外部獲得f1的內(nèi)部變量了。
閉包就是函數(shù)f2,即能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。由于在JavaScript語言中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取內(nèi)部變量,因此可以把閉包簡(jiǎn)單理解成“定義在一個(gè)函數(shù)內(nèi)部的函數(shù)”。閉包最大的特點(diǎn),就是它可以“記住”誕生的環(huán)境,比如f2記住了它誕生的環(huán)境f1,所以從f2可以得到f1的內(nèi)部變量。在本質(zhì)上,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來的一座橋梁。
閉包的最大用處有兩個(gè),一個(gè)是可以讀取函數(shù)內(nèi)部的變量,另一個(gè)就是讓這些變量始終保持在內(nèi)存中,即閉包可以使得它誕生環(huán)境一直存在。請(qǐng)看下面的例子,閉包使得內(nèi)部變量記住上一次調(diào)用時(shí)的運(yùn)算結(jié)果。
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
上面代碼中,start是函數(shù)createIncrementor的內(nèi)部變量。通過閉包,start的狀態(tài)被保留了,每一次調(diào)用都是在上一次調(diào)用的基礎(chǔ)上進(jìn)行計(jì)算。從中可以看到,閉包inc使得函數(shù)createIncrementor的內(nèi)部環(huán)境,一直存在。所以,閉包可以看作是函數(shù)內(nèi)部作用域的一個(gè)接口。
為什么會(huì)這樣呢?原因就在于inc始終在內(nèi)存中,而inc的存在依賴于createIncrementor,因此也始終在內(nèi)存中,不會(huì)在調(diào)用結(jié)束后,被垃圾回收機(jī)制回收。
閉包的另一個(gè)用處,是封裝對(duì)象的私有屬性和私有方法。
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = Person('張三');
p1.setAge(25);
p1.getAge() // 25
上面代碼中,函數(shù)Person的內(nèi)部變量_age,通過閉包getAge和setAge,變成了返回對(duì)象p1的私有變量。
注意,外層函數(shù)每次運(yùn)行,都會(huì)生成一個(gè)新的閉包,而這個(gè)閉包又會(huì)保留外層函數(shù)的內(nèi)部變量,所以內(nèi)存消耗很大。因此不能濫用閉包,否則會(huì)造成網(wǎng)頁的性能問題。
立即調(diào)用的函數(shù)表達(dá)式(IIFE)
在Javascript中,一對(duì)圓括號(hào)()是一種運(yùn)算符,跟在函數(shù)名之后,表示調(diào)用該函數(shù)。比如,print()就表示調(diào)用print函數(shù)。
有時(shí),我們需要在定義函數(shù)之后,立即調(diào)用該函數(shù)。這時(shí),你不能在函數(shù)的定義之后加上圓括號(hào),這會(huì)產(chǎn)生語法錯(cuò)誤。
產(chǎn)生這個(gè)錯(cuò)誤的原因是,function這個(gè)關(guān)鍵字即可以當(dāng)作語句,也可以當(dāng)作表達(dá)式。
為了避免解析上的歧義,JavaScript引擎規(guī)定,如果function關(guān)鍵字出現(xiàn)在行首,一律解釋成語句。因此,JavaScript引擎看到行首是function關(guān)鍵字之后,認(rèn)為這一段都是函數(shù)的定義,不應(yīng)該以圓括號(hào)結(jié)尾,所以就報(bào)錯(cuò)了。
解決方法就是不要讓function出現(xiàn)在行首,讓引擎將其理解成一個(gè)表達(dá)式。最簡(jiǎn)單的處理,就是將其放在一個(gè)圓括號(hào)里面。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
上面兩種寫法都是以圓括號(hào)開頭,引擎就會(huì)認(rèn)為后面跟的是一個(gè)表示式,而不是函數(shù)定義語句,所以就避免了錯(cuò)誤。這就叫做“立即調(diào)用的函數(shù)表達(dá)式”(Immediately-Invoked Function Expression),簡(jiǎn)稱IIFE。
注意,上面兩種寫法最后的分號(hào)都是必須的。如果省略分號(hào),遇到連著兩個(gè)IIFE,可能就會(huì)報(bào)錯(cuò)。
通常情況下,只對(duì)匿名函數(shù)使用這種“立即執(zhí)行的函數(shù)表達(dá)式”。它的目的有兩個(gè):
一是不必為函數(shù)命名,避免了污染全局變量;
二是IIFE內(nèi)部形成了一個(gè)單獨(dú)的作用域,可以封裝一些外部無法讀取的私有變量。
// 寫法一
var tmp = newData;
processData(tmp);
storeData(tmp);
// 寫法二
(function (){
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
上面代碼中,寫法二比寫法一更好,因?yàn)橥耆苊饬宋廴救肿兞俊?/p>