在块语句中的函数声明
下面这个示例,在JS中会输出什么呢?
{
a = 1
function a() {
}
a = 2
console.log(a) // 输出1
}
console.log(a) // 输出2
只要跑一下程序,就会知道答案是“先2后1”:
2 // 输出1
1 // 输出2
然而“输出2”为什么会是1
呢?
这个问题得从远古时期的JavaScript说起,那个时代还没有ES6所谓的“环境(Environment)”,与之相似的概念称为“作用域”。也因为这个缘故,在通常的JavaScript中,把这个在ES6之后才发展起来的东西称为“块级作用域”。
然而仅仅是“块级作用域”,是不足以解释上面这个例子的独特之处的。
在没有块级作用域之前
在ES5及其之前的时代,JavaScript只有“函数”和“全局”两个级别的作用域。这带来了许多的问题,其中之一是“eval执行在哪里”的问题,另一个就是今天要讲到的“在语句中的函数声明的作用域在哪里”的问题。例如:
function foo(tag) {
if (tag) {
function bar() {
console.log("Hello")
}
}
else {
function bar() {
console.log("World")
}
}
bar();
}
foo(true); // 输出1
foo(false); // 输出2
在这个例子中,从正常人的思维来说,我们会认为输出下面的内容:
Hello // 输出1
World // 输出2
但是在没有块级作用域之前,不同的JS引擎对上述代码的理解并不相同。例如在微软的JScript中,bar就被作为函数foo()的内嵌函数来理解,因此代码中事实上是连续两次声明了函数bar(),所以只有最后一个声明有效。因此无论foo(true)
还是foo(false)
都将输出World
,上面的结果也就是两个World
。
然而Mozilla Firefox提出不同的看法,他们认为应该在结果上与“正常人的思维”一致,因此在ES5之前的时代他们就提出并实现了“(类似)块级作用域”的概念,这里被处理成了“条件声明”,也就是说bar()这个函数在foo()中将是未被声明的,只有执行到了if
的某个分支之后,它才会“正式地”声明出来。——在它被“正式地”声明出来之前,bar
这个函数名并不存在,无需理会。
这其实带来了更多的问题,例如如果函数bar()尚不存在,那么在if
条件语句之前就会访问到全局,而执行了语句之后,就会访问到foo()函数内的声明。——不要忘了,如果动态地在函数内使用eval语句,还会动态地创建出“foo”这样的名字来,所以……没有人能真正搞懂函数foo()中有没有bar()这个名字,又或者调用函数foo()的时候会不会访问到全局中的bar——函数或者变量名等等。
所以,在“块级作用域”出来之前,上面的示例的含义就成了悬案。Mozilla Firefox也只是在1.6+的JavaScript扩展语法中支持它们(Spider Monkey JavaScript 1.5支持到ECMAScript 3)。
有了ES6的块级作用域,问题却更大了
好了,ES6提出了块级作用域,也就是说有了“语句”这个级别的作用域。但是有了这个东西,对于上述示例来说,问题却更加复杂。
NOTE1:在《JavaScript语言精髓与编程实践》的第一、二版中都讨论过,JavaScript 1.3~1.5的语言设计中没有补全“语句”和“表达式”这两个级别的作用域。而从ES6开始,才有了“语句”级别,亦即是所谓的“块级作用域”。
NOTE2:并不是只有块语句才有“块级作用域”,也并不是所有的语句都有块级作用域。可以参见《JavaScript语言精髓与编程实践(第3版)》“4.4 语句与代码分块”以及表4-6。
这又得从另一个历史问题来讲起了。首先,与“块级作用域”一起出现的、由块级作用域限制的是let/const语句声明的变量名,而在传统上,函数名是与“var变量名”类似的名字。——因此,函数名事实上也是没有块级作用域的,它“理论上”应该和“var变量名”一样放到函数或全局级别的作用域。这常常被称为“提升(Hoisting)”,例如:
function foo() {
console.log(x); // undefined
{
// 块语句
var x = 100;
let y = 200; // `y`在块语句的作用域中
function bar() {
...
}
}
console.log(x); // 100, `x`在函数的作用域中
}
既然其中的“var变量名”被提升到了“foo()函数的作用域”,那么“与它性质相似的”bar()函数也应当被提升到foo()函数中啊。
——这看起来很合理啊。然而大家还记得吗,Mozilla firefox在js1.6+的时代已经实现过了所谓的“条件声明”,那么这就意味着Spidermonkey系的js引擎把这样的bar()函数声明留在了“语句一级”,而反过来,这一次反倒是JScript占了优势,他们的设计与新规范站到了一起。
然而两种主要的引擎都有极大量的用户,尤其那已经是Firefox与Chrome开始联手颠覆IE的市场的时代了,所以两种声音都有足够的话语权,最终在这个问题上