let
let不存在变量提升
老规矩,先看代码
1 console.log(a) //undefined 2 let lock = false 3 4 if (lock) { 5 var a = 10 6 }else{ 7 var a =20 8 } 9 10 console.log(a) //20
ES5中只有全局作用域和函数作用域,因此var变量声明被提升到了全局作用域,ES6有了块级作用域,let 声明的变量只能在声明的作用域以及其子作用域使用,而var依然只有全局和函数作用域之分。再看下面代码。
1 console.log(a) // 报错:a is not undefined 2 let lock = false 3 4 if (lock) { 5 let a = 10 6 }else{ 7 let a =20 8 }
a定义在if的block scope(块级作用域)里,let不存在变量提升,全局作用域无法使用
注意下面这些写法会报错
1 let lock = false 2 3 if (lock) let a = 10 //报错: Lexical declaration cannot appear in a single-statement context 4 else let a =20 5 6 7 console.log(a)
1 let a = 2 2 switch(a){ 3 case 1:let b = 20;break; 4 case 2:let b = 30;break; //Identifier 'b' has already been declared 5 }
暂存死区(TDZ)
look代码
1 { 2 console.log(b) //报错:Cannot access 'b' before initialization 3 typeof(b) //报错同上 4 let b 5 }
在let b所属的作用域内,从作用域顶部到let b声明语句之前都存在暂存死区,此时对b进行任何有关的操作均会报错,这就是暂存死区。在全局作用域和函数作用域,let声明的变量都存在暂存死区现象。
const声明的变量也存在暂存死区。
注意事项
同一作用域内let声明的变量不允许重复声明
1 { 2 let b 3 let b //报错:Identifier 'b' has already been declared 4 }
外层作用域无法访问内层作用域中let声明的变量,但内层可以访问到外层let声明变量。内外层声明互不干扰。自己可以动手谢谢看。
常见例子
1 for (var i = 0; i < 5; i++) { 2 setTimeout(function() { 3 console.log(i); 4 }, 1000); 5 } 6 //这个例子执行完输出什么结果? 7 8 //答案是 5,5,5,5,5 9 10 //如果想要输出0,1,2,3,4改怎么实现? 11 //其中一种方案就是把var换成let就可以了。let的块级作用域在这里就体现出来了。 12 13 for (let i = 0; i < 5; i++) { 14 setTimeout(function() { 15 console.log(i); 16 }, 1000); 17 }
为什么下面的代码可以实现需求所需要的输出?
这就需要用到函数预编译相关知识了。函数的预编译在函数执行前一刻才会进行。这时候函数会把参数传入函数内部。对上面含var的for循环而言,当函数调用时也就是1s后,这时for已经执行完,函数预编译传入参数i。var声明的变量没有块级作用域概念, 仅有全局和函数作用域,故函数预编译寻找参数i的值时,找到的都是在全局作用域中的i,即都为5。
对含let的for循环,循环每进行一次就声明一个i,这个i仅在那次循环的块作用域中,因此当函数执行时,预编译找到的参数i值仍是当时那次循环体内的对应的那个i值。
在ES6以前,还有一种方法可以实现这种需求。
1 for (var i = 0; i < 5; i++) { 2 (function (i) { 3 setTimeout(function () { 4 console.log(i); 5 }, 1000); 6 })(i); 7 }
这段代码和let那段for循环都可以实现一样的输出,为什么?
前面说了,函数预编译在函数执行前一刻进行,这里使用立即执行函数(IIFE),当函数定义完立即执行,此时循环还未执行完,i就已经被当作参数传入外面那层function,当一秒后内存function执行时,函数预编译在上一层函数作用域(和var的全局作用域找到的i不同)找到了那一次循环时的i值,因此效果和let一样,但原理上并不完全相同。
参考babel对ES6中let转换成ES5的方法也印证了我上面的说法。
关于函数预编译和立即执行函数相关知识,可参考下面两个链接。
https://zhuanlan.zhihu.com/p/50236805
https://www.cnblogs.com/L-xmin/p/11178599.html
说完了let,说说const吧
let具有的特征const都具有,比如不可重复声明和暂存死区等。下面说说const特有的
const声明时必须赋值,且该值只读,不可修改。emmm,对object和array有点不一样。
对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。const
命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。
1 const a = []; 2 a.push('Hello'); // 可执行 3 a.length = 0; // 可执行 4 a = ['Dave']; // 报错
上面代码中,常量foo
储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo
指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
1 const a = []; 2 a.push('Hello'); // 可执行 3 a.length = 0; // 可执行 4 a = ['Dave']; // 报错
如果要做到对象完全不可更改,可以使用Object.freeze(),参考:https://segmentfault.com/a/1190000019348510
还有点破事。。。
前面讲var的时候博主讲过,对变量提升一词并没有准确的定义,而现在流行的看法也不见得完美无缺。
众所周知let不存在变量提升,事实真的是这样吗?来看看下面这张图。
注意在图1中,代码执行到var i = 0时,c的值在chrome的监控中是not available。但执行到块作用域内的var a = 20时候,c为undefined。如果按照之前var的说法,变量提升是把变量声明提升到作用域顶部,
并初始化为undefined,那在这里的块作用域let是不是也存在所谓的变量提升?然let与var不同,即便c为undefined,但你在let c语句前对c的任何操作浏览器均会报错,因为暂存死区。如果按照另一种说法,变量提升的判断标志是能否在声明前使用,那么let是不存在变量提升的。注意,上面的情况只有块作用域有,全局作用域的let没有这种现象。这也许与chrome内部的机制有关,由于博主能力有限,并无法深究下去,有想法的小伙伴可以给我私信哦。但这不影响我们使用let,所以,以后尽量使用let和const声明变量而不是var。