ES6:let與const

塊級作用域

es5語法規則中,只有全局作用域和函數作用域,不存在塊級作用域(一個塊級作用域即爲一個{}內)。

這會導致很多場景不合理。

第一種場景,內層變量可能會覆蓋外層變量。

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

第二種場景,用來計數的循環變量泄露爲全局變量。

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

塊級作用域的應用:實際上使得原來廣泛應用的立即執行匿名函數(IIFE)不再必要。

            // IIFE 寫法
            ( function(){
                var tmp = `hello dengjing!`;
                // 該變量只在該匿名函數作用域內有效
            }() );

            // 塊級作用域 寫法
            {
                let tmp = `hello dengjing!`; 
            }

塊級作用域與函數聲明

        function test9(){
            function f(){ console.log(`I am outside!`) }

            ( function(){
                if(false){
                    // 重複聲明一次函數f
                    function f(){ console.log(`I am inside`); }
                }

                f();
            }() )
        }
        test9();

上面代碼,在ES5中運行結果:I am inside。因爲被聲明的函數會被提升到函數頭部,實際運行代碼如下:

        function test9(){
            function f(){ console.log(`I am outside!`) }

            ( function(){
                function f(){ console.log(`I am inside`); }
                if(false){
                    // 重複聲明一次函數f
                }

                f();
            }() )
        }
        test9();

ES6瀏覽器實現由如下規則:

1、允許在塊級作用域內聲明函數

2、函數聲明類似於var,即會提升到 全局作用域 或 函數作用域 的頭部

3、同時,函數聲明還會提升到所在的塊級作用域的頭部

上段代碼,在符合ES6的瀏覽器中都會報錯,其實際運行的代碼如下所示:

        function test9(){
            function f(){ console.log(`I am outside!`) }

            ( function(){
                var f = undefined;
                if(false){
                    // 重複聲明一次函數f
                    function f(){ console.log(`I am inside`); }
                }

                // console.log(f);  // undefined
                f();
            }() )
        }
        test9();   //Uncaught TypeError: f is not a function.

總結:考慮到環境導致的行爲差異太大,應避免在塊級作用域內聲明函數。如確有需求,也應寫成函數表達式的形式,而非函數聲明語句。

        // 函數聲明語句
        {
            let a = `hello dengjing!`;
            function f(){
                return a;
            }
        }

        // 函數表達式
        {
            let a = `hello dengjing!`;
            let f = function(){
                return a;
            }
        }

let

        function test5(){
            // snippet 1
            var a = [];
            for(var i = 0;i<10;i++){
                a[i] = function(){
                console.log(i);
                } 
            }
            a[6](); // => 10
        }
test5();

上面代碼:變量i是var聲明的,全局範圍有效。全局只有一個變量i,只是每次循環變量i都會重新賦值。而循環內,被賦給數組a函數內部的console.log(i)中的i指向全局變量i。也就是說,所有數組a的成員中的i指向的都是同一個i,導致運行時輸出的是最後一輪的i值,也就是10。

function test6(){
    // snippet 2
    {
        let n = 11;
        { let n = 4;let m  = 100; console.log(n);}
        // 4
        {console.log(n);}  
        // 11
    }

    let m  = 0;
    var a = [];
    let i = 9;   // location 1
    for(let i = 0,m = 10;i<10;i++){   // location 2
        // 使用let聲明循環變量i,相當於此處又重新聲明瞭一個變量i,let i ;
        // 使用let聲明循環變量i 類似於 {let i = 0;{let i }}
        a[i] = function(){
            console.log(i);
        } 
        // console.log(ff);       // 報錯:ff is not defined
        
        // console.log('m:',m);  // Uncaught ReferenceError: Cannot access 'm' before initialization
        let m = 20;
        console.log('m:',m);  // 20
    }
    a[6](); // => 6
};
test6();
// location 1、location 2、location 3 注意此3處let i的聲明。

let聲明的變量僅在塊級作用域內有效{}。

上面代碼:變量i是let聲明,當前的i只在本輪循環{}有效。so每一次循環的i其實都是一個新的變量。js引擎內部會記住上一輪循環的值,初始化本輪的變量i時,就在上一輪循環的基礎上進行計算。

attention:for循環有一個特別之處,那就是設置循環變量的那部分 是一個父作用域,而循環體內部是一個單獨的子作用域;可以理解爲 {let i = 0;{let i }}。for循環聲明的變量i 與 循環變量i 不在同一個作用域,而是有各自單獨的作用域。

        function test7(){
            // snippet 3
            var a = [];
            let i = 0,m = 10;
            for(;i<10;i++){ 
                a[i] = function(){
                    console.log(i);
                } 
            }
            console.log('a = '+a);
            console.log('i = '+i); // => 10
            a[6](); // => 10
        };

        test7();

1、不存在變量提升

console.log(bar); // Uncaught ReferenceError
let bar = 2;

2、暫時性死區

只要塊級作用域內存在let命令,它所聲明的變量就‘綁定’(binding)這個區域,不再受外部的影響。

ES6明確規定:如果區塊中存在let和const命令,則這個區塊對這些命令聲明的變量從一開始就形成封閉作用域。只要在聲明之前就使用這些變量,就會報錯。

總之,在代碼塊內,使用let命令聲明變量之前,該變量都是不可用的。在語法上稱爲“暫時性死區”(temporal dead zone,簡稱TDZ)。

        function test8(){
            if(true){
                // TDZ開始
                tmp = `abc`;      // ReferenceError
                console.log(tmp); // ReferenceError

                let tmp ; // TDZ結束
                console.log(tmp); // undefined

                tmp = '123';
                console.log(tmp); // 123
            }

            typeof x; // ReferenceError
            let x;

            // 如一個變量根本沒被聲明,使用typeof則不會報錯
            typeof undeclared_variable;   // undefined

            function bar(x = y,y = 2){
                return [x,y];
            }
            bar();   // 報錯

            function bar1(x = 2,y = x){
                return [x,y];
            }
            bar1();  // [2,2]

            var y = 1;
            var y = y;  // 不報錯
            let y = y;  // ReferenceError: x is not defined
            // 以上報錯也是因爲暫時性死區
        };
test8();

暫時性死區的本質就是:只要進入當前作用域,所要使用的變量就已經存在,但是不可獲取,只有等到聲明變量的那一行代碼出現後,纔可以獲取和使用該變量。

3、不允許重複聲明

let不允許在同一作用域內部重複聲明同一個變量。

            {
                { let insane = `hello world` }
                console.log(insane);  //報錯
            }

            {
                let insane = `hello world0`;
                { let insane = `hello world1` }
                console.log(insane);  //報錯
            }

const

const聲明一個只讀常量。一旦聲明就必須立即賦值,不能留後賦值,過後變量就不能再次賦值。

因此,對於const而言,只聲明不賦值就會報錯。

const作用域:只在聲明所在的塊級作用域內有效,這點與let相同。

const也存在以下特性:

1、不存在變量提升

2、暫時性死區

3、不允許重複聲明

        if(1){
            const MAX = 10;
        }
        //   console.log(MAX); // Uncaught ReferenceError: MAX is not defined

        if(1){
            // console.log(MAX);  // Uncaught ReferenceError: Cannot access 'MAX' before initialization
            const MAX = 10;
        }

        var message = `hello dengjing`;
        let age = 18;
        // Uncaught SyntaxError: Identifier 'message' has already been declared
        // const message = `hello dengjing`;
        // Uncaught SyntaxError: Identifier 'age' has already been declared
        // const age = 18;

const本質

const實際上保證的並不是變量的值不得改動,而是變量指向的那個內存地址不得改動。

我們知道,對於簡單數據類型(數值,字符串,布爾值等)而言,值就保存在變量指向的內存地址中。但是,對於複合數據類型(對象、數組等)而言,變量指向的內存地址保存的只是一個指針,而const只能保證這個指針是固定的,至於指針它指向的數據結構是不是可變的,這是完全無法控制的。

故而,當將一個對象聲明爲常量時必須當心。

        const foo = {};

        // 爲foo字面量對象添加一個屬性,成功
        foo.prop = `I am dengjing.`;
        console.log(foo.prop);

        // 將foo地址指針改變指向,報錯
        // foo = {};  // Uncaught TypeError: Assignment to constant variable.
        const a = [];
        a.push(`hello`);
        a.length = 0;
        // a = ['kelly']; // Uncaught TypeError: Assignment to constant variable.

如真想將對象凍結,應使用Object.freeze方法。

對象凍結後,添加新屬性時不起作用,嚴格模式時甚至還會報錯。

        const foo = Object.freeze({});
        // 常規模式時,下行代碼不起作用
        // 嚴格模式時,下行報錯
        foo.prop = `I am dengjing.`;
        console.log(foo.prop);  // undefined

除了將對象本身凍結,對象的屬性也應該凍結。徹底凍結對象函數如下:

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章