let和const

ES6新增let和const命令,用来声明变量,用法类似于var,但与var也有所区别。

  1. let不存在变量提升
  2. 块级作用域内存在let命令会造成暂时性死区
  3. let在同一作用域内不允许重复声明
  4. const和let类似,但const用来声明常量

一、变量提升

ES6之前,js并不存在块级作用域,js作用域只有两种形式:全局作用域和函数作用域

变量的提升即将变量的声明提升到它所在作用域的最开始的地方(仅提升变量的声明而不提升变量的赋值),在js中var声明的变量会造成变量的提升。

    console.log(a,1);
    var a = 10;
    console.log(a,2);
    
    输出结果:
    undefined 1
    10 2

上述代码中,先打印变量a,然后进行变量a的声明和赋值,最后再次打印变量a,其打印结果依次为undefined、10。
看似变量a在为声明之间进行了使用,其实它是进行了变量的提升,它的实际执行顺序如下:

    var a;  // 声明变量
    console.log(a,1);  // 打印未初始化变量a,结果为undefined
    a = 10;  // 为变量a赋值
    console.log(a,2);  // 打印赋值为10的变量a

var命令造成的“变量提升”现象使得变量可以在声明之前使用,但这并不符合正常的编码逻辑,且容易造成变量覆盖全局变量污染现象。

1.变量覆盖

    var a = 'first';
    function f(){
        console.log(a,1);
        if(0){
            var a = 'second'
        }
    }
    f();  // undefined 1

这段代码第一眼会误以为输出为“first 1”,是因为在函数作用域中有同名变量通过var声明时,在函数作用域内变量进行了提升(代码预编译时即完成了变量提升,故与逻辑判断是否进入无关),其实际执行顺序如下:

    var a;
    a = 'first';
    function f(){
        var a;
        console.log(a,1);
        if(0){
            a = 'second'
        }
    };
    f();  // undefined 1

代码本意为f()内使用外部a的值,当if为真时使变量a的值改变,但因变量覆盖导致f()内a的值被覆盖为undefined,故var声明容易造成变量覆盖现象

2.全局变量污染

    var a = 'hello world!';
    
    for (var i = 0; i < a.length; i++) {
      console.log(a[i]); // h e l l o w o r l d !
    }
    
    console.log(i); // 12

由上述代码不难看出,于for循环内以var声明的计数变量i内部使用完后仍然存在,并泄漏为全局变量,造成了全局变量污染。

二、let与块级作用域

ES6中明确规定,如果一个区块中存在let和const命令,这个区块对这些声明的变量,从一开始就形成了封闭作用域,该作用域就被称之为块级作用域

ES5中只有全局作用域和函数作用域,这会造成上述所说的变量覆盖和全局变量污染,为了解决变量提升的问题,ES6提出let和const命令,其不存在变量提升现象,故只能先声明后使用,同时在let和const命令所存在的区块会形成暂时性死区。该变量不再受外部的影响,此区块的作用域称之为块级作用域。

1.let

let和var用法类似,但是let并不会进行变量的提升,所声明的变量仅在let命令所在的代码块生效

    {
      let a = 10;
      var b = 1;
    }

    a // ReferenceError: a is not defined.
    b // 1

let声明变量不会进行变量提升,需要先声明后使用,否则报错。

    console.log(a,1);// Uncaught ReferenceError: a is not defined
    let a = 10;
    console.log(a,2);

相同作用域内,不允许使用let重复声明同一个变量

    // Identifier 'a' has already been declared
    function func() {
      let a = 10;
      var a = 1;
    }

    // Identifier 'a' has already been declared
    function func() {
      let a = 10;
      let a = 1;
    }

故此,若函数携带外部参数,则不允许在函数内部重新声明参数。

    function func(arg) {
      let arg;
    }
    func() // Identifier 'arg' has already been declared
    
    function func() {
      let arg;
    }
    func() // undefined
    
    function func(arg) {
      {
        let arg; //仅在{}内生效
      }
    }
    func() // undefined

let声明变量不会造成变量覆盖,因为各自声明的变量仅在自己所属作用域内生效

    let a = 'first';
    function f(){
        console.log(a,1);
        if(1){
            let a = 'second'
            console.log(a,2)
        };
        console.log(a,3);
    }
    f()
    
    // first 1
    // second 2
    // first 3

let声明变量不会存在全局变量污染

    var a = [];
    for (var i = 0; i < 10; i++) {
      a[i] = function () {
        console.log(i);
      };
    }
    a[6](); // 10
    console.log(i);// 10
    
    let a = [];
    for (let i = 0; i < 10; i++) {
      a[i] = function () {
        console.log(i);
      };
    }
    a[6](); // 6
    console.log(i);// i is not defined
在for循环中设置循环变量的那部分是父作用域,循环体内部为一个单独的子作用域,当循环体内有let声明变量时,每一次循环其内部都形成新的块级作用域
    for (let i = 0; i < 3; i++) {
      let i = 'abc';
      console.log(i);
    }
    // abc
    // abc
    // abc

注:当for循环中用let设置循环变量时,每一次循环所生成的变量都是重新声明的,此时循环次数是由JavaScript引擎记住的

2.块级作用域

ES6所新增的块级作用域实际上为let或const命令声明变量时形成。

暂时性死区

只要块级作用域内存在let命令,则它所声明的变量就绑定在这个区域,不再受外部的影响。

在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)
    if (true) {
      // TDZ开始
      tmp = 'abc'; // ReferenceError
      console.log(tmp); // ReferenceError
    
      let tmp; // TDZ结束
      console.log(tmp); // undefined
    
      tmp = 123;
      console.log(tmp); // 123
    }
ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。

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

块级作用域
实际上ES6中的let命令为JavaScript新增了块级作用域

总的来说,块级作用域就是由一对大括号{}所包裹的内部具有let或者const声明变量的区块。
其中由一对大括号{}、内部具有let或const声明变量都为必须条件(若内部有函数声明函数则结合环境考虑)

ES6允许块级作用域任意嵌套,内层作用域可以定义外层作用域的同名变量。

    {
        {
            {
                {
                    let insane = 'Hello World';
                    { let insane = 'Hello World' }
                }
                console.log(insane,1) // insane is not defined
            }
            var a = 1;
        }
        console.log(a,2) // 1
    };

块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。

    // IIFE 写法
    (function () {
      var tmp = ...;
      ...
    }());
    
    // 块级作用域写法
    {
      let tmp = ...;
      ...
    }
块级作用域与函数声明

函数是否能在块级作用域中声明,到现在仍是一个难以定性的问题。

ES5 规定,函数只能在顶层作用域和函数作用域之中声明。

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

虽然ES5、ES6如此规定,但是实际上考虑到以老代码的兼容问题,浏览器采取了自己的行为方式

ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。

  1. 允许在块级作用域内声明函数。
  2. 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  3. 同时,函数声明还会提升到所在的块级作用域的头部。
    function f() { console.log('I am outside!'); }
    
    (function () {
        if (false) {
            // 重复声明一次函数f
            function f() { console.log('I am inside!'); }
        }
        console.log(f) // undefined
        f();  // Uncaught TypeError: f is not a function
    }()); 

上述代码实际执行顺序为:

    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();  // Uncaught TypeError: f is not a function
    }()); 

在不同的环境中,于块级作用域内进行函数声明处理的方式不一样,故一般来说不在块级作用域内声明函数。若确实需要则应写为函数表达式

    function f() { console.log('I am outside!'); }
    
    (function () {
        if (false) {
            // 重复声明一次函数f
            let f = function() { console.log('I am inside!'); }
        }
        console.log(f) // ƒ f() { console.log('I am outside!'); }
        f();  // I am outside!
    }()); 

三、const

const声明一个只读的常量,一但声明,常量的值就不能改变

    const a = 10;
    console.log(a) // 10
    a = 5
    console.log(a) // Assignment to constant variable

const声明的常量需立即初始化,未初始化则报错

    const a; // Missing initializer in const declaration

const声明所形成的作用域与let相同,会形成暂时性死区,且只在声明所在的块级作用域内有效

    if(1){
        const a = 10;
        console.log(a,1) // 10 1
    }
    console.log(a,2) // a is not defined
    
    if(1){
        console.log(a,1) // Cannot access 'a' before initialization
        const a = 10;
    }

const声明的常量,也与let一样不可重复声明。

    const a = 10;
    const a = '10' // Identifier 'a' has already been declared

const所声明的常量,其本质上是固定其指向的内存地址的值固定不变

  1. 针对简单类型数据(数值、字符串、布尔值),其值本身就存在变量所指向的内存地址,故等同于常量
  2. 针对复合类型数据(对象、数组等),变量指向的内存地址保存的只是一个指向实际数据的指针,const只能保证指针的指向固定,而其指向的数据结构则无法控制
    const a = [];
    a.push('Hello');
    // a的值改变
    a; // ["Hello"]
    // 赋值操作失败
    a = ['world'] // Identifier 'a' has already been declared

若想冻结对象,则可以使用Object.freeze方法

const a = Object.freeze([]);
a.push('Hello'); // Cannot add property 0, object is not extensible

但Object.freeze方法仅冻结当前一层对象,若对象内部还有对象则里面的对象仍可改变

const a = Object.freeze({'name':'张三','age':'14',obj:{'address':'张家山路1号'}});
a.obj.address = '熊猫大道1号'
a.obj.address //'熊猫大道1号'

故冻结对象时应除了本身冻结外,属性也应全部冻结

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

四、小结

  1. var声明变量会变量提升,容易造成变量覆盖和全局变量污染
  2. let和const声明变量会形成暂时性死区,使其在遇到let声明前无法进行调用,促成先声明后使用的良好编程理念
  3. 暂时性死区和块级作用域不是一个概念
  4. 在for循环的循环体中,let声明所形成的块级作用域每一次循环都形成新的块级作用域
  5. const指令声明数值、字符串、布尔值时为常量,但是声明对象、数组等复合类型数据时仅能保证指针指向不变,但是其内部数据结构的改变无法保证
  6. 若想冻结对象有Object.freeze方法,但仅可冻结一层,若对象多层则应遍历后将属性全部冻结

参考文献:ECMAScript 6 入门 阮一峰

五、茶谈

ES5中并无块级作用域这一说法,块级作用域是由ES6中由let或const命令在一对{}中所形成。

那仅存在{},内部是否是块级作用域?

你可能感兴趣的:(javascriptes6前端)