var 声明及变量提升机制

在函数作用域或全局作用域中通过关键字var声明的变量,无论实际上是在哪里声明的,都会被当成在当前作用域顶部声明的变量,这就是我们常说的提升机制。

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

并不是在condition的值为true时才会创建变量value。在预编译阶段,JavaScript 引擎会将上面的getValue 修改成下面这样

    function getValue(condition) {

          var value;

        if (condition) {
            value = "blue";

            return value;
        } else {

            // 此处可访问变量value,其值为undefined

            return null;
        }

        // 此处可访问变量value,其值为undefined
    }

因此es6引入块级作用域来强化对变量声明周期的控制

块级声明(词法作用域)

块级声明用于声明在指定块的作用域之外无法访问的变量。块级作用域存在于

  • 函数内部
  • 块中(字符{和}之间的区域)

let声明

不能在声明变量前访问
把变量的作用域限制在当前代码块中。
let声明不会被提升,因此通常放在封闭代码块的顶部,以便整个代码块都可以访问。

1.不会被提升至函数顶部
2.离开if块,value立刻被销毁
3.如果condition的值为false,就永远不会声明并初始化value

只在当前代码块内生效

    function getValue(condition) {

        if (condition) {
            let value = "blue";

            //其他代码

            return value;
        } else {

            // 变量value在此处不存在

            return null;
        }

            // 变量value在此处不存在
    }

禁止重复声明

在作用域中已经存在某个标识符,此时再使用let关键字声明它就会抛出错误

    var count = 30;

    // 抛出语法错误
    let count = 40;
//或

    let count2 = 30;

    // 抛出语法错误
    var count2 = 40;

如果当前作用域内嵌另一个作用域,便可在内嵌的作用域中用let声明同名变量

    let count = 30, condition = true;

    if (condition) {

        // 不会抛出错误 内部块中的count会屏蔽全局作用域中的count
        let count = 40;
        console.log(count); //40
    }
        console.log(count); //30

const声明

不能在声明变量前访问
const声明常量,其值一旦被设定后不可更改。因此,每个通过const声明的常量必须进行初始化

把常量的作用域限制在当前代码块中。
const声明不会被提升,因此通常放在封闭代码块的顶部,以便整个代码块都可以访问。

常量必须进行初始化

    // 有效的常量
    const maxItems = 30;

    // 语法错误:常量未初始化
    const name;

只在当前代码块内生效

    if (condition) {
        const maxItems = 5;
    }
    // 此处无法访问 maxItems

禁止重复声明

    var message = "Hello!";
    let age = 25;

// 这两条语句都会抛出错误
    const message = "Goodbye!";
    const age = 30;

禁止常量再赋值

    const maxItems = 5;

    // 抛出语法错误
    maxItems = 6;

用const声明对象

const声明不允许修改绑定,但允许修改值。意味着const声明对象后,可以修改该对象的属性值。

    const person = {
        name: "Nicholas"
    };

    // 可以修改对象属性的值
    person.name = "Greg";

    // 抛出语法错误
    person = {
        name: "Greg"
    }

临时死区(Temporal Dead Zone)

let和const不能在声明变量前访问
let和const声明的变量如果在声明之前访问,会触发引用错误

    if (condition) {
        console.log(typeof value); //引用错误
        let value = "blue"; //此时value还位于JavaScript的临时死区中
    }

JavaScript引擎在扫描代码发现变量声明时。

  1. var声明:提升到作用域顶部
  2. let声明const声明:把声明放到TDZ中

type是在声明变量value的代码块外执行的,此时不value 不在TDZ中

    console.log(typeof value); //"undefined"
    if (condition) {
        let value = "blue"; //此时value还位于JavaScript的临时死区中
    }

循坏中的块作用域绑定

    for (var i = 0; i < 10; i++) {

    }
        //此处仍然可访问变量i
    console.log(i); //10

        // js引擎在预编译阶段会将代码转换成
        var i;    
        for (i = 0; i < 10; i++) {

    }
        //此处仍然可访问变量i
    console.log(i); //10

let 与块级作用域绑定

    for (let i = 0; i < 10; i++) {
        // i只能在此处访问
    }
        //抛出错误
    console.log(i);

循坏中的函数

循坏里每次迭代同时共享着变量i,循环内部创建的函数全部都保留了对相同变量的引用 。循坏结束时变量i的值为10,所以每次调用console.log(i)时就会输出数字10

    var funcs = [];
    for (var i = 0; i < 10; i++) {
        funcs.push(function () {
            console.log(i);
        });
    }

    funcs.forEach(function (func) {
        func(); // 10个10;
    })

//在预编译阶段js引擎会解读为  i为全局作用域中的变量

    var funcs = [],
        i;
    for (i = 0; i < 10; i++) {
        funcs.push(function () {
            console.log(i);
        });
    }

    funcs.forEach(function (func) {
        func(); // 10个10;
    })

es5解决方案立即执行函数

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

循坏中的let声明

es6解决方案
每次循坏的时候let声明都会创建一个新变量i。并将其初始化为i的当前值

    var funcs = [];
    for(let i = 0;i<10;i++){
        funcs.push(function () {
            console.log(i);
        })
    }
    funcs.forEach(function(func){
        func(); //输出0-9
    })

每次循坏创建一个新的key绑定,因此每个函数都有一个变量key的副本,于是每个函数都输出不同的值

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

let声明在循环内部的行为是标准中专门定义的,它不一定与let的不提升特性相关

循坏中的const声明

当i++时 尝试改变i的值时会报错

    var funcs = [];
    for (const i = 0; i < 10; i++) {
        funcs.push(function () {
            console.log(i); //报错
        });
    }

每次不会修改已有绑定,而是会创建一个新绑定

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

全局作用域绑定

var 会覆盖全局属性

    var RegExp = "Hello!";
    console.log(window.RegExp); //"Hello!";

    var ncz = "Hi!";
    console.log(window.ncz); //"Hi!"
    console.log(window.RegExp === RegExp); //true 此时 window.RegExp被var RegExp 覆盖了原有属性值

let 或 const 不会覆盖全局变量,而只能遮蔽它。

    let RegExp = "Hello!";
    console.log(RegExp);//"Hello!";
    console.log(window.RegExp === RegExp); //false;

    const ncz = "Hi!";
    console.log(ncz); //"Hi!"

    console.log("ncz" in window); //false

参考 1.let 和 const 命令 《1.let 和 const 命令》
深入理解es6

小结

块级作用域绑定的let和const为JavaScript引入了词法作用域,它们声明的变量不会提升,而且只可以在声明这些变量的代码块中使用。如此一来,JavaScript声明变量的语法与其他语言更相似了,同时也大幅降低了产生错误的几率,因为变量只会在需要它们的地方声明。与此同时,这一新特性还存在一个副作用,即不能在声明变量前访问它们,就算用typeof这样安全的操作符也不行。在声明前访问块级绑定会导致错误,因为绑定还在临时死区(TDZ)中。let和const的行为很多时候与var一致。然而,它们在循环中的行为却不一样。在for-in和for-of循环中,let和const都会每次迭代时创建新绑定,从而使循环体内创建的函数可以访问到相应迭代的值,而非最后一次迭代后的值(像使用var那样)。let在for循环中同样如此,但在for循环中使用const声明则可能引发错误。当前使用块级绑定的最佳实践是:默认使用const,只在确实需要改变变量的值时使用let。这样就可以在某种程度上实现代码的不可变,从而防止某些错误的产生。