# 块级作用域
[toc]
# 1、暂时性死区
只要块级作用域内存在let命令,他所声明的变量就绑定了这个区域,不再受外部的影响。
var tmp = 10;
if (true) {
tmp = 20; // Uncaught ReferenceError: Cannot access 'tmp' before initialization
let tmp;
}
上面代码中,存在全局变量tmp
,但是块级作用域内let
又声明了一个局部变量tmp
,导致后者绑定这个块级作用域,所以在let
声明变量前,对tmp
赋值会报错。
ES6 明确规定,如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
# 2、var变量提升
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
第一个场景:内层变量覆盖外层变量。
if (true) {
var a = 10;
}
console.log(a, window.a);// 10 10
虽然变量a的声明是在if区块中, 但是还是影响到了外部,说明if的区块并没有形成独立的块作用域。
第二个场景:内层变量覆盖外层变量。
var tmp = '1';
var tmp2 = '3'
function foo() {
console.log(tmp, tmp2);
if (false) {
var tmp = '2';
}
}
foo();// undefined '3'
由于foo函数内部的 tmp变量提升, foo函数内部自己的变量对象此时初始化了tmp对象。执行到 console.log(tmp)
这一句时,tmp还未被赋值,所以打印undefined。而tmp2变量由于存在作用域链查找, 会输出 '3'。
第三个场景:计数的循环变量泄漏为全局变量
for (var i = 0; i < 5; i++) {
// ...
}
console.log(i); // 5
上面代码中,变量i
只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
# 3、块级作用域
let关键字为ES6新增了块级作用域。
function foo() {
let n = 6;
if (true) {
let n = 10;
}
console.log(n); // 6
}
foo();
上面的函数有两个代码块,都声明了变量n
,运行后输出 6。这表示外层代码块不受内层代码块的影响。如果两次都使用var
定义变量n
,最后输出的值才是 10。
块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式不再必要了。
// 匿名立即执行函数
(function () {
var tmp = 1;
})();
// 块级作用域写法
{
let tmp = 1;
}
# 4、在块级作用域中声明函数
函数能不能在块级作用域之中声明?这是一个相当令人混淆的问题。
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
比如下面的例子是非法的
if (true) {
function f() {}
}
{
function foo(){}
}
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let
,在块级作用域之外不可引用。
function f() {
console.log('outside');
}
var a = 10;
(function () {
if (false) {
function f() {
console.log('inside');
}
}
try {
f();
} catch (error) {
console.error(error); // TypeError: f is not a function
}
console.log(a); // 10
window.f(); // 'outside'
})();
在ES6的浏览器运行上面的代码,按照块级作用域的函数的处理规则。 理论上应该输出inside, 但是实际是报错的。这是因为
原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B (opens new window)里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式 (opens new window)。
- 允许在块级作用域内声明函数。
- 函数声明类似于
var
,即会提升到全局作用域或函数作用域的头部。 - 同时,函数声明还会提升到所在的块级作用域的头部。
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let
处理。
DANGER
根据这三条规则,浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var
声明的变量
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
::: warning注意
ES6 的块级作用域必须有大括号。如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
:::
下面的写法是错误的
// 'let' declarations can only be declared inside a block
if(true) let x = 1; // Lexical declaration cannot appear in a single-statement context
# 5、块级作用域内的默认变量
在块级作用域里面不使用var、let、const等关键字声明的变量, 称为默认变量
try {
console.log(a);
} catch (error) {
console.error(error); // ReferenceError: a is not defined
}
if (true) {
a = 10;
}
console.log(a); // 10
console.log(window.a); // 10
第一句打印报错, 说明块级作用域内的默认变量不会提升到作用域的顶层。后面两个log输出了10,说明一旦执行过a = 10
这一句赋值之后,顶层window就被添加了a属性。
小结
在块级作用域内部声明的默认变量(不适用let,var,const修饰),只有等到执行过你定义那个变量的那行代码后才可以访问,才给window赋值这个属性,在那行代码之前访问会报错
块内的 默认变量依旧是全局变量
# 6、重温块级作用域内的函数声明
console.log(window.a, a); // undefined undefined
if (true) {
console.log(window.a, a); // undefined ƒ a() {}
function a() {}
console.log(window.a, a); // ƒ a() {} ƒ a() {}
}
console.log(window.a, a); // ƒ a() {} ƒ a() {}
从上面的执行结果可以看出来
- 块内的函数声明会提升到块内的顶部(在块内可以执行),同时也会提升声明到全局作用域, 初始值是undefined
- 只有在解释完
function a() {}
这一句之后,会把块内的变量a对应的值给到window。(注意是块内活动对象的属性a, 并不一定是函数a).每解释一次就执行这个操作,比如多次声明function a() {}
没有执行代码之前的执行上下文栈是这样子的,
ESStack = [
BlockContext: {
AO: {
a: ƒ a() {}
},
scope: [BlockContext.AO, GlobalContext.AO]
},
GlobalContext: {
AO: {
a: undefined
},
scope: [GlobalContext.AO]
}
]
BlockContext
表示块的上下文,GlobalContext
表示全局上下文,BlockContext.AO
表示块内的活动对象,BlockContext.scope
表示块内的作用域链。
在解释完 function a() {}
这一句之后,会把块内的变量a对应的值给到window。执行完这一句之后,执行上下文栈变成了如下
ESStack = [
BlockContext: {
AO: {
a: ƒ a() {}
},
scope: [BlockContext.AO, GlobalContext.AO]
},
GlobalContext: {
AO: {
a: ƒ a() {}
},
scope: [GlobalContext.AO]
}
]
此时的全局上下文和块级上下文中的活动对象中的变量a都变成了函数
# 7、块内同时有同名的默认变量和函数声明
看下面的例子1,多了一个默认变量 a = 50;这一行
console.log(window.a, a); // undefined undefined
if (true) {
if (true) {
console.log(window.a, a); // undefined ƒ a() {}
a = 50;
function a() {}
console.log(window.a, a); // 50 50
}
}
console.log(window.a, a); // 50 50
执行上下文栈的变化如下
初始上下文
ESStack = [ BlockContext: { AO: { a: ƒ a() {} }, scope: [BlockContext.AO, GlobalContext.AO] }, GlobalContext: { AO: { a: undefined }, scope: [GlobalContext.AO] } ]
解释完 a = 50, 注意这一句赋值和函数声明不同, 不会给全局上下赋值块内的属性
ESStack = [ BlockContext: { AO: { a: 50 }, scope: [BlockContext.AO, GlobalContext.AO] }, GlobalContext: { AO: { a: undefined }, scope: [GlobalContext.AO] } ]
解释完 function a() {}。
BlockContext.AO
里面的属性a(50)会被赋值到GlobalContext.AO
ESStack = [ BlockContext: { AO: { a: 50 }, scope: [BlockContext.AO, GlobalContext.AO] }, GlobalContext: { AO: { a: 50 }, scope: [GlobalContext.AO] } ]
再看下面的例子2,切换了一下 function a() {}
, a = 50
的顺序,结果完全不同, 仍然按照上面的理论分析
console.log(window.a, a); // undefined undefined
if (true) {
if (true) {
console.log(window.a, a); // undefined ƒ a() {}
function a() {}
a = 50;
console.log(window.a, a); // ƒ a() {} 50
}
}
console.log(window.a, a); // ƒ a() {} ƒ a() {}
执行上下文栈的变化如下
初始上下文
ESStack = [ BlockContext: { AO: { a: ƒ a() {} }, scope: [BlockContext.AO, GlobalContext.AO] }, GlobalContext: { AO: { a: undefined }, scope: [GlobalContext.AO] } ]
解释完 function a() {}。会把块内的变量a对应的值给到window。
ESStack = [ BlockContext: { AO: { a: ƒ a() {} }, scope: [BlockContext.AO, GlobalContext.AO] }, GlobalContext: { AO: { a: ƒ a() {} }, scope: [GlobalContext.AO] } ]
解释完 a = 50,
ESStack = [ BlockContext: { AO: { a: 50 }, scope: [BlockContext.AO, GlobalContext.AO] }, GlobalContext: { AO: { a: ƒ a() {} }, scope: [GlobalContext.AO] } ]
只要按照执行上下文栈的顺序一步步去分析, 就能得出结论了
再看一个例子3, 声明了两次函数a
console.log(window.a, a); // undefined undefined
{
console.log(window.a, a); // undefined ƒ a() {/* 2 */}
function a() {
/* 1 */
}
console.log(window.a, a); // ƒ a() {/* 2 */} ƒ a() {/* 2 */}
a = 10;
console.log(window.a, a); // ƒ a() {/* 2 */} 10
function a() {
/* 2 */
}
console.log(window.a, a); // 10 10
a = 20;
console.log(window.a, a); // 10 20
}
console.log(window.a, a); // 10 10
有一个点就是重复声明同名函数,前面的会被后面的覆盖
初始化上下文栈
ESStack = [ BlockContext: { AO: { a: ƒ a() {} }, scope: [BlockContext.AO, GlobalContext.AO] }, GlobalContext: { AO: { a: undefined }, scope: [GlobalContext.AO] } ]
解释完第一次声明function a() {}
ESStack = [ BlockContext: { AO: { a: ƒ a() {} }, scope: [BlockContext.AO, GlobalContext.AO] }, GlobalContext: { AO: { a: ƒ a() {} }, scope: [GlobalContext.AO] } ]
解释完 a = 10;
ESStack = [ BlockContext: { AO: { a: 10 }, scope: [BlockContext.AO, GlobalContext.AO] }, GlobalContext: { AO: { a: ƒ a() {} }, scope: [GlobalContext.AO] } ]
解释完第二次声明function a() {}
ESStack = [ BlockContext: { AO: { a: 10 }, scope: [BlockContext.AO, GlobalContext.AO] }, GlobalContext: { AO: { a: 10 }, scope: [GlobalContext.AO] } ]
解释完a = 20;
ESStack = [ BlockContext: { AO: { a: 20 }, scope: [BlockContext.AO, GlobalContext.AO] }, GlobalContext: { AO: { a: 10 }, scope: [GlobalContext.AO] } ]
# 8、块内同时有同名的变量和函数声明
看下面的例子
<script>
a(); // 'function a'
function a() {
console.log('function a');
}
var a = 10;
console.log(a); // 10
</script>
这个其实也好解释,函数声明和变量声明都会提升, 但是函数声明的优先级更高。所以一开始可以执行到函数,后面变量a被赋值为了10。
但是这段代码放在块级作用域内会报错
Uncaught SyntaxError: Identifier 'a' has already been declared
if (false) {
a();
function a() {
console.log('function a');
}
var a = 10;
console.log(a);
}
相同作用域内, 不允许重复声明同一个变量。