ES6学习笔记(一)

JavaScript 的发展就像脱缰野马,在我刚刚能够!😄娴熟!😄的使用ES5的时候,ES6来的是那么的猝不及防,让我吐了一口老血。时代在发展,社会在进步,本着可以跟不上时代的脚步,但不能被时代淘汰的想法,开始查阅关于ES6的资料,系统的学习一下。于是乎,买了两本书,一本是红皮书作者《深入理解ES6》 ,另一本则是阮一峰大大《ES6标准入门》。工作之余翻阅学习,写下笔记。

var let const 声明
  1. var声明的变量,无论在哪里声明的,都会被当成在当前作用域顶部声明的变量。也就我们常说的变量提升(Hoisting)机制。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function getValue(condition){
    if(condition){
    var value = 'blue';
    return value;
    }else{
    //此处可以访问到变量 value , 值为 undefined 。
    return null;
    }
    //此处可以访问到变量 value , 其值也为 undefined 。
    }

    上面的代码在预编译阶段,JS引擎会将其解析为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function getValue(condition){
    var value;
    if(condition){
    value = 'blue';
    return value;
    }else{
    return null;
    }
    }
  2. letconst 声明的变量可以把变量的作用域限制在当前代码块中(也就是所说的块级作用域)。它们不会被提升至作用域顶部,只在当前代码块内有效,一旦执行到块外会立即被销毁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function getValue(condition){
    if(condition){
    let value = 'blue';
    return value;
    }else{
    //此处不能访问到变量 value 。
    return null;
    }
    //此处不能访问到变量 value 。
    }

    变量value改用let声明后,不再被提升至函数顶部。并且,如果condition的值为false,就永远不会声明并初始化value。是不是很神奇!!!

  3. const关键字声明的变量为常量,其值一旦被设定就不可更改。因此,每个通过const声明的变量必须进行初始化。

    1
    2
    3
    const num = 2;
    //语法错误,常量没有初始化。
    const name;
禁止重声明
  1. 如果作用域中已经存在了某个标识符,那么在相同作用域下再次声明,就会抛出语法错误;但是如果当前作用域内嵌套了另一个作用域,那么就可以在内嵌的作用域中用let声明同名变量;与let类似,在同一作用域下用const声明已经存在的标识符也会导致语法错误,无论该变量是用var声明的还是用let声明的:

    1
    2
    3
    4
    5
    var message = 'hello';
    let age = 25;
    //下面语句都会抛出语法错误
    const message = 'word';
    const age = 26;
  2. constlet的不同之处在于,const定义的常量只能赋值一次,即在初始化的时候赋值。否则会抛出语法错误。

    1
    2
    3
    const temp = 5;
    //抛出语法错误
    temp = 6;
  3. 虽然const声明的变量不能再赋值,但如果用const声明的是对象的话,就又不一样了。const声明虽然不允许修改绑定,但允许修改值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const person = {
    name: 'sean';
    };
    //可以修改对象的属性值
    person.name = 'emoii';
    //抛出语法错误,不能修改绑定
    person = {
    name: 'emoii';
    };
暂时死区(TDZ > Temporal Dead Zoon
  1. 什么是TDZ?
    ECMAScript标准中并没有给出TDZ的定义,这是JS社区里提出来的一种叫法。是指:变量被声明和被初始化之间的这段时间。
  2. let / const 与 var 的不同
  • let/const 使用的是区块作用域;var 使用的是函数作用域。
  • 在 let/const 声明之前访问对应的变量与常量,会抛出ReferenceError错误;但在var声明之前访问对应的变量,则会得到undefined
  1. 那么问题来了:为什么一个会抛出ReferenceError错误,而另一个会得到undefined呢?

    在ES6标准中对let/const声明的章节,有以下文字说明:

    1
    The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated.

    意思是说由let/const声明的变量,当它们包含的词法环境(Lexical Environment)被实例化时会被创建,但只有在变量的词法绑定(LexicalBinding)已经被求值运算后,才能够被访问。

    通俗点讲就是:当程序的控制流程在新的作用域(module, function或block作用域)进行实例化时,在此作用域中的用let/const声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定,也就是对声明语句进行求值运算,所以是不能被访问的,访问就会抛出错误。

    从上面的理解看,就产生了疑惑,在此作用域中的用let/const声明的变量会先在作用域中被创建出来这句话是不是意味着用let/const声明的变量也会存在提升机制呢?

    1
    2
    3
    4
    5
    var a = 1;
    (function(){
    console.log(a);
    let a = 2;
    })()

    如果用let声明的变量不会发生提升的话,那么这里的console语句输出的结果应该为1;但这里也会报ReferenceError错误,这也就说明了用let声明的变量会提升,用let/const/class声明的变量均会发生提升。

    既然用let声明的变量会发生提升,上面的报错又该怎么解释?
    接着看ES6标准中的文字说明:

    1
    A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.

    意思是说,以let/const声明的变量或常量,必须是经过对声明的赋值语句的求值后,才算初始化完成,创建时并不算初始化。如果以let声明的变量没有赋给初始值,那么就赋值给它undefined值。也就是经过初始化的完成,才代表着TDZ期间的真正结束,这些在作用域中的被声明的变量才能够正常地被访问。

    那么这个问题就好解释了,产生报错的原因在于 var 和 let 声明的变量在发生声明提升时,初始化(initialisation)的行为不同导致的。用 var 声明的变量会初始化为undefined;而用 let 声明的变量会保持为未初始化(uninitialised)的状态。也就是说var声明的变量在hoist之后,被初始化为undefined,但是let声明的变量并没有初始化,直到真正声明它的地方才完成初始化。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    (function(){
    console.log(a); // a 在这里被初始化为 undefined
    console.log(b); // b 在这里保持为未初始化状态,报 ReferenceError 错误
    var a = 1;
    let b; // undefined
    console.log(a); // 1
    console.log(b); // undefined
    b = 2;
    console.log(b); // 2
    })()
  2. 参考及引用
    理解ES6中的暂时死区(TDZ)
    javascript 中 TDZ 的理解

循环中的函数
  1. IIFE(Imdiately Invoked Function Expression)
    IIFE全称为立即调用的函数表达式,也称为立即执行函数。
    它产生的原因很有趣:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var funcs = [];
    for(var i = 0; i < 10; i++){
    funcs.push(function(){
    console.log(i);
    });
    }
    funcs.forEach(function(func){
    func(); // 输出十次数字 10
    });

    预期的结果是输出数字 0~9 ,但却输出了一连串的数字 10 ,这是因为循环里的每次迭代同时共享着变量 i ,循环内部创建的函数全部都保留了对相同变量的引用。循环结束时变量 i 的值为 10 ,所以每次调用console语句输出的都是数字 10 。

    为了解决这个问题,在开发中使用立即调用函数表达式(IIFE),以强制生成计数器变量的副本。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var funcs = [];
    for(var i = 0; i <10; i++){
    funcs.push((function(value){
    return function(){
    console.log(value);
    }
    }(i)));
    }
    funcs.forEach(function(func){
    func(); // 依次输出0~9
    });

    在循环内部,IIFE表达式为接受的每一个变量 i 都创建了一个副本并存储为变量 value 。这个变量的值就是相应迭代创建的函数使用的值。因此调用每个函数都会像从 0 到 9 循环一样得到期望的值。

    具体的IIFE介绍以及更多使用细节请参考:
    深入理解闭包系列第三篇——IIFE
    详解javascript立即执行函数表达式(IIFE)

  2. 循环中的let声明和const声明
    ECMAScript6中提供的let和const不仅简化了代码,还让我们不用这么折腾,毕竟这种代码不够清晰。
    使用let声明,上述的例子就简单了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var funcs = [];
    for(let i = 0; i < 10; i++){
    funcs.push(function(){
    console.log(i);
    });
    }
    funcs.forEach(function(func){
    func(); // 依次输出0~9
    });

    这是因为let声明在每次循环迭代时都会创建一个新的变量,并以之前迭代中同名变量(也就是变量 i )的值将其初始化,所以循环内部创建的每个函数都能得到属于自己的 i 的副本。

    对于for-in循环和for-of循环来说也是一样的,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var funcs = [],
    object = {
    a: true,
    b: true,
    c: true
    };
    for(let key in object){
    funcs.push(function(){
    console.log(key);
    });
    }
    funcs.forEach(function(func){
    func(); // 依次输出a、b、c
    });

    那么能不能在循环中使用const呢?如何使用?

    1
    2
    3
    4
    5
    6
    var funcs = [];
    for(const i = 0; i < 10; i++){
    funcs.push(function(){
    console.log(i);
    });
    }

    这段代码肯定报错,是因为在初始阶段变量 i 被声明为常量,在循环的第一个迭代中 i 为 0 ,迭代执行成功。然后执行 i++,这条语句试图修改常量 i ,所以抛出错误。

    再看这例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var funcs = [],
    object = {
    a: true,
    b: true,
    c: true
    };
    for(const key in object){
    funcs.push(function(){
    console.log(key);
    });
    }
    funcs.forEach(function(func){
    func(); // 依次输出a、b、c
    });

    for-in循环和for-of循环中,let和const都会每次迭代时都会创建新绑定,从而使循环体内创建的函数可以访问到相应迭代的值。所以上面这段代码不会报错,因为每次迭代不会修改已有绑定,而是会创建一个新的绑定。

全局块作用域绑定
  1. 当var被用于全局作用域时,它会创建一个新的全局变量作为全局对象(浏览器环境中的window对象)的属性。这意味着用var很可能会无意中覆盖已经存在的全局属性:

    1
    2
    var RegExp = 'hello';
    console.log(window.RegExp); // hello
  2. 但是,如果使用的是let或者const的话,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。通俗点将就是:用let或者const声明的变量不能覆盖全局变量,只能遮蔽全局变量。

    1
    2
    3
    let RegExp = 'hello';
    console.log(RegExp); // hello
    console.log(window.RegExp === RegExp); // false
  3. 如果希望在全局对象下定义变量,仍然可以使用var。例如:iframe、跨window访问代码

当前使用块级绑定的最佳实践是:默认使用const,只在确实需要改变变量的值的时候使用let,尽量不使用var,尤其是在全局对象下,避免全局空间污染。

0%