ES6(二: 變數作用域)

Justin0223發表於2017-09-24

let命令

首先來看一個例子

if(true){
    var a =3;
}
console.log(a); // 3

if(true){
    let b =3;
}
console.log(b); // ReferenceError: b is not defined

這是babel編譯結果

if (true) {
    var a = 3;
}
console.log(a); // 3

// babel 將塊級宣告成不同的變數名
if (true) {
    var _b = 3;
}
console.log(b); // ReferenceError: b is not defined

上面程式碼在程式碼塊之中,分別用 let 和 var 宣告瞭兩個變數。然後在程式碼塊之外呼叫這兩個變數,結果 let 聲
明的變數報錯, var 宣告的變數返回了正確的值。這表明, let 宣告的變數只在它所在的程式碼塊有效。
for 迴圈的計數器,也合適使用 let 命令。for(){}

再看一個比較經典的列子

var a =[];

for(var i =0;i<10;i++){
    a[i] = function(){
        console.log(i);
    }
}
// 不管你這裡是2 3 4 5 6,輸出都一樣
a[3](); // 10
a[6](); // 10

對於這種情況,其實沒let之前,我們也是可以處理的。大概樣子就是這樣

var a =[];

for(var i =0; i<10; i++){
    (function(i){ // 通過匿名函式自調
        a[i] = function(){
            console.log(i);
        }
    })(i)
}
a[3](); // 3
a[6](); // 6

上面程式碼,通過一次匿名函式自調,將i的值,做了一次儲存操作,至於為什麼,最後總結會說。

現在我們來看使用let

var a =[];

for(let i =0; i<10; i++){
    a[i] = function(){
        console.log(i);
    }
}
a[3](); // 3
a[6](); // 6

為什麼會輸出這樣,我們來看下babel的編譯結果

"use strict";

var a = [];

var _loop = function _loop(i) { // 使用一個函式包裝
    a[i] = function () {
        console.log(i);
    };
};

for (var i = 0; i < 10; i++) {
    _loop(i);
}
a[3](); // 3
a[6](); // 6

上面程式碼,其實和我們處理方案一樣,都是通過一個函式包裝,儲存了變數。

(1) 不存在變數提升

我們來看一個宣告提前的。

console.log(a);
var a = 3; // undefined

上面程式碼其實是這樣

var a;
console.log(a);
a = 3; // undefined

上面程式碼,是變數提升到作用域頂部,賦值留在本地。

我們來看let會怎麼樣

console.log(b); // ReferenceError: b is not defined
let b = 3;

babel官方的編譯結果,發現其實還有有點看不懂,貼出來吧

"use strict";

console.log(b); // undefined
var b = 3;

這裡就覺得有點坑。

(2) 暫時性死區

只要塊級作用域記憶體在 let 命令,它所宣告的變數就“繫結”(binding)這個區域,不再受外部的影響。

var tmp = 123;
if (true) {
    tmp = 'abc'; // ReferenceError: tmp is not defined
    let tmp;
}

上面程式碼中,存在全域性變數 tmp,但是塊級作用域內 let 又宣告瞭一個區域性變數 tmp,導致後者繫結這個塊級作用域,所以在 let 宣告變數前,對 tmp 賦值會報錯。

ES6 明確規定,如果區塊中存在 let 和 const 命令,這個區塊對這些命令宣告的變數,從一開始就形成了封閉作用域。

凡是在宣告之前就使用這些變數,就會報錯。

總之,在程式碼塊內,使用 let 命令宣告變數之前,該變數都是不可用的。這在語法上,稱為“暫時性死區”(temporal dead zone,簡稱 TDZ)。

PS
總之,暫時性死區的本質就是,只要一進入當前作用域,所要使用的變數就已經存在了,但是不可獲取,只有等到宣告變數的那一行程式碼出現,才可以獲取和使用該變數。

(3) 不允許重複宣告

let 不允許在相同作用域內,重複宣告同一個變數。

function f1() {
    let a = 10;
    var a = 11; // SyntaxError: Identifier 'a' has already been declared
}
// 報錯
function f2() {
    let a = 10;
    let a = 1;
}

因此,不能在函式內部重新宣告引數。

function func(arg) {
    let arg; // SyntaxError: Identifier 'arg' has already been declared
}
function func(arg) {
    {
        let arg; // 不報錯
    }
}

(4) 塊級作用域

ES5 只有全域性作用域和函式作用域,沒有塊級作用域,這帶來很多不合理的場景。
第一種場景,內層變數可能會覆蓋外層變數。

var tmp = new Date();
function f() {
    console.log(tmp);
    if (false) {
        var tmp = "hello world";
    }
} 
f(); // undefined

上面程式碼實際是這樣

var tmp = new Date();
function f() {
    var tmp;
    console.log(tmp);
    if (false) {
        tmp = "hello world";
    }
}
f(); // undefined

上面程式碼其實是一個常見的宣告提前的面試題,這裡涉及js的歷史問題,在es6之前,只有函式有作用域,如果是函式裡面,宣告提前會將變數宣告提前到函式頂部;若為全域性,會提升到全域性作用域頂部。

第二種場景,用來計數的迴圈變數洩露為全域性變數

var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5

上面程式碼中,變數 i 只用來控制迴圈,但是迴圈結束後,它並沒有消失,洩露成了全域性變數。

ES6的塊級作用域。

function f1() {
    let n = 5;
    if (true) {
        let n = 10;
    }
    console.log(n); // 5
}
f1();

babel編譯結果為:

"use strict";

function f1() {
    var n = 5;
    if (true) {
        var _n = 10; // 老套路,使用不一樣的變數宣告
    }
    console.log(n); // 5
}
f1();

上面的函式有兩個程式碼塊,都宣告瞭變數 n,執行後輸出 5。這表示外層程式碼塊不受內層程式碼塊的影響。如果使用 var 定義變數 n,最後輸出的值就是 10。因為var定義,if是沒作用域的。

ES6 允許塊級作用域的任意巢狀和內層定義外層同名變數。

{{{{
    let insane = 'Hello World';
    {let insane = 'Hello World'}
}}}};

babel編譯結果:

{
    {
        {
            {
                var insane = 'Hello World';
                {
                    var _insane = 'Hello World'; // 老套路了。
                }
            }
        }
    }
};

小結:

(1)在ES6之前,作用域分為兩種,一種是全域性作用域,一種是函式內部作用域,因為沒塊級作用域慨念,宣告提前就是很容易汙染全域性變數的問題。
(2)塊級作用域的出現,替代了匿名函式自調。儲存函式作用域的寫法。

(5) 塊級作用域與函式宣告

ES5 規定,函式只能在頂層作用域和函式作用域之中宣告,不能在塊級作用域宣告。

// 情況一
if (true) {
    function f() {}
}
// 情況二
try {
    function f() {}
} catch(e) {
}

但是,瀏覽器沒有遵守這個規定,還是支援在塊級作用域之中宣告函式,因此上面兩種情況實際都能執行,不會報錯。不過, ES5“嚴格模式”下還是會報錯。

ES6 引入了塊級作用域,明確允許在塊級作用域之中宣告函式。
並且 ES6 規定,塊級作用域之中,函式宣告語句的行為類似於 let,在塊級作用域之外不可引用。

function f() { console.log('I am outside!'); }
(function () {
    if (false) {
    // 重複宣告一次函式 f
    function f() { console.log('I am inside!'); }
    } 
    f();
}());

上面程式碼在 ES5 中執行,會得到“I am inside!”,因為在 if 內宣告的函式 f 會被提升到函式頭部。

考慮到環境導致的行為差異太大,應該避免在塊級作用域內宣告函式。如果確實需要,也應該寫成函式表示式,而不是函式宣告語句。

// 函式宣告語句
{
    let a = 'secret';
    function f() {
        return a;
    }
}
// 函式表示式
{
    let a = 'secret';
    let f = function () {
        return a;
    };
}

babel編譯結果

'use strict';

// 函式宣告語句
{
    var f = function f() {
        return a;
    };

    var a = 'secret';
}
// 函式表示式
{
    var _a = 'secret';
    var _f = function _f() {
        return _a;
    };
}

另外,還有一個需要注意的地方。

ES6 的塊級作用域允許宣告函式的規則,只在使用大括號的情況下成立,如果沒有使用大括號,就會報錯。

(5) const命令

const 宣告一個只讀的常量。一旦宣告,常量的值就不能改變。類似java的final static。
const 宣告的變數不得改變值,這意味著。

(1)const 一旦宣告變數,就必須立即初始化,不能留到以後賦值。
(2)const 的作用域與 let 命令相同:只在宣告所在的塊級作用域內有效。
(3)const 命令宣告的常量也是不提升,同樣存在暫時性死區,只能在宣告的位置後面使用。
(4)const 宣告的常量,也與 let 一樣不可重複宣告。
(5)對於複合型別的變數,變數名不指向資料,而是指向資料所在的地址。 const 命令只是保證變數名指向的地址不變,並不保證該地址的資料不變,所以將一個物件宣告為常量必須非常小心。

對最後一點舉一個例子

const foo = {};
foo.prop = 123;
// 123
foo = {}; // TypeError: "foo" is read-only

上面程式碼中,常量 foo 儲存的是一個地址,這個地址指向一個物件。
不可變的只是這個地址,即不能把 foo 指向另一個地址,但物件本身是可變的,所以依然可以為其新增新屬性。

下面是另一個例子

const a = [];
a.push('Hello'); // 可執行
a.length = 0; // 可執行
a = ['Dave']; // 報錯

上面程式碼中,常量 a 是一個陣列,這個陣列本身是可寫的,但是如果將另一個陣列賦值給 a,就會報錯。
如果真的想將物件凍結,應該使用 Object.freeze 方法。

const foo = Object.freeze({});
// 常規模式時,下面一行不起作用;// 嚴格模式時,該行會報錯
foo.prop = 123;

上面程式碼中,常量 foo 指向一個凍結的物件,所以新增新屬性不起作用,嚴格模式時還會報錯。
除了將物件本身凍結,物件的屬性也應該凍結。下面是一個將物件徹底凍結的函式。

var constantize = (obj) => {
    Object.freeze(obj);
    Object.keys(obj).forEach( (key, value) => {
        if ( typeof obj[key] === 'object' ) {
            constantize( obj[key] );
        }
    });
};

ES5 只有兩種宣告變數的方法: var 命令和 function 命令。
ES6 除了新增 let 和 const 命令,後面章節還會提到,另外兩種宣告變數的方法: import 命令和 class 命令。所以, ES6 一共有 6 種宣告變數的方法。

相關文章