JS作用域

170 阅读3分钟

1. 作用域(Scope)

储存变量的规则叫作用域,用来限定代码的使用范围。

JS采用的是词法作用域(静态作用域),即在定义时就已确定,关注点在代码在何处声明。与之相反的是动态作用域,即在调用时确定,关注点在代码在何处调用。这样就理解为什么下面代码在词法作用域情况下输出3,在动态作用域下输出4了。

var a = 3
function f(){
    console.log(a)
}
function g(){
    var a = 4
    f()
}
g()

在ES6之前,JS只有全局作用域和函数作用域,ES6的带来了块级作用域。

(1)全局作用域

全局作用域可以理解为在页面的任何地方都能被访问到,页面不关闭,就一直不会释放,一直占用内存。

(2)函数作用域

函数作用域即为局部作用域,只能在函数内部被访问。

(3)块级作用域

ES6的新特性之一,花括号的内部就是块级作用域,定义在块级作用域中的变量在块外部不能被访问,拥有和函数作用域相同的行为。

2. 作用域链

了解作用域链,先了解JS运行前的词法分析,词法分析分三个步骤:分析参数 => 分析变量声明 => 分析函数声明。

代码片段一:

function f(a){
    console.log(a)
    var a = 3
    console.log(a)
    function a(){}
    console.log(a)
}
f(4)

函数在执行的过程中会生成活动对象(Active Object),简称AO。上面代码的词法分析如下:

分析参数:

(1)分析形参:AO.a = undefined (函数接受形参,并作为AO的属性,值为undefined)

(2)分析实参: AO.a = 4 (函数接受形参,添加到AO.a,覆盖之前的undefined)

分析变量声明:

AO中已经存在属性a,不做修改。(如果在上一步中没有属性a,则为AO.a赋值为undefined,如果有则不做任何修改)

分析函数声明:

AO.a = function a(){} (AO上如果有与函数名同名的属性,属性名会被函数覆盖)

 函数f执行的时候,在第一个console.log(a)之前,AO.a = function a(){},所以结果为 function a(){}。在第二个console.log(a)之前,a重新被赋值为3,所以结果为3。在第三个console.log(a)之前a没有被修改,所以结果为3。如下图所示:


代码片段二:

function g(b){
    console.log(b)
    var b = function (){
        console.log(b)
    }
    b()
}
g(6)

分析g()

       分析参数:

       (1)形参分析:AO.b = undefined

       (2)实参分析: AO.b = 6

      分析变量声明:AO中已经有b属性,并且有值,不做任何修改。

      分析函数声明:无函数声明。

      分析阶段最终结果是:AO = {b : 6 }

分析b()

       分析参数:无参数。

       分析变量声明:无变量声明。

       分析函数声明:无函数声明。

       分析阶段最终结果是:AO = {}

当b()的AO为空时,会向父级调用,父级的AO对象是g,g.AO.b = function (){ console.log(b) },所以会输出 function (){ console.log(b),(同一个变量,前面的赋值会被后面的赋值覆盖,所以输出的不是6)。函数整体输出结果如下图所示:


从代码片段二的分析过程可以看出。JS的每一个函数执行的时候,会先在自己创建的AO上找对应的属性值,如果找不到,则往父函数的AO上找,如果依然找不到,就继续向上层父级的AO中找,直到找到全局作用域windows,这条寻找AO的链就是JS中的作用域链。