let声明定义变量
let跟var的作用差不多,用来定义变量,最明显的区别是:let声明的范围是块作用域,而var声明的范围是函数作用域
块作用域是函数作用域的子集,因此适用于var的作用域限制同样也适用于let。特点:let不允许同一个块作用域中出现冗余声明。
let对比var有如下特点:
- 变量声明不会被提升,即在变量声明之前无法使用该变量,否则产生“暂时性死区”
- 具有局部作用域,即let声明的变量只能在对应代码块中使用
- 不允许重复声明
console.log(b) //undefined
//console.log(c) 报错:c is not defined
//使用let声明的变量不具有变量提升的作用,它的作用域为局部作用域
{
var b=20
let c=30
console.log(c)
}
console.log(b)
//console.log(c) //报错:c is not defined
从上述实例中,我们可以知道,let声明的变量不像var声明的变量一样,使用var声明的变量会自动提升到函数作用域顶部。
暂时性死区
let和var的另外一个重要区别,就是let声明的变量不会在作用域中被提升
实例:
console.log(name) //undefined
var name="zhangsan"
console.log(age) //报错:age没有定义
let age=18
在解析代码时,javascript引擎也会注意出现在块后面let声明,只不过在此之前不能以任何方式引用未声明的变量。而在let声明之前的执行瞬间就称之为“暂时性死区”,有了“暂时性死区”的概念后,我们的typeof
不再是一个百分之百安全的操作。
示例:
typeof x; // ReferenceError
let x;
上述代码中,变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要在此之前使用到该变量,就会报错。但是如果一个变量在使用之前根本没有被声明,使用typeof
反而不会报错。
示例:
typeof x // "undefined"
但是有些"暂时性死区"是比较隐蔽的,不太容易被发现。
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
上面代码中,调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。如果y的默认值是x,就不会报错,因为此时x已经声明了。如果y的默认值是x,就不会报错,因为此时x已经声明了。
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
另外,下面的代码也会报错,与var的行为不同。
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
上面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。
总结:ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
for循环中的let声明
在let出现之前,for循环定义的迭代变量会渗透到循环体外部
示例:
var arr=[]
for(var i = 0 ; i<3;i++){
arr[i]=function (){
console.log(i)
}
}
arr[0]() //arr[0]表示的时函数,运行需要给() ====>3
arr[1]() //=========>3
arr[2]() //=======>3
全都是3 ,因为变量i是var命令声明的,在全局范围内都有效,而全局只有一个变量i。每一次循环,变量i的值都会发生改变,
而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 3。
改成使用let之后,这个问题就消失了,因为迭代变量的作用域仅限于for循环内部
示例:
let arr2=[]
for(let i = 0 ; i<3;i++){
arr2[i]=function (){
console.log(i)
}
}
arr2[0]() //arr[0]表示的时函数,运行需要给() ====>0
arr2[1]() //=========>1
arr2[2]() //=======>2
而在使用let声明迭代变量时,JavaScript引擎在后台会为每一个迭代循环声明一个新的迭代变量。每个function
引用的都是不同的变量实例,所以console.log(i)
输出的就是0,1,2,也就是我所期望的值,也就是循环执行过程中每个迭代变量的值。
let声明不允许重复声明
let不允许在相同作用域内,重复声明同一个变量。
//let声明的变量不允许重复声明
// 报错 Identifier 'a' has already been declared
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
因此,不能在函数内部重新声明参数。
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错
块级作用域
为什么需要块级作用域?
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
第一种场景,内层变量可能会覆盖外层变量
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。
第二种场景,用来计数的循环变量泄露为全局变量
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
在es6中新增了块级作用域,块级作用域的特性:
- 允许块级作用域的任意嵌套。
- 内层作用域可以定义外层作用域的同名变量
- 块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了
注意点:
- JavaScript引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标识符不会报错,而是因为同一块中没有没有重复声明。
- 对于声明的冗余报错不会因为混用了let和var而受到影响。这两个关键词声明的并不是不同类型的变量,它们只是指出变量在相关作用域如何存在。
块级作用域与函数声明
如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在浏览器的实现可以不遵守上面的规定,有自己的行为方式。
- 允许在块级作用域内声明函数。
- 函数声明类似于var,即会提升到全局作用域或函数作用域的头部
- 同时,函数声明还会提升到所在的块级作用域的头部
还有一个需要注意的地方。ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
// 第一种写法,报错
if (true) let x = 1;
// 第二种写法,不报错
if (true) {
let x = 1;
}
上面代码中,第一种写法没有大括号,所以不存在块级作用域,而let只能出现在当前作用域的顶层,所以报错。第二种写法有大括号,所以块级作用域成立
const常量声明
const声明一个只读的常量。一旦声明,常量的值就不能改变。
const具有与let相同的特性,此外会有一些其他特性:
- 变量声明不会被提升,即在变量声明之前无法使用该变量
- 具有局部作用域,即let声明的变量只能在对应的代码块中使用
- 不允许重复声明
- const声明的变量在声明的时候就需要赋值,并且只能赋值一次,不能修改
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值,否则报错
const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
es6中声明变量的6种方式
ES5 只有两种声明变量的方法:var
命令和function
命令。ES6 除了添加let
和const
命令,另外两种声明变量的方法:import
命令和class
命令。所以,ES6 一共有 6 种声明变量的方法。