ES6 學習筆記之一 塊做用域與let和const

在學習ES6的塊做用域和 let、const 以前,咱們先來看看ES5之前的 var 關鍵字。chrome

var 關鍵字用於定義一個變量,一般咱們會將其與變量的賦值合併爲一條語句,就像下面這樣(例1):編程

var age = 30;

但實際狀況是有些微妙的。閉包

在JavaScript中,變量的定義與否,雖然不像強類型語言那樣重要,但也仍是有所不一樣的。app

變量未定義,是一種未捕獲類型的錯誤,輸出的結果是變量未定義,同時終止後續腳本的執行,示例以下(例2):編程語言

console.log(age);

輸出:函數

age is not defined.

已經定義但未賦值的變量,輸出的結果是undefined(這個undefined不是未定義,是指變量的值爲undefined,這是JavaSript中的一個特殊的值)。示例以下(例3):學習

var age;
console.log(age);

輸出:spa

undefined

var 定義變量語句,在JavaScript中會被編譯器提高到整個做用域內有效,不管它寫在腳本的哪一個位置。示例以下(例4):firefox

console.log(age);
var age;

輸出code

undefined

這個待遇只有 var 定義語句都有,賦值語句是不會被提高的。當咱們在代碼中寫下一條定義的同時賦值的 var 語句時,編譯器會將其拆分爲定義語句和賦值語句,並將定義語句提高,而賦值語句仍然只對其後的代碼有效。

請看下面的例子(例5):

console.log(age);
var age = 30;
console.log(age);

它與以下例子的輸出是一致的(例6):

var age;
console.log(age);
age = 30;
console.log(age);

輸出結果:

undefined
30

咱們還知道,JavaScript中變量能夠不定義就使用,好比(例7):

age = 30;
console.log(age);

輸出:

30

這種直接賦值的語句,與定義的同時賦值,仍是有所不一樣。參見下面的例子(例8)

console.log(age);
age = 30;
console.log(age);

輸出結果:

ReferenceError: age is not defined

之因此一例5有這麼大的區別,在於這裏只有變量賦值,沒有 var 定義部分。編譯器不會自動補充一個 var 定義並提高(或者說編譯器補充了一個隱含的 var 定義,可是這個隱含的 var 定義不會被提高),所以第一個console.log語句會直接報出變量未定義的錯誤,以後的變量賦值和輸出語句不被執行。

爲避免誤用未定義的變量,在使用變量前,要養成在使用變量前用 var 進行定義的好習慣:)。除非在函數做用域內要使用父做用域的變量,這也是閉包存在的根源。

若是可能,可使用嚴格模式,即在腳本做用域的第一行,寫下 "use strict"(注意要帶着又引號哦)。這樣作的一個好處是,WebStorm 這樣的 IDE 會在爲未定義的變量賦值時,直接給出警告。

塊做用域

塊做用域是指用一對花括號括住的語句塊使用域。在ES5之前,是沒有塊做用域的,它只有函數做用域,所以 var 在非函數做用域的括號內,與在括號外,沒有區別。爲了保持向下兼容,在ES6中,var 在塊做用域中仍然保持其原有性狀。以下例(例9):

console.log(age);
{
    var age = 30;
}
console.log(age);

未定義就賦值的變量,也與原來的表現一致(例10)

{
    age = 30;
}
console.log(age);

要使塊做用域別有不同的意義,就須要 let 和 const,這兩個關鍵字了。

 let 關鍵字與 var 的做用相似,都是定義變量,也均可以在定義變量的同時,爲變量賦值。不一樣之處有如下兩點:

第一,let 能夠定義(僅在)塊做用域內(有效)的變量。以下例所示(例11):

{
    let age = 30;
}
console.log(age);

輸出結果爲:

ReferenceError: age is not defined

對照例9,能夠看出區別所在。

第二,let 定義變量語句,不會被提高到整個做用域有效。見下例(例12):

console.log(age);
let age = 30;

輸出結果爲:

ReferenceError: age is not defined

對照例4,能夠看出區別所在。

還有一個細節,let 和 var 都有在子做用域內定義與父做用域同名變量時,屏蔽父做用域同名變量的做用。

見以下兩例:

(例13)

var age = 20;
(function () {
    var age = 30;
    console.log(age);
})();
console.log(age);

(例14)

var age = 20;
{
    let age = 30;
    console.log(age);
}
console.log(age);

輸出結果均爲:

30
20

區別在於,若是在子做用域中,定義同名變量以前就使用該變量,對於 var 而言,因爲其會被提高,所以獲得的是undefined。對於let,不會被提高,卻也不會使用父做用域的變量,而是報出引用錯誤。示例以下:

(例15)

var age = 20;
(function () {
    console.log(age);
    var age = 30;
})();

輸出:

undefined

(例16)

var age = 20;
{
    console.log(age);
    let age = 30;
}

輸出:

ReferenceError: age is not defined

 還要說明一點,在ES6中,全部具備語句塊含義花括號括住的部分都構成塊做用域,包括單純的語句塊、if語句塊、while語句塊、for語句塊、break語句塊和函數語句塊。

在 for 的循環變量定義,會在每次迭代中執行一遍,就至關於每次迭代定義一個變量,迭代結束銷燬,循環多少次,生成多少個不一樣的循環變量。 這個特色的實際意義,能夠用下面的兩個示例說明。

(例17)

var i;
var fn = [];
for (i = 0; i < 3; i++) {
    fn.push(function () {
        console.log(i);
    });
}
fn[0]();
fn[1]();
fn[2]();

可能會預期其輸出結果爲:

0
1
2

實際輸出結果爲:

3
3
3

(例18)

var fn = [];
for (let i = 0; i < 3; i++) {
    fn.push(function () {
        console.log(i);
    });
}
fn[0]();
fn[1]();
fn[2]();

輸出結果爲:

0
1
2

在沒有塊做用域和let關鍵字以前,要實現一樣效果,須要利用閉包(例19)

var i;
var fn  = [];
for (i = 0; i < 3; i++) {
    fn.push((function (i) {
        return function () {
            console.log(i);
        }
    })(i));
}
fn[0]();
fn[1]();
fn[2]();

附帶說一句:for 語句塊的循環變量賦值部分的let定義,做用域很特殊,是循環體做用域的父做用域。

例20:

for (let i = 0; i < 3; i++) {
    let i = "s";
    console.log(i);
}

輸出結果爲:

s
s
s

輸出三個 s ,說明循環變量 i 保持了從 0 到 3 的自增,而循環體內的 i 屏蔽了循環變量 i ,因此會輸出 s。

因爲 let 相較於 var 優點明顯,又沒有明顯的缺點,在ES6兼容環境下,能夠徹底拋棄 var,而用 let 代替。

《你不知道的JavaScript》中,提倡C語言風格的 let 使用方法,即在做用域的起始處使用 let 定義做用域內的全部變量,這樣能夠避免變量未定義就使用的狀況發生。

我的以爲仍是按現代編程語言就近聲明的慣例更合適,能夠避免無心識的誤用,並且在閱讀時也不須要在代碼間跳躍以尋找變量定義和賦值。不過若是能保持代碼塊的簡短,又能堅持同一變量在同一做用域內不做兩用的原則,其實放在哪兒都可有可無。

對於 const,其實沒有太多須要說明的,常量定義而已。

PS:

還有一個細節:變量在使用時,可能處於如下四種狀態:未聲明、已聲明未初始化、已初始化、已賦值。這裏的初始化,指的是默認賦值undefined。

var 和 let 語句,在聲明變量的同時,若是沒有明確給變量賦值,則被初始化爲undefined,以下兩例能夠證實:

(例21)

var age;
console.log(age);

(例22)

let age;
console.log(age)

兩例均輸出:

undefined

前面提到 var 語句會被提高,指的就是初始化這個功能,let 的初始化功能不會被提高。

可是 var 和 let 的變量聲明功能,則是在整個做用域內生效的。即無論 var 和 let 處於做用域內什麼位置,所聲明的變量在整個做用域內都是已聲明狀態。沒有使用 var 和 let 聲明直接賦值的變量,則沒有此待遇,在其賦值前,變量未聲明。

下面舉例說明:

未聲明(例23)

console.log(age);
age = 30;
console.log(age);

輸出結果爲:

ReferenceError: age is not defined

已聲明未初始化(例24):

console.log(age);
let age = 30;
console.log(age);

firefox 輸出:

ReferenceError: can't access lexical declaration `a' before initialization

chrome 的輸出比較含混:

ReferenceError: age is not defined

已初始化(例25)

console.log(age);
var age = 30;
console.log(age);

輸出:

undefined
30

對於已聲明未初始化和未聲明,能夠用 typeof 運算符來作區分,未聲明的變量 typeof 的結果爲 undefined,而對於已聲明未初始化的變量,typeof 則會拋出 ReferenceError。

未聲明(例26)

console.log(typeof age);
age = 30;
console.log(typeof age);

輸出結果爲:

undefined
number

已聲明未初始化(例27)

console.log(typeof age);
let age = 30;
console.log(typeof age);

chrome 輸出:

ReferenceError: age is not defined

firefox 輸出:

ReferenceError: can't access lexical declaration `a' before initialization

對於未聲明和已聲明且已初始化爲 undefined 的變量,typeof 就力不從心了,由於 typeof undefined 的結果也是 undefined,和未聲明變量的 typeof 結果相同。見下例(例28)

console.log(typeof age);
age = 30;
console.log(typeof name);
var name;

輸出:

undefined
undefined

還想區別,就用以下方式(例29):

console.log(typeof age && undefined === age);

對於已聲明且已初始化爲 undefined 的變量,輸出結果爲 true。

不然,chrome 拋出 ReferenceError: age is not undefined,firefox 拋出 ReferenceError: can't access lexical declaration `a' before initialization。

相關文章
相關標籤/搜索