从ES6开始 js 申请变量的方式有:var、let、const。今天我们来一起讨论这三种的区别。
关于变量提升
想必大家都知道,用var声明变量,js会在预编译时进行变量提升。即如下代码,不会报错。
function a(){
console.log(x);//undefined
}
a();
var x = 2;
可能你会疑惑,既然提升了为什么还是undefined,难道没有提升?
是这样的:js在预编译时,确实会将var声明的变量进行提升,但此时并没有为其赋值,所以是undefined,否则就会报错。不信我们看看const和let。
有图有真相^ _ ^ 由此我们知道const 和 let 必须先声明再使用 (let也是同样报错,在这里就不截图了)
暂时性死区
定义:在代码块内,使用let/const命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // 暂时性死区结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。
typeof a;//ReferenceError
let a;
但直接typeof一个未声明的变量不会报错,返回undefined。
比较隐蔽的死区
function fun(x=y,y=2){
return [x,y];
}
fun();//ReferenceError
调用 fun 函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。
总结:ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
作用域
ES5中只有全局作用域和函数作用域。因此导致一些弊端
- 变量提升导致覆盖
var a = new Object();
funcrion fun(){
console.log(a);
var a = 1;//编译到这一句不会进行操作,因为a已经声明过了,执行到这一句才会赋值为1
console.log(a);
}
fun();//undefined 1
具体请看 js预编译
- 循环计数变量污染
for(var i=0;i<5;i++){
····
}
console.log(i);//5
而ES6中const,let 支持块级作用域,有效避免变量覆盖
const a = 'a';
let b = 'b';
var c = 'c';
if(1) { //创建块级作用域
const a = 'a2';
let b = 'b2';
var c = 'c2';
console.log(a, b, c);// 输出 a2 b2 c2
}
console.log(a, b, c);// 输出 a b c2 如果if语句内容变成函数,本句c的值是也是c,原因是函数执行完后,销毁了函数内部变量,另外函数外部也不能访问内部变量,与覆盖没有关系
关于const
在很多人眼里,这个声明的是一个常量,但是这是一个很错误的认识。实质上是变量名字在内存中的指针不能够改变,但是指向这个变量的值 可能 改变。
const arr =["Amy"];
arr.push("Mike");
console.log(arr);// ["Amy", "Mike"]
常量arr储存的是一个地址,这个地址指向一个对象(数组也是一种对象)。不可变的只是这个地址,即不能把arr指向另一个地址,但对象本身是可变的,可以为其添加值。
但是,如果我们尝试修改变量索引到一个新的数组——即使是和现在内容一样的数组,则会报错,如下:
const arr = ["Amy"];
arr=["Amy"];
console.log(arr);// Uncaught TypeError: Assignment to constant variable.报错不能给常量赋值
符合预期的for循环
for (var i = 0; i != 3; i++) {
setTimeout(function() {
console.log(i);
},10);
}// 依次打印 3 3 3
上面的变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。
for (let i = 0; i != 3; i++) {
setTimeout(function()
{ console.log(i);
},10);
}//依次打印 0 1 2
变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
有人想问:上面说,每次循环i都会创建一个新变量,但let不允许重复定义,这个作何解释?
我们观察发现,这样声明变量并不会报错,说明1、2不是同一作用域,而是两个单独的作用域。而在1中 let 为什么可以在每次循环时被重新创建呢,是因为,在每次创建时,各个 i 都是一个独立的作用域。
设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
如果没有在子作用域对循环变量进行修改或重新定义时,则会沿着作用域链向上一级寻找,如上例子,会打印0、1、2;反之会使用本级作用域中的变量 i。
总结
const、let 缩小了变量作用域,完美避免变量污染;const 固定变量(即固定变量类型)。
声明方式 | 变量提升 | 作用域 | 初始化 | 重复定义 |
---|---|---|---|---|
var | 是 | 全局 | 不需要 | 允许 |
const | 否 | 支持块级 | 是 | 不允许 |
let | 否 | 支持块级 | 不需要 | 不允许 |