JavaScript 作用域(scope)与提升(hoisting)机制
作用域(scope)
在ES6之前,仅有两种作用域:
- 全局作用域
- 函数作用域
在ES6新增了新的作用域,故ES6支持的scope为3种:
- 全局作用域
- 函数作用域
- 块作用域
全局作用域
全局作用域就是字面意思,即变量或函数在任意上下文中都可被访问。同时全局作用域的变量或函数等价于定义了window
的属性和方法。例如:
var a = 5;
function fun(){
console.log(a); //a具有全局作用域,此处可访问。
}
// 同时上述定义同时定义了
// window.a = 5;
// window.fun = function(){
// console.log(a); //a具有全局作用域,此处可访问。
// }
如下图所以,验证了上述说法。
函数作用域
函数作用域表示变量或函数的定义仅在当前函数上下文中生效。即函数外部无法访问函数内部定义的变量和函数。例如:
function fun(){
var a = 5;
}
fun();
console.log(a); //此处会报错,因为a未定义
如下图所示,由于a只有函数作用域,所以函数外部使用a时呈现未定义状态。
块作用域
ES6中新增了let声明,用于定义具有块作用域的变量。先看个例子来理解为什么需要块作用域。
function fun(a){
if(a==1){
var b = 4;
}
console.log(b);
}
fun(1);
这个例子的输出如下,可以看到在if块中定义的变量b,跳出if后仍然可以正常访问。为什么会出现这个现象,这里就设计JS提升(hoisting)机制。关于hoisting,将在后面展开讨论。
为了防止出现上述的块中定义的变量跳出块后仍然可以访问的现象。ES6中新增了let声明,使用let声明的变量只具有块作用域。如下:
function fun(a){
if(a==1){
let b = 4;
}
console.log(b);//b未定义,报错
}
fun(1);
提升(hoisting)机制
上面的例子中已经提到了使用var定义的变量不具有块作用域,其本质是因为JS的提升机制,在将hoisting之前,我们要先讲下JS的解析。
JS的解析与运行
JS作为脚本语言,并不存在编译的过程,即运行一行解析一行,所以JS的解析在运行时进行。所以执行一段JS代码大致分为三部分:
- 语法分析
- 预编译
- 执行过程
语法分析就是查看是否存在语法错误,这里不多说。接下来重点聊聊预编译的过程。
预编译
JS的预编译始终是针对代码块进行的。比如整个<script>
中的内容属于一个代码块,又或者写的function也是属于一个代码块。
解析过程JS首先扫描代码块生成词法环境(LexicalEnviroment, LE) ES5中使用lexical environment
来管理静态作用域,而不再是ES3中的AO/VO
。例如上面的例子:
var a = 5;
function fun(){
console.log(a); //a具有全局作用域,此处可访问。
}
// 同时上述定义同时定义了
// window.a = 5;
// window.fun = function(){
// console.log(a); //a具有全局作用域,此处可访问。
// }
初始时JS会预编译整个代码块,由于始终是针对代码块编译,所以并不会对fun内部进行预编译。这段代码的LE如下:
LE :{ //此处的LE相当于window
variable: {
a: undefined
},
function: {
fun: undefined
}
}
当调用fun函数时才会对生成fun函数的词法化境。所以上述LE就解释了为什么具有全局作用域的变量在window下也会存在一份拷贝。同时也解释了为什么会分为函数作用域和块作用域。
预编译部分具体可以看这篇文章。JS词法环境
hoisting
hoisting就是发生在解析时,hoisting机制就是将变量/函数的声明提前。还是上面的代码。
var a = 5;
function fun(){
console.log(a);
}
这段代码经过解析后,由于hoisting的存在,等价如下的代码:
var a = undefined;
var fun = undefined;
a = 5
fun = function (){
console.log(a);
}
可以看到,经过解析后,将a和fun的声明都提前到了最上方。
PS: 这里需要注意,hoisting只会提升函数声明,不会提升函数表达式,即var fun = function(){}不会被提升。
几个例子
基于scope以及hoisting的存在,我们就不难理解上述例子为什么会如此了。接下来我们再看几个例子加深对hoisting的理解。
例子1
var sum = 0;
function fun(b){
if(b==0){
var sum = 100;
}
console.log(sum);
}
fun(1);
对于上面的例子会输出什么。很多人可能会说输出0,因为有全局变量sum的存在。但是实际上上面代码会输出undefined。
出现undefined的原因还是因为hoisting机制,fun代码块在解析的时候由于hoisting机制的存在,会被解析成如下代码:
var sum = 0;
function fun(b){
var sum = undefined; //提升
if(b==0){
sum = 100; //赋值
}
console.log(sum);
}
所以当调用
“`fun(1)“`时,`sum`值仍为`undefined`。
例子2
我们看下下面这个例子会输出什么
console.log(fun);
function fun(a){
console.log(a);
}
var fun = 5;
结果如下图,当定义发生冲突的时候,hoisting处理的原则并不像变量赋值那样,遵循先后的原则。而是遵循函数优先原则,即存在命名为fun
的函数和变量同时存在时,保留函数,舍弃冲突的变量。
总结
要彻底理解JS的scope和hoisting,只要记住以下三点即可:
1、所有申明都会被提升到作用域的最顶上
2、同一个变量申明只进行一次,并且因此其他申明都会被忽略
3、函数声明的优先级优于变量申明,且函数声明会连带定义一起被提升
发表评论