EcmaScript 6 - 塊級作用域(block scope)

1. EcmaScript 5作用域

EcmaScript5的作用域有全局作用域(global scope)與函數(shù)作用域(function scope)兩種。

1.1 全局作用域

在全局作用域中定義的變量,在整個上下文中都是可以訪問的。

var msg = 'Hello world';
console.log(msg); // Hello world
function sayHi(){
    console.log(msg);// Hello world
}
sayHi();

上面例子中msgsayHi()函數(shù)內(nèi)外都可以訪問。
在NodeJs中,在js文件中直接使用var關(guān)鍵字聲明的變量,將會在模塊上聲明。
在瀏覽器中,在script標簽中直接使用var關(guān)鍵字聲明的變量,將定義在全局變量window上,window對象中的屬性擁有全局作用域。

在嚴格模式(strict mode)下,變量在初始化前必須聲明,否則會拋出ReferenceError

'use strict';
a = 2;
console.log(a); // Uncaught ReferenceError: a is not defined

在非嚴格模式下,使用未聲明的變量將隱式聲明為全局變量,定義在window上

a = 2;
console.log(a); // 2
console.log(a === window.a) // true

1.2 函數(shù)作用域

在函數(shù)作用域中定義的變量,只能在函數(shù)中被訪問

function fn(){
    var a = 1;
    console.log(a); // 1
}
console.log(a); // Uncaught ReferenceError: a is not defined

2. 聲明提升

聲明提升(hoisting)指通過var關(guān)鍵字聲明的變量,將提升到函數(shù)(或者全局作用域)的頂部進行聲明,與聲明語句的實際位置無關(guān)。

function sayHi(condition){
    if(condition){
        var msg = 'Hello world';
        console.log(msg); // Hello world
    }else{
        console.log(msg); // undefined
    }
}

sayHi(true);
sayHi(false);

上面例子中msgif語句中進行了聲明和初始化賦值,但實際上msg將“提升”到函數(shù)頂部進行聲明,而賦值的位置不變,還在if語句中。因此在else語句中同樣可以訪問msg變量,其值為undefined。Javascript引擎會將上面的代碼處理成類似下面的樣子。

function sayHi(condition){
    var msg;
    if(condition){
        msg = 'Hello world';
        console.log(msg); // Hello world
    }else{
        console.log(msg); // undefined
    }
}

sayHi(true);
sayHi(false);

3. 塊級作用域聲明

EcmaScript 6引入了塊級作用域(block scope),塊級作用域只能在塊中被訪問,以下兩種情況可以創(chuàng)建塊級作用域的變量。

  • 在函數(shù)中
  • 在被{}包裹的塊中

3.1 let聲明

let關(guān)鍵字的作用類似var,用來聲明變量,不同的是其聲明的變量具有塊級作用域。

'use strict';
function sayHi(condition){
    if(condition){
        let msg = 'Hello world';
        console.log(msg); // Hello world
    }else{
        console.log(msg); // ReferenceError: msg is not defined
    }
}

sayHi(true);
sayHi(false);

如上面例子,msg只能在if語句塊中被訪問,在else中訪問會產(chǎn)生ReferenceError錯誤

3.2 不能重復聲明

使用var關(guān)鍵字聲明的變量,在同一個作用域可以重復進行聲明,后面的將會覆蓋前面的。而使用let關(guān)鍵字聲明的變量,在同一個作用域不能重復聲明(不管之前是用varlet, 或者const聲明),否則將會觸發(fā)SyntaxError錯誤。

'use strict';
var a = 1;
var a = 2;

//var b = 1;
//let b = 2; // SyntaxError: Identifier 'b' has already been declared

//let c = 1; 
//let c = 2; // SyntaxError: Identifier 'c' has already been declared

const d = 1;
let d = 2; // SyntaxError: Identifier 'd' has already been declared

在變量包含的作用域是用let關(guān)鍵字聲明的變量,不會生成錯誤。如下面代碼,在if語句中,新的變量a將覆蓋外面的變量a。

'use strict';
let a = 1;

if(true){
    let a = 2;
    console.log(a); // 2
}
console.log(a); // 1

3.3 const聲明

使用const關(guān)鍵字聲明的變量將作為常量使用,一旦被賦值,將不能再被改變,因此const關(guān)鍵字聲明的變量必須同時進行初始化,否則會拋出SyntaxError錯誤

'use strict';
const maxItems = 30;

const name; // SyntaxError: Unexpected token

上面例子中的name變量沒有進行初始化,因此將觸發(fā)SyntaxError。

const關(guān)鍵字與let關(guān)鍵字相同的地方

  • 聲明的變量具有塊級作用域。
  • 在相同的作用域,重復聲明變量將會拋出SyntaxError錯誤

const關(guān)鍵字與let關(guān)鍵字不同的地方

  • const聲明的變量不能重復進行賦值, 否則將拋出TypeError錯誤

const聲明的對象不能改變,但是對象中的屬性可以進行改變。const綁定的是對象的引用,對象中實際的值是可以改變的。

'use strict';
const person = {
    name:'Mary'
};

person.name = 'Jim';
person = {
    name:'Bary'
}; // TypeError: Assignment to constant variable

3.4 臨時死區(qū)

使用letconst聲明的變量,只有在聲明之后才能夠使用,否則會觸發(fā)ReferenceError錯誤,即使在ES5中可以安全使用的typeof關(guān)鍵字在ES6中也不能保證可以安全使用。這種特殊行為就叫做臨時死區(qū)(Temporal Death Zone)。

'use strict';
if(true){
    console.log(typeof value); // ReferenceError: value is not defined

    const value = 'blue';
}

如上面代碼,由于臨時死區(qū)的存在,value變量在聲明之前是不能被訪問的。
Javascript引擎遇到一個代碼塊并且代碼塊中存在變量聲明,那么要么進行變量聲明提升(hoisting),將變量提升到函數(shù)或者全局作用域頂部進行聲明(使用var關(guān)鍵字);要么將變量放入臨時性死區(qū)(使用const或者let關(guān)鍵字),嘗試訪問臨時性死區(qū)中的變量將會導致ReferenceError錯誤。在遇到變量聲明(const或者let關(guān)鍵字)后,該變量將被移出臨時性死區(qū),變量就可以被訪問了。

'use strict';
console.log(typeof value); // undefined
if(true){
    let value = 'blue';
}

如上面代碼,value變量并未放入臨時性死區(qū),使用typeof關(guān)鍵字仍然是安全的。

4. 循環(huán)

4.1 循環(huán)的塊級作用域

實際開發(fā)中比較長使用的是在循環(huán)中使用塊級作用域,在Javascript中,var關(guān)鍵字在循環(huán)中使用有著很多不被其他語言開發(fā)者了解的缺陷。

for(var i=0;i<10;i++){
    // do something
}

console.log(i); // 10

例如上面代碼中,開發(fā)者希望達到的效果是i在循環(huán)之后不能訪問,但是由于Javascript會進行變量聲明“提升”,i在循環(huán)結(jié)束之后,仍然能夠訪問。
使用let關(guān)鍵字,可以有效避免這個問題。

for(let i=0;i<10;i++){
    // do something
}

console.log(i); // ReferenceError

4.2 函數(shù)的循環(huán)

使用var關(guān)鍵字創(chuàng)建的變量在循環(huán)內(nèi)部的函數(shù)中使用有一些問題。例如下面的代碼,開發(fā)者期待輸出從09,但是由于i在循環(huán)之后仍然能夠訪問,funcs中的每個函數(shù)引用的是同一個i,因此在調(diào)用后輸出了十次10。

var funcs = [];
for(var i = 0;i < 10;i++){
    funcs.push(function(){console.log(i);});
}

funcs.forEach(function(func){
    func(); // / outputs 10 ten times
});

上面的問題可以通過立即執(zhí)行函數(shù)(Immidiately Invoked Function Expressions,IIFEs)來解決

var funcs = [];
for(var i = 0;i < 10;i++){
    funcs.push((function(val){
        return function(){console.log(val);};
    })(i));
}

funcs.forEach(function(func){
    func(); // outputs 0, then 1, then 2, up to 9
});

如上面的代碼,使用立即執(zhí)行函數(shù)強制將i作為參數(shù)傳入到每個函數(shù)中,這樣每個函數(shù)保存的是循環(huán)過程中i的副本,因此可以正確輸出。

4.3 在循環(huán)中使用let

使用let關(guān)鍵字可以用比較簡單清晰的代碼解決上面的問題。

'use strict';
var funcs = [];
for(let i = 0;i < 10;i++){
    funcs.push(function(){console.log(i);});
}

funcs.forEach(function(func){
    func(); // outputs 0, then 1, then 2, up to 9
});

如上面代碼,使用let會在每次循環(huán)過程中,創(chuàng)建一個新的變量i,因此每個函數(shù)讀取到的是每次循環(huán)的i的副本,每個i的副本的值由循環(huán)初始化時i的值決定。
for infor of循環(huán)中,let關(guān)鍵字有同樣的特性。如下面代碼,在每次循環(huán)時創(chuàng)建一個新key綁定,這樣每次循環(huán)都一個新的key變量,因此每個函數(shù)輸出不同的key。如果使用var關(guān)鍵字代替let,那么所有函數(shù)將輸出相同的值c

var funcs = [],
    object = {
        a: true,
        b: true,
        c: true
    };

for (let key in object) {
    funcs.push(function() {
        console.log(key);
    });
}

funcs.forEach(function(func) {
    func();     // outputs "a", then "b", then "c"
});

注意:let關(guān)鍵字在循環(huán)中的特性在規(guī)范中定義,但是跟let關(guān)鍵字的“非提升”特性并不相關(guān)。實際上,早起的一些let實現(xiàn)并沒有實現(xiàn)上述在循環(huán)中的特性。這些特性是逐漸添加的。

4.4 在循環(huán)中使用const

EcmaScript 6規(guī)范并沒有在循環(huán)中使用const給予禁止,但是在不同的循環(huán)類型(for, for in, for of),其行為是不同的。在for循環(huán)中,const可以在循環(huán)初始化時使用,但是如果嘗試修改其值,那么將會拋出錯誤。如下面代碼,i在初始化時時沒問題的,但是在調(diào)用i++時,將會發(fā)生TypeError錯誤,因為++操作嘗試修改一個常量的值。

'use strict';

var funcs = [];
for(const i=0;i<10;i++){ // TypeError: Assignment to constant variable
    funcs.push(function(){
        console.log(i);
    });
}

for in, for of循環(huán)中使用const關(guān)鍵字則不會發(fā)生錯誤

'use strict';
var funcs = [],
    object = {
        a: true,
        b: true,
        c: true
    };

// doesn't cause an error
for (const key in object) {
    funcs.push(function() {
        console.log(key);
    });
}

funcs.forEach(function(func) {
    func();     // outputs "a", then "b", then "c"
});

如上面代碼,使用const關(guān)鍵字與let關(guān)鍵字的不同之處只在于,key變量在塊中不能被修改。由于在for in, for of循環(huán)中,每次遍歷都會綁定一個新的key而不是嘗試修改原來的key,因此const關(guān)鍵字在for in, for of中使用是沒問題的。

4.5 全局作用域

在全局作用域使用let或者const關(guān)鍵字與var是不同的。當在全局作用域使用var關(guān)鍵字時,創(chuàng)建一個新的全局變量,并將其綁定到全局對象的屬性上。因此,可以使用var覆蓋全局對象上已經(jīng)存在的變量。

// in a browser
var RegExp = "Hello!";
console.log(window.RegExp);     // "Hello!"

var ncz = "Hi!";
console.log(window.ncz);        // "Hi!"

上面的代碼中,window對象原有的RegExp被修改,ncz被定義為一個全局變量,并寫入全局對象的屬性上。
letconst關(guān)鍵字會創(chuàng)建在全局作用域創(chuàng)建一個變量,但是并不會寫入到全局對象的屬性。因此,letconst關(guān)鍵字創(chuàng)建的變量不會覆蓋全局變量,而只能創(chuàng)建一個優(yōu)先讀取的“影子”變量。

// in a browser
let RegExp = "Hello!";
console.log(RegExp);                    // "Hello!"
console.log(window.RegExp === RegExp);  // false

const ncz = "Hi!";
console.log(ncz);                       // "Hi!"
console.log("ncz" in window);           // false

如上面代碼,RegExpwindow.RegExp是不同的,ncz也并不在window對象上。因此,當開發(fā)者不需要在全局對象上創(chuàng)建變量,使用letconst關(guān)鍵字在全局作用域創(chuàng)建對象相比var是安全的。

5. 現(xiàn)有的最佳實踐

由于let關(guān)鍵字的特性更符合其他語言的習慣,不會產(chǎn)生var關(guān)鍵字導致的各種問題,因此盡量使用let關(guān)鍵字廣被開發(fā)者倡導。
由于大部分變量實際上不會改變,而修改變量會導致不可測的bug,因此對于那些不會發(fā)生改變的變量,要使用const關(guān)鍵字

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

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