基础环节
let
和const
是ES6中新增加的重要关键字,这两个关键字比起var
最明显的区别就是,let
和const
都拥有块级作用域(在一对{}内形成的作用域),且没有变量提升(必须先声明在使用)。
- var 只存在函数作用域和全局作用域(没有块级作用域的概念)
{
let a = 123;
var b = 123;
}
//读取不到a,能读取b
console.log(a) // Uncaught ReferenceError: a is not defined
console.log(b) //123
let关键字
let
不能重复声明赋值,var
可以
let a = 3;
let a; //Wrong!!会报错:Identifier 'a' has already been declared(标识符a已声明)
let
常用与for循环中
for(let i = 0; i < 5; i++){
setTimeout(()=>{
console.log(i)
},100);
}
// 0 1 2 3 4
对比var
for(var i = 0; i < 5; i++){ //同步
//setTimeout可以看做一个异步的过程,在JavaScript的事件循环中,先执行同步代码,再执行异步代码,当异步代码执行时,for循环已经循环完毕,又因为在for循环中,由于用var定义变量,在循环体中执行的是同一个i,所以会输出5个5
setTimeout(()=>{ //异步
console.log(i)
},100);
}
// 5 5 5 5 5
但是如果用var
要实现同一效果:
var _loop = function _loop(i) {
setTimeout(function() {
console.log(i);
}, 100);
};
for (var i = 0; i < 5; i++) {
_loop(i);
}
用var
*实现同一效果的原理就是要使用更多的闭包,而用***let
**实现并没有形成闭包而只是使用了块级作用域,使用过多闭包会形成内存泄漏!
因为let
是局部变量,所以可以在不同代码块(作用域)中重复声明
let a = 1;
{
let a = 2;
console.log(a); // 2
}
console.log(a); // 1
不存在变量提升(有待商榷)
var
命令会发生“变量提升”
现象,即变量可以在声明之前使用,值为undefined
。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
为了纠正这种现象,let
命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错Uncaught ReferenceError: bar is not defined
let bar = 2;
上面代码中,变量foo
用var
命令声明,会发生变量提升,即脚本开始运行时,变量foo
已经存在了,但是没有值,所以会输出undefined
。变量bar
用let
命令声明,不会发生变量提升。这表示在声明它之前,变量bar
是不存在的,这时如果用到它,就会抛出一个错误。
暂时性死区(TDZ)
只要块级作用域内存在let
命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // Uncaught ReferenceError
let tmp;
}
上面代码中,存在全局变量tmp
,但是块级作用域内let
又声明了一个局部变量tmp
,导致后者绑定这个块级作用域,所以在let
声明变量前,对tmp
赋值会报错。
ES6 明确规定,如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
总之,在代码块内,使用let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
块级作用域
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
上面的函数有两个代码块,都声明了变量n
,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用var
定义变量n
,最后输出的值才是 10。
为什么需要块级作用域?
var的使用只有全局作用域和函数作用域,没有块级作用域,容易导致变量泄漏,这会带来很多不合理的场景。
第一种场景,内层变量可能会覆盖外层变量。
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
上面代码的原意是,if
代码块的外部使用外层的tmp
变量,内部使用内层的tmp
变量。但是,函数f
执行后,输出结果为undefined
,原因在于变量提升,导致内层的tmp
变量覆盖了外层的tmp
变量。
第二种场景,用来计数的循环变量泄露为全局变量。
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
上面代码中,变量i
只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
const关键字
const
相当于只读变量,在声明创建变量的同时必须初始化
const PI = 3.1415926
PI; //3.1415926
const a;//Wrong!!会报错:Missing initializer in const declaration(常量声明中缺少初始值设定项)
const
一旦声明初始化后不能修改,只读不写
const a = 3;
const a = 4;//Wrong!!会报错:Identifier 'a' has already been declared(标识符a已声明)
const
和let
一样拥有块级作用域,但是与let不同,即使在不同的代码块中,也不能重复声明
const a = 1;
{
const a = 2;
a;
}
a;
//Wrong!!会报错:Identifier 'a' has already been declared(标识符a已声明)
本质
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据——>引用值
(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // Uncaught TypeError: Assignment to constant variable.
为什么要进行变量提升?
变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。
造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象和变量。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。
在解析阶段
,JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。
- 全局上下文:变量定义,函数声明
- 函数上下文:变量定义,函数声明,this,arguments
在执行阶段,就是按照代码的顺序依次执行。
那为什么会进行变量提升呢?主要有以下两个原因:
- 提高性能
- 容错性更好
提高性能
在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。
容错性更好
变量提升可以在一定程度上提高JS的容错性,看下面的代码:
a = 1;
var a;
console.log(a);
如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。
虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。
总结:
- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了let、const来定义变量,它们就没有变量提升的机制。下面看一下变量提升可能会导致的问题:
var tmp = new Date();
function fn(){
console.log(tmp);
if(false){
var tmp = 'hello world';
}
}
fn(); // undefined
在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。
重谈Let和Const
细化知识
1、首先明确一点:提升不是一个技术名词。
要搞清楚提升的本质,需要理解 JS 变量的「创建create、初始化initialize 和赋值assign」,一个变量在使用之前必须经历这三步,有的地方把创建说成是声明(declare),有的地方把初始化叫做绑定(binding)
在回看之前的知识认知:
var的变量提升:函数执行在预编译过程中,AO(函数作用域)的产生时,变量会提升到AO当中,先创建函数执行时需要用到的变量并初始化为undefined
function的变量提升:在预编译过程中,AO的产生时,函数变量也会提升到AO当中,先创建函数名的变量并初始化为undefined,在把函数体赋值进去
2、在用let声明for循环变量——>for(let i = 0; i < 5; i ++ )中:
for( let i = 0; i< 5; i++) 这句话的圆括号之间,有一个隐藏的作用域
for( let i = 0; i< 5; i++) { 循环体 } 在每次执行循环体之前,JS 引擎会把 i 在循环体的上下文中重新声明及初始化一次。
所以这跟for(var i =0 ; i < 5; i++)不同在于,var的变量在for循环开始到执行完毕都是同一个i,而let的变量在for循环中包括循环5次中,一共有6个不同的i(同名)
颠覆认知
提出观点
let我认为是有变量提升的
甚至说所有的声明(var
, let
, const
, function
, function*
, class
)都会进行提升(hoisted),这一观点是来自 StackOverflow的一个高票回答中的(冤有头债有主系列…)
最终把我说服的是这一个例子:
let a = 1
{
a = 2
let a //Uncaught ReferenceError: Cannot access 'a' before initialization
}
证明观点
遇事不决反证法!
假如let没有变量提升的话,上面的a = 2会隐式变成 var a = 2
,对a进行创建并提升到全局变量上,将let a = 1
覆盖掉,这时全局变量a的值因为var a = 2
的变量提升变成2,但是结果是运行发现 a = 2 报错:未捕获的引用错误:在初始化之前无法访问“a”。
上面的图可以体现出来,let a
语句的确对上面的变量创建造成了影响
浏览器也对错误提示已经有了优化,也明确指出了变量创建的初始化环节之前导致无法访问变量,以前的浏览器提醒是:Uncaught ReferenceError: a is not defined
这说明上面的代码近似近似近似近似近似近似地可以理解为:(注意看注释中的 TDZ)
let a = 1
{
let a // TDZ 开始的地方就是这里(let创建变量的‘create’环节提升)
'start a TDZ'
a = 2 // 由于 a = 2 在 TDZ 中,所以报错
a // TDZ 结束的地方就是这里(let创建变量的‘init’环节)
'end a TDZ'
}
陈述观点
我认为let也是有变量提升的,只是let是提升了变量创建三个流程(create创建,initialize初始化,assign赋值)中的第一项,而var是将前两项进行了提升
所以直接说let 和const没有变量提升我认为这个说法并不完全正确。