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]);
                }
            })
        };

 

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