【你不知道的JavaScript】(二)函數(shù)作用域和塊作用域

(一)函數(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/catchcatch 分句會(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 aa = 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í)行读规。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末抓督,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子束亏,更是在濱河造成了極大的恐慌铃在,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碍遍,死亡現(xiàn)場(chǎng)離奇詭異定铜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)雀久,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門宿稀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赖捌,你說我怎么就攤上這事祝沸“耄” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵罩锐,是天一觀的道長(zhǎng)奉狈。 經(jīng)常有香客問我,道長(zhǎng)涩惑,這世上最難降的妖魔是什么仁期? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮竭恬,結(jié)果婚禮上跛蛋,老公的妹妹穿的比我還像新娘。我一直安慰自己痊硕,他們只是感情好赊级,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著岔绸,像睡著了一般理逊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盒揉,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天晋被,我揣著相機(jī)與錄音,去河邊找鬼刚盈。 笑死羡洛,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的藕漱。 我是一名探鬼主播翘县,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼谴分!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起镀脂,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤牺蹄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后薄翅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沙兰,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年翘魄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鼎天。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡暑竟,死狀恐怖斋射,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤罗岖,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布涧至,位于F島的核電站,受9級(jí)特大地震影響桑包,放射性物質(zhì)發(fā)生泄漏南蓬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一哑了、第九天 我趴在偏房一處隱蔽的房頂上張望赘方。 院中可真熱鬧,春花似錦弱左、人聲如沸窄陡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泳梆。三九已至,卻和暖如春榜掌,著一層夾襖步出監(jiān)牢的瞬間优妙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工憎账, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留套硼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓胞皱,卻偏偏與公主長(zhǎng)得像邪意,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子反砌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容