使用let/const定义变量的场景

1,091 阅读9分钟

背景

javaScript中,定义变量是一个非常常见的操作,在Es5中,通常使用var定义声明变量,而在Es6中新增了letconst关键字,也是用于声明定义变量

那究竟在什么样的情况下使用它们,解决自己开发过程当中定义变量的一些困扰

为什么使用Let,const定义变量更节省内存?

javScript中有多少种方法定义声明变量呢?

ES5中只有两种声明变量的方法,varfunction两个关键字,而Es6新增了let,和const,另外,还有两种就是import,和class关键字

var声明及变量提升

在Es5中只有函数作用域和全局作用域,没有块级作用域,通过关键字var声明的变量,无论是在哪里声明的,都会被当成在当前作用域顶部声明的变量,这就是我们常说的提升机制

这会带来一些问题

场景1-函数内层变量可能会覆盖外层变量

var tmp = new Date();
function f(condition) {
    // 此处可以访问变量tmp,其值为undefined
    console.log(tmp);
    if(condition) {
        var tmp = "itclanCoder";
        return tmp;
    } else {
        // 此处可访问变量value,其值为undefined
        console.log(tmp);
        return null;
    }
}
f(false);  // undefined

上面的代码,或许你会认为只有condition条件为true时,才会创建变量tmp,事实上,函数f无论如何变量tmp都会被创建,在预编译阶段,javaScript引擎会将上面的f函数修改成下面这样

函数f执行后,输出结果为undefined,原因就是在于,当使用函数声明时,变量会提升到运行坏境的顶部,导致内层的tmp变量覆盖了外层的tmp变量

它会变成如下这样

function f(condition) {
    var tmp; // 先定义变量
    if(condition) {
        tmp = "itclanCoder";
        return tmp;
    }else {
        return null;
    }

}

var tmp = new Date();
f(false)

变量tmp的声明被提升至函数顶部,而初始化操作依旧停留在原处执行,这就意味着else中的也可以访问到该变量tmp,因为此时变量还没有初始化,只有定义,但没有赋值,所以值是undefined

场景2-用来计数循环变量泄露为全局变量

循环遍历一字符串javaScript,输出打印出每个字符

var str = "javaScript";
for(var i = 0;i<str.length;i++) {
    console.log(i,str[i]);
}

console.log(i);

//0 'j'
1 'a'
2 'v'
3 'a'
4 'S'
5 'c'
6 'r'
7 'i'
8 'p'
9 't'
10

上面的i变量只是用来控制循环,但是循环结束后,它并没有消失,释放,而是泄露成了全局变量,这样会造成全局变量的污染

解决办法:

若使用let定义变量,则变量不会被提升置作用域顶部,它只会在它定义的块级作用域内生效

注意事项

使用let,const定义变量,因为它不存在变量提升,所以,变量一定要在声明后使用,否则会报错

console.log(tmp);  // ReferenceError
let tmp = 2;

上面的i变量只是用来控制循环,但是循环结束后,它并没有消失,释放,而是泄露成了全局变量,这样会造成全局变量的污染

解决办法:

使用let定义变量的话,那么for循环的计数器变量i,只在for循环内有效

如下示例所示

var arr = [];
for(var i = 0;i<10;i++) {
    arr[i] = function() {
        console.log(i);
        return arr.push(i)
    }
}
console.log(arr[8]());  // 10, 11

在上面的代码中,使用var声明的,在全局范围内都是有效的,所以每一次循环,新的i值都会覆盖旧值,导致最后输出的是最后一轮的i的值

如果使用let,声明的变量仅在块级作用域内有效,最后将输出8

块级声明及块级(词法)作用域

正因为Es5中使用var声明的变量,没有块级作用域,会污染全局变量,如果使用不当,会产生一些达不到自己预期的效果,所以在Es6中就有了块级作用域

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

函数内部

  1. 块中(字符{}之间的区域)

  2. 块级与块级之间的代码块是相互隔离的,互不影响的,如下所示

示例代码:

function fn() {
    let n = 12;
    if(true) {
        let n = 20;
    }
    console.log(n);  // 12
}

上面的函数有两个代码块,都声明了变量n,运行后输出12

注意事项

Es6允许块级作用域任意嵌套,外层作用域无法读取内层作用域的变量

{{{{let name = 'itclanCoder'}}}}

内层作用域可以定义外层作用域的同名变量,内部声明的函数都不会影响到作用域的外部

{
    let name = '随笔川迹'
    {
        let name = 'itclanCoder'
    }
}

有了块级作用域的出现,立即执行匿名函数变得不在必要了

(function() {
  var tmp = '';
}())
// 块级作用域
{
    let tmp = '';
}

不存在变量的提升

let不像var那样会发生变量提升现象,所以,变量一定要先声明后使用,否则就会报错

console.log(foo) // ReferenceError
let foo = 2222;

在同一块作用域内不允许重复声明

// 报错
function () {
    let a = 1;
    var a = 2;
}

不能在函数内部重新声明参数

function func(arg) {
    let arg; // 报错
}

但是要是这样的话则是不报错的

function func(arg) {
    if(true) {
        let arg; // 不报错
    }
}

暂时性死区

只要在花括号{}let,const声明定义的变量,它会绑定在这个区域内,不会受外部的影响,它会形成自己封闭的作用域,只要在声明之前使用这些定义的变量,就会报错

在代码块内,使用let,const命令声明变量之前,该变量都是不可用的,这称为暂时性死区(TDZ),换言之,需要提前声明并且赋值,就可以使用

if(true) {
    // 暂时性死区开始
    tmp = 'itclanCoder'; // ReferenceError,报错
    console.log(tmp);

    let tmp; // 暂时性死区结束
    console.log(tmp); // undefined
    tmp = "随笔川迹";
    console.log(tmp);
}

let命令声明变量tmp之前,都属于变量的tmp的死区

之所以定义暂时性死区,和不存在变量的提升,主要是为了减少运行时的错误,防止在变量声明之前就使用这个变量,从而导致一些Bug

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

为什么使用let,const声明变量可节省内存空间

如下面代码

function f(condition) {
    if(condition) {
        let dateVal = new Date();
        return dateVal;
    }else {
        // 变量dateVal在此处不存在
        return null;
    }

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

以上函数f内定义的dateVal变量在使用let声明后,不在被提升至函数顶部,当离开if语句块后,dateVal会立即被销毁

condition的值为false,那么永远不会声明并初始化dateVal

const 声明命令

constEs6新增的关键字,一旦声明后,它的值就不能被更改,所以通过const声明的常量必须进行初始化,不能留到以后在赋值

// 有效的常量
const maxLength = 10;
// 语法错误,常量未初始化
const name;

关于循环中const声明

在代码中,经常会用到for循环,需要初始化变量,对于for循环来说,可以在初始化时使用const,但要是更改这个变量的话,它就会抛出错误

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

在这段代码中,变量i被声明为常量,在第一次循环中,i0,迭代执行成功,然后执行i++,因为这条语句试图修改常量,因此抛出错误,如果后续循环不会修改该常量,那么可以使用const声明

比如:for-infor-of循环中使用const时的行为与使用let一致,如果使用const定义的常量,后续不会被修改,那么可以使用

var arrs = [];
var object = {
    a: true,
    b: true,
    c: true
}

// 不会产生错误
for(const key in object) {
    arrs.push(function() {
        console.log(key);
    })
}

arrs.forEach(function(arr) {
    arr();
})

注意事项

对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址,const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变

因此,将一个对象声明为常量必须非常小心

const foo = {};
foo.data = 123;
console.log(foo.data) // 123
foo = {};  // TypeError: 'foo' is read-only不起作用

在上面的代码中,常量foo存储的是一个地址,指向一个对象,不可变的只是这个地址,不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新的属性

关于全局块作用域的绑定

var,和function被用于全局作用域时,它会创建一个新的全局变量对象作为全局对象(浏览器环境中的window对象),使用var会覆盖一个已经存在的全局变量

let,constclass命令声明的全局变量不属于全局对象的属性,声明的变量不会提升,而且只可以在声明这些变量的代码块中使用

不能在声明变量前访问它们

如果不想为全局对象创建属性,则使用letconst要安全得多

如果希望在全局对象下定义变量,仍然可以使用var,这种情况下常见用于在浏览器中跨ifram或跨window访问代码

具体什么时候使用var,let,const

对于需要写保护的变量则使用const,只有确实需要改变变量的值时,则使用let,因为大部分变量的值在初始化后不应该在改变,而预料外的变量的值的改变会产生很多Bug

如果希望在全局对象下定义变量,可以使用var

总结

块级作用域绑定的let,constjavaScript引入了词法作用域,使用它们声明变量不会提升,而且只可以在声明这些变量的代码块种使用\

使用let,const也能够节省内存空间,不会造成全局变量的污染,必须的得前置声明赋值,然后才能使用(暂存性死区)\

对于变化的变量,则使用let,而不改变的定义变量,使用const声明,如:for循环体中,使用const定义初始化值变量,那么就会报错,因为常量不能被改变\

for..in,for..of循环中,let,const都会每次迭代创建一个新的绑定,从而使循环体内创建的函数可以访问到相应迭代的值,而非最后一次迭代后的值

原文出处-使用Let/const定义变量的场景