# 前言
JavaScript存在变量提升的特性,导致了很多缺陷。
ES6引入块级作用域 + let、const来避免这种设计缺陷。
# 作用域
作用域是变量和函数的可见区域,控制变量和函数的可见性和生命周期。
ES6之前:
- 全局作用域 => 全局作用域中的变量和函数在任何地方都能访问,生命周期跟随页面的生命周期。
- 函数作用域 => 函数内部的变量和函数只能在函数内部被访问,当函数执行结束,函数作用域会被销毁。
ES6之后:
- 全局作用域 => 同上
- 函数作用域 => 同上
- 块级作用域 => 一对大括号包裹的一段代码可以看作是一个块级作用域,块内定义的变量在外部不可访问。
块级作用域示例:
// block
{ }
// if
if(1) { }
// while
while(1) { }
// function
function foo() { }
// for loop
for(let i = 0; i < 100; i++) { }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 变量提升的缺陷
JavaScript当初没有块级作用域,设计成把作用域内部的变量统一进行提升。
那为何要变量提升?
- 提高JavaScript执行性能。 JavaScript执行前会先进行编译(编译只进行一次),编译期间进行变量提升。这样避免了执行代码的过程中多次重新解析变量or函数,变量和函数的代码一般是不会改变的,编译一次即可。有一种预编译的感觉,让代码执行起来更快。
- 增强容错性。 可以说是一把双刃剑,如果写代码出现了先使用后定义,代码依旧能正常执行。
# 缺陷
- 变量容易被覆盖。变量提升会把变量的值赋值为undefined。
- 变量无法销毁。
function foo(){
for (var i = 0; i < 3; i++) { }
// for循环结束,i变量按道理应该被销毁了,但是仍然可以读取到。
// 原因:foo创建执行上下文的时候,i变量已经被提升,就算循环结束,i并不会被销毁。
console.log(i);
}
foo()
1
2
3
4
5
6
7
2
3
4
5
6
7
# ES6块级作用域
使用let、const关键字,可以实现块级作用域。 JavaScript在编译阶段:
- var声明存放到变量环境中。
- let、const声明存放到词法环境(栈)中,不会被提升到变量环境中。
- 词法环境内部维护一个小型栈结构,栈底部是函数内部最外层的let变量,每当遇到一个新的块级作用域,压入栈内;每当块级作用域执行完毕,会从栈顶弹出。
- 变量查找过程:词法环境内部块级作用域栈顶 => 栈顶向下 => 变量环境(对象)。
# 暂时性死区
let name = 'mobs'
{
// Uncaught ReferenceError: Cannot access 'name' before initialization
console.log(name)
let name = 'mobs'
}
1
2
3
4
5
6
2
3
4
5
6
在块级作用域中,从开头 ~ let name = 'mobs'代码之间会形成一个暂时性死区,如果在这中间去访问变量name,会报初始化之前不能访问name的错误。
# 总结
- var和let定义的变量分别存在于变量环境和词法环境,互不影响,变量提升仍然有效产生变量环境。
- 词法环境内部通过栈去维护let定义变量的块级作用域。
- 变量查找规则,先从词法环境栈顶向下寻找,继续到变量环境中查找。