ES5中规定,函数只能在顶层作用域或函数作用域之中声明,不能在块级作用域声明。
javascript
// 情况一
if (true) {
function f() {}
}
// 情况二
try {
function f() {}
} catch(e) {
// ...
}
上面两种函数声明,根据 ES5 的规定都是非法的。但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let
,在块级作用域之外不可引用。
javascript
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
上面代码在 ES5 中运行,会得到"I am inside!",因为在if
内声明的函数f
会被提升到函数头部,实际运行的代码如下。
javascript
// ES5 环境
function f() { console.log('I am outside!'); }
(function () {
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}());
ES6 就完全不一样了,理论上会得到"I am outside!"。因为块级作用域内声明的函数类似于let
,对作用域之外没有影响。
但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,这是为什么呢?
javascript
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
- 允许在块级作用域内声明函数。
- 函数声明类似于
var
,即会提升到全局作用域或函数作用域的头部。 - 同时,函数声明还会提升到所在的块级作用域的头部。
根据这三条规则,浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var
声明的变量。上面的例子实际运行的代码如下。
javascript
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
再看一个例子
javascript
console.log("第一次输出a: ", a) //输出 本地a
if (true) {
// 这里js隐式的把function a的定义放到这里来了,此刻这里有一个 块a
a = 1 // 将 块a的值 由函数修改为1
console.log("第二次输出a: ",a) // 此时输出的是 块a的值
function a() {} // 执行到function a的声明处,此时会将if块中的a同步到外部去,也就是把块a赋值给了本地a, 本地a此刻从undefined被修改为了1
a = 2 // 此处修改的是 块a, 块a的值由1修改为2
console.log("第三次输出a: ", a) // 输出块a的值
}
console.log("第四次输出a: ",a) // if执行完毕,没有块了,此时输出的是本地a的值
在if语句处打一个断点。当程序执行到if语句时(linenumber 2),本地a的定义是undefined,且不存在块,说明2成立,函数声明a被提升到全局作用域。
当语句执行进入到if时(linenumber4),此时就会产生块,debug停留在a=1;这句时(此时赋值还未进行),块a已经变为了function,3成立,if块中的函数声明被提升到了所在块的顶部。但此时本地a还是undefined:
当debug停留在a=2;这句时(linenumber 7,赋值还未进行),此时块中执行了function a的函数声明,该声明被同步提升到了块外,影响了本地a,本地a此时从undefined变为了1,这也是2的证实,执行到具体的"声明"语句时,全局就有了值,这与ES6中使用var声明变量时的效果是一样的。
当debug执行到if块的最后一句console.log时(linenumber8),打印的是块中a的内容(就近原则,或者说块a遮蔽了本地a),所以输出的结果是2。
当debug执行到最后一句console.log时(linenumber 10),此时if语句已经执行完毕,块没有了,所以打印的只能是本地a的内容,本地a的内容此时的值是1
===========================华丽的分割线====================================
块内的函数声明会有一次全局性提升(例如例子中的function a),在作用域顶部声明了一个与函数同名的变量(也就是vscode调试中的本地变量a),初始值是undefined。进入块之后,函数声明又会被提升到块的顶部(也就是块变量a,初始值是function)。此时本地一个变量a(值是undefined),块内一个变量(值是function)。然后块内代码继续执行,每次执行到块内的函数声明那句话时,就会进行一次块内变量为本地同名变量的赋值,就好像块内与本地同名变量的同步。
javascript
//本地变量a,初始值是undefined
console.log(`1 ${window.a},${a}`);//undefined,undefined
{ //块变量a,初始值是function a{}
console.log(`2 ${window.a},${a}`);//undefined,function a(){}
function a(){} // 同步
console.log(`3 ${window.a},${a}`);//function a(){},function a(){}
a = 42;//为块变量a赋值
console.log(`4 ${window.a},${a}`);//function a(){},42
function a(){}//同步
console.log(`5 ${window.a},${a}`);//42,42
}//块变量a消失,此时仅有本地变量a
console.log(`6 ${window.a},${a}`);//42,42
注意,这个示例代码在本地VSCode上会有严重的错误提示(function a不能重复定义!!!),但是在Chrome中可以正常运行,运行的结果就如提示中所示。
考虑到环境导致的行为差异太大,在MDN上有关于块中定义函数时的建议:
应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
函数声明语句并非真正的语句,JS 规范只是允许它们作为顶级语句。它们可以出现在全局代码中,或者内嵌在其他函数中。相比之下,函数表达式是更大意义上表达式的一部分。函数表达式不会出现函数提升,放在哪里就在哪里执行,不会像函数声明那样随意放置具有误导性。
上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let
处理。
本文大量参考了阮一峰大神的文章,获益匪浅:ES6 的块级作用域