ES6标准入门(二)之let const命令

336 阅读1分钟

let 命令

  1. 基本用法
    ES6 新增了 let 命令,用于声明变量。用法和 var 类似,但是声明的变量只在 let 命令所在的代码块内有效。

    {
        let a = 1;
        var b = 2;
    }
    console.log(a) // ReferenceError: a is not defined
    console.log(b) // 2
    

    上面的代码在代码块中分别用了 letvar 命令声明变量,又在代码块之外调用了这两个变量,let 声明的变量输出报错,说明 let 声明的变量在代码块外无法访问。

    for 循环的计数器用 let 命令再合适不过了!

    for (let i = 0; i < 10; i++) {
        console.log(i) // 1 2 3 4 ... 10
    }
        console.log(i) // ReferenceError: i is not defined
    

    以上代码中的计数器 i 只在 for 循环体内有效,在循环体外引用就会报错。

    下面的题属于比较经典的一个面试题,可以很好的反应 let 和 var 的区别

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

    为什么会是这样的结果呢?

    • 当用 var 声明 i 变量时,i 在全局范围内都有效,所以全局只有一个变量 i,每一次循环 i 的值都会发生改变,在循环内被赋给数组 a 的函数内部输出的 i 是指向全局的 i,也就是说 a 数组成员中的 i 指向的都是同一个 i,导致运行时输出的是最后一轮的 i 值,也就是10。
    • 当用 let 声明 i 变量时,当前的 i 只在本轮循环有效,所以每一轮循环的 i 都是一个新变量,于是最后输出的是 6 另外,for 循环还有一个特别之处:设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
    for (let i = 0; i < 3; i++) {
        let i = 'abc';
        console.log(i); 
    }
    // abc
    // abc
    // abc
    

    正确运行以上代码并且输出三次 abc,表明函数内部变量 i 与循环变量 i 不在同一作用域!

    1. 不存在变量提升 var 命令会发生 变量提升 现象。
      什么叫做 变量提升 呢?变量提升指的是变量可以在声明之前调用,其值为undefined。其实按照一般的逻辑,这样是有点奇怪的,正常的顺序应该是先声明再使用。
      为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后才能使用,否则会报错!
    console.log(a); // undefined
    var a = 1;
    ---------------
    console.log(b);
    let b = 2; // 报错 ReferenceError
    

    在以上代码中,变量avar命令声明会发生变量提升,即脚本开始运行时,变量a已经存在,但是没有值,所以输出undefined。变量blet命令声明则不会发生变量提升。这表明在声明它之前,变量b是不存在的,这时如果用到它就会抛出一个错误。

    1. 暂时性死区 只要块级作用域内存在let命令,它所声明的变量就绑定这个区域,不再受外部影响!
    var temp = 123;
    if(1) {
        temp = 456; // ReferenceError
        let temp;
    }
    

    上面的代码中存在全局变量temp,但是块级作用域内let又声明了一个局部变量temp,导致后者绑定这个块级作用域,所以在let声明变量前对temp赋值会报错!
    ES6明确规定,如果块级作用域中存在letconst命令,则这个区块对这些命令声明的变量一开始就形成封闭的作用域,只要在声明之前就使用变量就会报错!
    总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上称为“暂时性死区”(temporal dead zone,简称TDZ)
    有些死区比较隐蔽,不太容易发现。

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

    以上代码之所以报错是因为参数x的默认值等于另一个参数y,而此时y还没有声明,属于死区。

    ES6规定暂时性死区和letconst语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。

    总之,暂时性死区的本质就是,只要进入当前作用域,所要使用的变量就已经存在,但是不可获取,只有等到声明变量的哪一行代码出现才可以获取和使用该变量。

    1. 不允许重复声明 let 不允许在相同作用域内重复声明同一个变量
    // 报错
    function () {
        let a = 10;
        var a = 1;
    }
    
    // 报错
    function () {
        let a = 10;
        let a = 1;
    }
    
    // 不能在函数内重复声明函数
    function func(arg) {
        let arg; // 报错
    }
    
    function func(arg) {
        {
            let arg; // 正常运行
        }
    }
    

    块级作用域

    1. 为什么需要块级作用域 ES5只有全局作用域和函数作用域,没有块级作用域,这导致很多场景不合理。
      第一种场景,内层变量可能会覆盖外层变量。
    var temp = new Date();
    function f() {
        console.log(temp);
        if(false){
            var temp = 'abc';
        }
    }
    f(); // undefined
    

    第二种场景,用来计数的循环变量泄露为全局变量。

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

    上面的代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,而是泄露成为了全局变量。

    1. ES6 的块级作用域 let实际上为 JavaScript 新增了块级作用域。
    function fn() {
        let n = 5;
        if(true) {
            let n = 10;
        }
        console.log(n); // 5
    }
    

    代码中两个变量n属于不同块,打印只会输出同一块下的该变量,如果用var声明变量n,则输出的结果为10。
    块级作用域的出行,实际上使得广泛应用的立即执行匿名函数(IIFE)不再必要了!

    1. 块级作用域与函数声明 你知道吗,这样声明函数在ES5中其实是非法的:
    // 方式一
    if (true) {
        function fn () {}
    }
    
    // 方式二
    try {
        function f () {}
    } catch (e) {}
    

    其实把代码敲进编译器会发现浏览器其实也能运行,并不会报错,其实是因为浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域中去声明函数!

    ES6中引入了块级作用域,明确允许在块级作用域中声明函数。ES6规定,在块级作用域中,函数声明语句的行为类似于let,块级作用域外不能引用!

    const 命令

    1. 基本用法 const声明一个制度的常量,一旦声明,常量的值就不能改变。
    const PI = 3.1415;
    console.log(PI); // 3.1415
    PI = 3; // Assignment to constant variable.
    

    表明改变常量的值会报错!

    const声明的常量不得改变值,这意味着,const一旦声明常量,就必须立即初始化,不能留到以后赋值,只声明不赋值也会报错。

    const的作用域和let命令相同:不可用重复声明,只在声明所在的块级作用域内有效,声明的常量也不会提升,同样存在暂时性死区,只能在声明后使用。

    1. 本质 const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单的数据类型(数值、字符串、布尔值)而言,值就保存在变量指向的内存地址中,因为等同于常量。但是对于复合型的数据(主要是对象和数组)而言,变量指向的内存地址保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,这完全不能控制,因此,将一个对象声明为常量时必须非常小心。
    const foo = {};
    
    // 为 foo 添加一个属性,可以成功
    foo.prop = 123;
    
    // 将 foo 指向另一个对象,报错
    foo = {}; // TypeError:"foo" is read-only
    

    上面的foo储存的是一个地址,这个地址指向一个对象,不可变的是它的地址,即不可用把foo指向另一个地址,但是对象本身是可变的,所以依然可以为其添加新属性。

    const a = [];
    a.push('Hello'); // 可以
    a.length = 0; // 可以
    a = ['Dave']; // 报错
    
    1. ES6声明变量的6种方法
    • ES5 只有两种声明变量的方法:var命令和function命令。
    • ES6 除了添加letconst命令,还有import命令和class命令

    顶层对象的属性

顶层对象在浏览器环境中指的是 window 对象,在 Node 环境中指的是 global 对象。在 ES5 中,顶层对象的属性与全局变量等价。

顶层对象的属性与全局变量相关,被认为是 JavaScript 语言中最大的设计败笔之一。这样的设计带来了几个很大的问题:

  • 无法在编译时就提示变量未申明的错误,只有运行时才能知道
  • 开发过程中很容易不知不觉就创建全局变量(比如打错字)
  • 顶层对象的属性到处都可以访问到,非常不利于模块化编程
  • window 对象是有实体含义的,指的是浏览器的窗口对象 综上,这个设计模式都是不太合适的。
    在ES6中,var命令和function命令声明的全局变量依旧是顶层对象的属性,letconstclass命令声明的全局变量不属于顶层对象的属性。
var a = 1; 
// 如果在Node的 REPL 环境,可以写成 global.a
console.log(window.a); // 1

let b = 1;
console.log(window.b); // undefined