var声明及变量提升(Hoisting)机制
提升机制:在函数作用域或全局作用域中通过关键字var
声明的变量以及关键字function
声明的函数,无论实际上是在哪里声明,都会被当成在当前作用域顶部声明的变量。
注意:提升机只是提升声明操作,初始化操作依旧留在原处执行。
块级声明
块级作用域,也称为词法作用域,用于声明在指定块的作用域之外无法访问的变量,其存在于:
- 函数内部
- 块中(字符{和}之间的区域)
let声明
let
声明,可以把变量的作用域限制在当前代码块,且通常放在封闭代码块的顶部。
1 | function getValue(condition){ |
执行流离开if块,value立刻被销毁。
禁止重声明
假设作用域中已经存在某个标识符(由关键字var
或function
创建),此时再使用let
关键字声明它会抛出错误。
1 | var count=30; |
但是可以在内嵌的作用域中用let
声明同名变量
1 | function foo(){}; |
内部块中的count
会遮蔽全局作用域中的count
,后者只有在if块外才能访问到。
const声明
const
声明的是常量,其值一旦被设定后不可更改,且每个const
声明的常量必须进行初始化。
const与let
- const与let相同点:
- 都是块级作用域,不会提升,只在当前代码块有效
- 声明已经存在的标识符(
var
、let
、function
)会导致语法错误
- const与let不同点:
- 无论在严格模式还是非严格模式,都不可以为
const
定义的常量再赋值(但对象中的值可以修改)。
用const声明对象
记住,const
声明不允许修改绑定,但允许修改值,意味着用const
声明对象后,可以修改该对象的属性值。
1 | const person={ |
如果直接给person
赋值,即要改变person
的绑定,就会抛出错误。
切记,const
声明不允许修改绑定,但允许修改绑定的值。
临时死区(Temporal Dead Zone)
先看例子:
1 | if(condition){ |
什么是临时死区?
JS引擎在扫描代码发现变量声明时,要么将它们提升至作用域顶部(遇到var
声明或function
),要么将声明放到TDZ中(遇到let
和const
声明)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移出,然后方可正常访问。
但在let声明的作用域外对该变量使用typeof则不会报错
1 | console.log(typeof value); //"undefined" |
此时value并不在TDZ中,也意味着不存在value与这个块级作用域的绑定。
循环中的块级作用域绑定
开发者希望实现for循环的块级作用域,这样可以把随意声明的计数器变量限制在循环内部。
1 | for(let i=0;i<10;i++){ |
循环中的函数
看这段代码
1 | var funcs=[]; |
循环里每次迭代同时共享着变量i,循环内部创建的函数全部都保留了对相同变量的引用。
可以使用IIFE来解决这个问题
1 | var funcs=[]; |
在循环内部,IIFE表达式为接受的每一个变量i都创建了一个副本并储存为变量value。
循环中的let声明
let
声明,每次迭代循环都会创建一个新变量,并以之前迭代中同名变量的值将其初始化:
1 | var funcs=[]; |
每次循环的时候let声明都会创建一个新变量i,并将其初始化为i的当前值,所有内部循环创建的每个函数都得到属于自己的i的副本。
对于for-in
循环和for-of
循环也一样
1 | var funcs=[], |
每次循环创建一个新的key
绑定,因此每个函数都有一个变量key
的副本,于是每个函数输出不同的值。
如果使用var
声明key
,则这些函数会输出“c”。
注意:let声明在循环内部的行为是标准中专门定义的,它不一定与let的不提升特性相关。
循环中的const声明
针对不同类型的循环const
会表现出不同的行为:
- 普通的for循环,可以在初始化变量时使用
const
,但是更改这个变量的值会抛出错误。1
2
3
4
5var funcs=[];
for(const i=0;i<10;i++){
console.log(i); //0,完成一次迭代后抛出错误
}
在循环的第一个迭代中,i是0,迭代执行成功。然后执行i++,因为该语句试图修改常量,因此抛出错误。
在for-in
和for-of
循环中使用const时的行为与使用let一致。
不会报错的原因,每次迭代不会修改已有绑定,而是会创建一个新绑定。
全局块作用域绑定
let和const,与var的另一个区别是他们在全局作用域中的行为。
- 当var被用于全局作用域时,他会创建一个新的全局变量作为全局对象的不可配置属性(不能删除)。
- 当let const用于全局作用域时,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。即,不能覆盖全局变量,而只能遮蔽。
1
2
3
4
5
6
7let RegExp="hello";
console.log(RegExp); //"hello"
console.log(window.RegExp===RegExp); //false
const ncz="hi";
console.log(ncz); //"hi"
console.log("ncz" in window); //false
块级绑定最佳实践的进化
一开始人们认为:
默认使用let,对于需要保护的变量则要使用const。
后来人们认为:
默认使用const,只有确实需要改变变量的值时使用let。
小结
let
与const
块级绑定将词法作用域引入了JS。这两种声明方式都不会进行提升,并且
只会在声明它们的代码块内部存在。由于变量能够在必要位置被准确声明,其表现更加接近
其他语言,并且能减少无心错误的产生。作为一个副作用,你不能在变量声明位置之前访问
它们,即便使用的是typeof
这样的安全运算符。由于块级绑定存在暂时性死区(TDZ ),
试图在声明位置之前访问它就会导致错误。
let
与const
的表现在很多情况下都相似于var
,然而在循环中就不是这样。在for-in
与for-of
循环中,let
与const
都能在每一次迭代时创建一个新的绑定,这意味着在循
环体内创建的函数可以使用当前迭代所绑定的循环变量值(而不是像使用var
那样,统一使
用循环结束时的变量值)。这一点在for
循环中使用let
声明时也成立,不过在for
循
环中使用const
声明则会导致错误。
块级绑定当前的最佳实践就是:在默认情况下使用const
,而只在你知道变量值需要被更改
的情况下才使用let
。这在代码中能确保基本层次的不可变性,有助于防止某些类型的错
误。