关于javascript:let和const

2次阅读

共计 5741 个字符,预计需要花费 15 分钟才能阅读完成。

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 命令在一对 {} 中所造成。

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

正文完
 0