是我
lllhy

JavaScript 作用域(scope)与提升(hoisting)机制

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具有全局作用域,此处可访问。
// }

如下图所以,验证了上述说法。

image-20210331150301178

函数作用域

函数作用域表示变量或函数的定义仅在当前函数上下文中生效。即函数外部无法访问函数内部定义的变量和函数。例如:

function fun(){
    var a = 5;
}
fun();
console.log(a); //此处会报错,因为a未定义

如下图所示,由于a只有函数作用域,所以函数外部使用a时呈现未定义状态。

image-20210331150726882

块作用域

ES6中新增了let声明,用于定义具有块作用域的变量。先看个例子来理解为什么需要块作用域。

function fun(a){
    if(a==1){
        var b = 4;
    }
    console.log(b);
}
fun(1);

这个例子的输出如下,可以看到在if块中定义的变量b,跳出if后仍然可以正常访问。为什么会出现这个现象,这里就设计JS提升(hoisting)机制。关于hoisting,将在后面展开讨论。

image-20210331151249154

为了防止出现上述的块中定义的变量跳出块后仍然可以访问的现象。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。

image-20210331154750869

出现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的函数和变量同时存在时,保留函数,舍弃冲突的变量。

image-20210331155315941

总结

要彻底理解JS的scope和hoisting,只要记住以下三点即可:

1、所有申明都会被提升到作用域的最顶上

2、同一个变量申明只进行一次,并且因此其他申明都会被忽略

3、函数声明的优先级优于变量申明,且函数声明会连带定义一起被提升

赞赏
没有标签
首页      所有文章      JavaScript 作用域(scope)与提升(hoisting)机制

lhy

文章作者

普通人。

发表评论

textsms
account_circle
email

lllhy

JavaScript 作用域(scope)与提升(hoisting)机制
JavaScript 作用域(scope)与提升(hoisting)机制 作用域(scope) 在ES6之前,仅有两种作用域: 全局作用域 函数作用域 在ES6新增了新的作用域,故ES6支持的scope为3种: 全局作用域…
扫描二维码继续阅读
2021-03-31