let是ES6中新加入的变量声明,和var类似,但是let声明的变量只能在其声明的块级作用域中使用
if(true){
var a=1;
}
console.log(a);//输出1
if(true){
let b=1;
}
console.log(b);//报错:Uncaught ReferenceError: b is not defined
在这里a通过var在if块中声明,声明后在if块外面仍能调用,但是let在if块中声明后在块外面调用时却报了变量未声明的错误,这是因为let声明的变量只能在其所在块级作用域中使用。使用for循环时也是一样:
a = [];
for (var i = 0; i < 5; i++) {
a[i] = () => console.log(i);
}
a[3]();//5
a = [];
for (let i = 0; i < 5; i++) {
a[i] = () => console.log(i);
}
a[3]();//3
在使用var时,因为i为全局变量,所以在每次循环的时候,i的值都会发生变化,即每次赋给console.log(i)中的i的值是一样的,也即是最后一次循环结束时的值。
而在使用let时,因为其声明的变量会在块级作用域中,所以每次i都是对应我们所要打印出来的值,除此之外,在使用for循环时使用let来声明要用于循环的变量,可以使该变量在循环结束后消除该变量。
这里有个小误区,有人会认为for循环中括号内的let声明在这个for循环中只创建了一个变量,但实际上并非如此,我们用下面这段代码来解释上面的代码。(要注意下面的for循环代码和上面使用let声明的for循环代码一致,除了最终在for循环后var声明的变量不会消除)
a = [];
for (var i = 0; i < 5; i++) {
let j=i;
a[j] = () => console.log(j);
}
这样就可以清晰地看出let声明在for循环中的真正工作了,在每次循环都会创建一个块级作用域的变量,即在上面的代码中(for后面的i用let声明的代码),i一共被声明了6次(第6次创建后与5进行比较后跳出循环),每次声明都在不同的块级作用域(不同的循环),所以不会报错。
在平时使用var时,我们会遇到一个奇怪的现象,即变量在声明之前就可以使用,即我们平常所说的变量提升
a=1;
var a;//1
在ES6中,let并没有这种奇怪的特性,看起来更为规范,当我们在用let声明前使用了该变量,会发生报错
a=1;
let a;// Uncaught ReferenceError: a is not defined
由于let没有变量提升,所以出现了暂时性死区,即在一个块级作用域中,如果let声明了一个变量,这个变量就会受其“绑定”,不受外部作用域影响,在该块级作用域中如果用let声明了一个变量,那么在这个块级作用域中在这个声明之前就不能使用该变量,即使在外部的作用域中已声明了该变量。
var a=1;
if(true){
a++;
let a;
}
//Uncaught ReferenceError: a is not defined
在上面这段代码中,虽然a已经被声明为全局变量,但因为在if后的块级作用域中,用了let声明变量a,但是却又在该声明前使用了a,所以报错了。有趣的是,对未声明的变量使用typeof本来不会报错,会得到undefined,但在暂时性死区中会报错。
{
console.log(typeof a);// undefined
console.log(typeof b); Uncaught ReferenceError: b is not defined
let b;
}
使用var或者不加任何修饰符是可以重复声明的,但这样显然有些奇怪,let解决了这一问题。let在同一作用域中不能重复声明,也不能重新声明函数传进来的参数。
if(true){
let a=1;
let a=2;
}
//Uncaught SyntaxError: Identifier 'a' has already been declared
function func(a) {
let a;
}
func(1);
//Uncaught SyntaxError: Identifier 'a' has already been declared
function func(a) {
{let a;}
}
func(1);
//undefined (不报错)
我们知道,当我们在全局写入一个
var a = 1
的时候,这个a会被作为globalThis的属性,在浏览器中也就是a的对象
var a = 1
console.log(window.a)
// 1
但是如果是使用let的话,是不会有这种情况的
let a = 1
console.log(window.a)
// undefined
我们可以通过打断点来查看let和const在全局声明时所在的作用域
可以看到,当我们在全局使用let和const声明变量的时候,变量是在与Global同级的Script中
const声明一个只读的常量,声明之后不能对其进行修改,出于这个特性,const在声明时就要赋值,如果在之后赋值会报错,其作用域与let一样,都是块级作用域,不存在变量提升,也会有暂时性死区。同样的,在上面也有截图看到,const在全局声明时并不会添加到全局对象上
const a=1;
a=2;//Uncaught TypeError: Assignment to constant variable.
const a;
a=1;//Uncaught SyntaxError: Missing initializer in const declaration
事实上const声明的变量也并非值不能改变,在阮一峰的ECMAScript 6入门中有说到
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
即是说,假如我们使用const声明一个变量指向一个对象,只是使得该变量变为一个固定指向指定的对象的“常量”,但对象里面的内容是可以修改的,即对象的属性和方法。
const b={
a:2
}
console.log(b.a); // 2
b.a=3; // 不会报错!
console.log(b.a) // 3
同样的,数组的const声明也是一样的,看看下面这段代码
const arr=[1,2,3];
arr[3]=4;
console.log(arr);
//[1, 2, 3, 4]
可以看到,我们很明显修改了这个数组,但是这里const不变的是对这个数组的引用,所以并不会报错。
在ES6开始,块内声明的函数,其作用域在这个快内,在块外调用块内声明的函数会报错(如果块外没有声明同名的函数的话)。看如下代码
{
fn(); // fn
function fn(){
console.log('fn');
}
}
fn(); // 报错
上面的代码因为是在块中声明的,所以在块外调用时会报错,我们将其放到babel下编译,就得到下面的代码
{
var _fn=function _fn(){
console.log('fn');
}
_fn();
}
fn();
这段代码就可以明显看到块中定义的函数只在块中有效了。出于ES6的这种特性,依赖于旧的非块级作用域的代码可能会出现问题。
if(flag){
function fn(){
console.log(1);
}
}else{
function fn(){
console.log(2);
}
}
fn();
这里的fn()调用最后会打印出什么,视当前所处的环境,若是在ES6之前的环境,不管flag是什么,最后都是打印出2。因为在ES6之前没有块级作用域的概念,两个fn方法会被提升到块的外部,而后声明的fn方法代替了前声明的fn方法,所以都是打印出2,而在ES6后的环境,最后的fn()会报错(如果在外部没有声明fn()方法的话),因为两个fn()方法都是在块内声明的,没法在块的外部调用。
参考文档:阮一峰的《ECMAScript 6入门》
Kyle Simpson的《你不知道的JavaScript 下卷》
ES6学习笔记目录(持续更新中)
ES6学习笔记(一)let和const
ES6学习笔记(二)参数的默认值和rest参数
ES6学习笔记(三)箭头函数简化数组函数的使用
ES6学习笔记(四)解构赋值
ES6学习笔记(五)Set结构和Map结构
ES6学习笔记(六)Iterator接口
ES6学习笔记(七)数组的扩展
ES6学习笔记(八)字符串的扩展
ES6学习笔记(九)数值的扩展
ES6学习笔记(十)对象的扩展
ES6学习笔记(十一)Symbol
ES6学习笔记(十二)Proxy代理
ES6学习笔记(十三)Reflect对象
ES6学习笔记(十四)Promise对象
ES6学习笔记(十五)Generator函数
ES6学习笔记(十六)asnyc函数
ES6学习笔记(十七)类class
ES6学习笔记(十八)尾调用优化
ES6学习笔记(十九)正则的扩展
ES6学习笔记(二十)ES Module的语法