(一)函數(shù)作用域
1. 函數(shù)中的作用域
函數(shù)作用域的含義是指,屬于這個(gè)函數(shù)的全部變量都可以在整個(gè)函數(shù)的范圍內(nèi)使用及復(fù)用(事實(shí)上在嵌套的作用域中也可以使用)。
2. 隱藏內(nèi)部實(shí)現(xiàn)
在任意代碼片段外部添加包裝函數(shù)业稼,可以將內(nèi)部的變量和函數(shù)定義“隱藏”起來,外部作用域無法訪問包裝函數(shù)內(nèi)部的任何內(nèi)容速勇,還可以避免同名標(biāo)識(shí)符之間的沖突截珍。
規(guī)避沖突的方法:
(1) 全局命名空間
一些常使用的第三方庫通常會(huì)在全局作用域中聲明一個(gè)名字足夠獨(dú)特的變量,通常是一個(gè)對(duì)象吞瞪。這個(gè)對(duì)象被用作庫的命名空間馁启,所有需要暴露給外界的功能都會(huì)成為這個(gè)對(duì)象(命名空間)的屬性,而不是將自己的標(biāo)識(shí)符暴漏在頂級(jí)的詞法作用域中尸饺。
var MyReallyCoolLibrary = {
awesome: "stuff",
doSomething: function() {
// ...
},
doAnotherThing: function() {
// ...
}
};
(2) 模塊管理
使用模塊管理器工具进统,任何庫都無需將標(biāo)識(shí)符加入到全局作用域中助币,而是通過依賴管理器的機(jī)制將庫的標(biāo)識(shí)符顯式地導(dǎo)入到另外一個(gè)特定的作用域中。
3. 函數(shù)作用域
(1) 函數(shù)聲明與函數(shù)表達(dá)式
區(qū)分函數(shù)聲明和表達(dá)式最簡(jiǎn)單的方法是看
function
關(guān)鍵字出現(xiàn)在聲明中的位置(不僅僅是一行代碼螟碎,而是整個(gè)聲明中的位置)眉菱。如果function
是聲明中的第一個(gè)詞,那么就是一個(gè)函數(shù)聲明掉分,否則就是一個(gè)函數(shù)表達(dá)式俭缓。
簡(jiǎn)單的說,不以function
開頭的函數(shù)語句就是函數(shù)表達(dá)式定義酥郭。
函數(shù)聲明和函數(shù)表達(dá)式之間最重要的區(qū)別是它們的名稱標(biāo)識(shí)符將會(huì)綁定在何處华坦。
// 函數(shù)聲明
function foo() {}
// 函數(shù)表達(dá)式
(function bar() {})
// 函數(shù)表達(dá)式
x = function hello() {}
if (x) {
// 函數(shù)表達(dá)式
function world() {}
}
// 函數(shù)聲明
function a() {
// 函數(shù)聲明
function b() {}
if (0) {
//函數(shù)表達(dá)式
function c() {}
}
}
(2) 匿名和具名
函數(shù)表達(dá)式可以是匿名的,而函數(shù)聲明則不可以省略函數(shù)名不从。
- 匿名函數(shù)
setTimeout( function() { // 匿名函數(shù)表達(dá)式
console.log("I waited 1 second!");
}, 1000 );
//匿名函數(shù)表達(dá)式書寫起來簡(jiǎn)單快捷惜姐,但也有幾個(gè)缺點(diǎn)需要考慮
//1. 匿名函數(shù)在棧追蹤中不會(huì)顯示出有意義的函數(shù)名,使得調(diào)試很困難椿息。
//2. 如果沒有函數(shù)名歹袁,當(dāng)函數(shù)需要引用自身時(shí)只能使用已經(jīng)過期的arguments.callee 引用,比如在遞歸中寝优。
// 另一個(gè)函數(shù)需要引用自身的例子条舔,是在事件觸發(fā)后事件監(jiān)聽器要解綁自身。
//3. 匿名函數(shù)省略了對(duì)于代碼可讀性/可理解性很重要的函數(shù)名乏矾。一個(gè)描述性的名稱可以讓代碼不言自明孟抗。
- 具名函數(shù)
行內(nèi)函數(shù)表達(dá)式非常強(qiáng)大且有用 —— 匿名和具名之間的區(qū)別并不會(huì)對(duì)這點(diǎn)有任何影響。始終給函數(shù)表達(dá)式命名是一個(gè)最佳實(shí)踐钻心。
setTimeout( function timeoutHandler() {
//給函數(shù)表達(dá)式指定一個(gè)函數(shù)名可以有效解決匿名函數(shù)表達(dá)式帶來的問題
console.log( "I waited 1 second!" );
}, 1000 );
(3) 立即執(zhí)行函數(shù)表達(dá)式
IIFE
凄硼,代表立即執(zhí)行函數(shù)表達(dá)式(Immediately Invoked Function Expression
)。
- 函數(shù)名對(duì)
IIFE
當(dāng)然不是必須的扔役,IIFE
最常見的用法是使用一個(gè)匿名函數(shù)表達(dá)式帆喇。
// IIFE 兩種常見的形式
(function (){ .. })();
// 第一個(gè)( ) 將函數(shù)變成表達(dá)式,第二個(gè)( ) 執(zhí)行了這個(gè)函數(shù)
// 另一個(gè)改進(jìn)形式
(function(){ .. }());
// 調(diào)用的() 括號(hào)被移進(jìn)了用來包裝的( ) 括號(hào)中
// 以上兩種形式在功能上是一致的亿胸,選擇哪個(gè)全憑個(gè)人喜好坯钦。
-
IIFE
的另一個(gè)非常普遍的進(jìn)階用法是把它們當(dāng)作函數(shù)調(diào)用并傳遞參數(shù)進(jìn)去。
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2
以上代碼中侈玄,將window
對(duì)象的引用傳遞進(jìn)去婉刀,但將參數(shù)命名為global
;當(dāng)然也可以從外部作用域傳遞任何你需要的東西序仙,并將變量命名為任何你覺得合適的名字
-
IIFE
還有一種變化的用途是倒置代碼的運(yùn)行順序突颊,將需要運(yùn)行的函數(shù)放在第二位,在IIFE
執(zhí)行之后當(dāng)作參數(shù)傳遞進(jìn)去。這種模式在UMD
項(xiàng)目中被廣泛使用律秃。
var a = 2;
(function IIFE( def ) {
def( window );
})(function def( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
});
函數(shù)表達(dá)式def
定義在片段的第二部分爬橡,然后當(dāng)作參數(shù)(這個(gè)參數(shù)也叫作def
)被傳遞進(jìn)IIFE
函數(shù)定義的第一部分中。最后棒动,參數(shù)def
(也就是傳遞進(jìn)去的函數(shù))被調(diào)用糙申,并window
傳入當(dāng)作global
參數(shù)的值。
(二)塊作用域
變量的聲明應(yīng)該距離使用的地方越近越好船惨,并最大限度地本地化柜裸。
1. with
用with
從對(duì)象中創(chuàng)建出的作用域僅在with
聲明中而非外部作用域中有效。有關(guān)with關(guān)鍵字的用法可查看《【你不知道的JavaScript】(一)作用域與詞法作用域》
2. try/catch
try/catch
的catch
分句會(huì)創(chuàng)建一個(gè)塊作用域粱锐,其中聲明的變量?jī)H在catch
內(nèi)部有效疙挺。
try {
undefined(); // 執(zhí)行一個(gè)非法操作來強(qiáng)制制造一個(gè)異常
}
catch (err) {
console.log( err ); // 能夠正常執(zhí)行!
}
console.log( err ); // ReferenceError: err not found
3. let
ES6
引入了新的let
關(guān)鍵字怜浅,提供了除var
以外的另一種變量聲明方式铐然;let
關(guān)鍵字可以將變量綁定到所在的任意作用域中(通常是{ .. }
內(nèi)部)。
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
console.log( bar ); // ReferenceError
(1) 垃圾收集
另一個(gè)塊作用域非常有用的原因和閉包及回收內(nèi)存垃圾的回收機(jī)制相關(guān)海雪。
(2) let
循環(huán)
for (var j=0; j<10; j++) {
console.log( j );
}
console.log( j ); // 10
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
// for 循環(huán)頭部的 let 不僅將 i 綁定到了 for 循環(huán)的塊中锦爵,
// 事實(shí)上它將其重新綁定到了循環(huán)的每一個(gè)迭代中,
// 確保使用上一個(gè)循環(huán)迭代結(jié)束時(shí)的值重新進(jìn)行賦值奥裸。
4. const
除了
let
以外,ES6
還引入了const
沪袭,同樣可以用來創(chuàng)建塊作用域變量湾宙,但其值是固定(常量)。之后任何試圖修改值的操作都會(huì)引起錯(cuò)誤冈绊。
var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在if 中的塊作用域常量
a = 3; // 正常!
b = 4; // 錯(cuò)誤!
}
console.log( a ); // 3
console.log( b ); // ReferenceError!
函數(shù)作用域和塊作用域的行為是一樣的侠鳄,可以總結(jié)為:任何聲明在某個(gè)作用域內(nèi)的變量,都將附屬于這個(gè)作用域死宣。
(三)提升
1. 先有雞還是先有蛋
正確的思考思路是伟恶,包括變量和函數(shù)在內(nèi)的所有聲明都會(huì)在任何代碼被執(zhí)行前首先被處理。
a1 = 2;
var a1;
console.log( a1 ); // 2
//↑上面代碼會(huì)發(fā)生以下處理
var a1; // 編譯
a = 21; // 執(zhí)行
console.log( a1 );
console.log( a ); // undefined
var a = 2;
//↑上面代碼會(huì)發(fā)生以下處理
var a; // 編譯
console.log( a );
a = 2; // 留在原地等待執(zhí)行
也就是說毅该,先有蛋(聲明)后有雞(賦值)博秫;只有聲明本身會(huì)被提升,而賦值或其他運(yùn)行邏輯會(huì)留在原地眶掌。
2. 注意事項(xiàng)
(1) 每個(gè)作用域都會(huì)進(jìn)行提升操作挡育;函數(shù)內(nèi)部的聲明會(huì)被提升到該函數(shù)中的最上方,而不是整個(gè)程序的最上方朴爬;
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
//↑上面代碼跟下面等同
function foo() {
var a;
console.log( a ); // undefined
a = 2;
}
foo();
(2) 函數(shù)聲明會(huì)被提升即寒,但是函數(shù)表達(dá)式卻不會(huì)被提升。
foo(); // 不是ReferenceError, 而是TypeError!
var foo = function bar() {
// ...
};
// ↑上面代碼中的變量標(biāo)識(shí)符`foo()`被提升并分配給所在作用域(在這里是全局作用域)
// 因此`foo()` 不會(huì)導(dǎo)致`ReferenceError`
// 但是`foo`此時(shí)并沒有賦值(如果它是一個(gè)函數(shù)聲明而不是函數(shù)表達(dá)式,那么就會(huì)賦值)母赵。
// `foo()`由于對(duì)`undefined`值進(jìn)行函數(shù)調(diào)用而導(dǎo)致非法操作逸爵,因此拋出`TypeError` 異常。
即使是具名的函數(shù)表達(dá)式凹嘲,名稱標(biāo)識(shí)符在賦值之前也無法在所在作用域中使用师倔。
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
這個(gè)代碼片段經(jīng)過提升后,實(shí)際上會(huì)被理解為以下形式:
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...self...
// ...
}
(3) 函數(shù)聲明和變量聲明都會(huì)被提升施绎。函數(shù)優(yōu)先 —— 函數(shù)會(huì)首先被提升溯革,然后才是變量。
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
↑以上代碼片段會(huì)被引擎理解為如下形式:
function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() {
console.log( 2 );
};
var foo
盡管出現(xiàn)在 function foo()...
的聲明之前谷醉,但它是重復(fù)的聲明(因此被忽略了)致稀,因?yàn)楹瘮?shù)聲明會(huì)被提升到普通變量之前。盡管重復(fù)的 var
聲明會(huì)被忽略掉俱尼,但出現(xiàn)在后面的函數(shù)聲明還是可以覆蓋前面的抖单。
小總結(jié)
- 我們習(xí)慣將
var a = 2;
看作一個(gè)聲明,而JavaScript
引擎將var a
和a = 2
當(dāng)作兩個(gè)單獨(dú)的聲明遇八,第一個(gè)是編譯階段的任務(wù)矛绘,而第二個(gè)則是執(zhí)行階段的任務(wù)。 - 無論作用域中的聲明出現(xiàn)在什么地方刃永,都將在代碼本身被執(zhí)行前首先進(jìn)行處理货矮。
- 形象地想象成所有的聲明(變量和函數(shù))都會(huì)被“移動(dòng)”到各自作用域的最頂端,這個(gè)過程被稱為提升斯够。
- 聲明本身會(huì)被提升囚玫,而包括函數(shù)表達(dá)式的賦值在內(nèi)的賦值操作并不會(huì)提升,會(huì)留在原地等待執(zhí)行读规。