[路飞]作用域的理解

435 阅读4分钟

作用域的理解

什么是作用域

作用域在我们编程中,可谓是随处可见,那么什么是作用域呢? 简单来说,作用域就是我们在代码中定义的变量所处的区域。作用域规定了我们如何查找变量,即确定当前执行代码对变量的访问权限。

词法作用域与动态作用域

词法作用域又称静态作用域,在JavaScript中的作用域规定的便是词法作用域,它和动态作用域可以大致理解为相反的两个东西。

词法作用域规定了函数的作用域在函数定义时就被确定了,而动态作用域则规定函数作用域在调用时才被确定。

通过下列代码来比较两者的区别:

// 词法作用域
var a = "first a"
function fa() {
    console.log(a)
}

function fn() {
    var a = "second a"
    fa()
}

fn()

让我们来分析一下上述JS代码。

首先代码中调用 fnfn 在内部又调用了函数 fa;

函数 fa 打印出变量 a 的值,那变量 a 的值是多少呢?

我们知道要访问变量 a 的值,一定要找到 a 的作用域,在函数中访问变量,首先会在函数中查找,是否能找到,否则就向外部查找,显然函数 fa 中没有定义变量 a ,我们要从函数外部访问;

这个时候词法作用域和动态作用域就有了区别,我们知道Javascript是词法作用域,他的作用域在定义的时候就被确定了,fa 会从书写函数的位置的作用域向上查找,如此看来 fa 输出的变量 a 应当是 "first a"

如果上述代码采用的是动态作用域,那么 fa 在没访问到变量 a 时,会从调用的位置作用域去查找,此时输出的应当是 "second a"

函数作用域

函数作用域指变量在声明它们的函数体以及这个函数体嵌套的任意函数体都是有定义的。

块级作用域

在ES6之后,JavaScript迎来了块级作用域的概念,为什么需要这个呢?我们来看下面的代码。

    var a = 'this is outer'
    if(true) {
        var a = 'this is inner'
    }
    console.log("a : ",a)  // 输出  a : this is inner

上述代码输出 a:this is inner ,显然这不是我们想要的,我们想要的是 a:this is outer , 这里 if(...){...} 内的变量从 if(...){...} 代码块内泄露,导致了变量污染。而在 ES6 后,这个问题被解决了, 看看 ES6 的写法。

    let a = 'this is outer'
    if(true) {
        let a = 'this is inner'
    }
    console.log("a : ",a)  // 输出  a : this is outer

这就是块级作用域的功劳,在 if(...){...} 代码块中定义的变量只能在该代码块及其内部嵌套的代码块中使用,花括号 {} 内部的环境称为块级作用域。

作用域链

在查找变量时,会从当前上下文中查找,如果没有找到,会从词法作用域上的父级的上下文中查找,一直查找到全局上下文。这样多个上下文的变量构成的链表叫做作用域链。

最开始我们就知道了JavaScript的作用域采用的是词法作用域,在创建的时候就确定了,而作用域链是在代码执行阶段,也就是执行上下文创建阶段确定的,我们来捋一捋两者的关系。

在函数的内部,有一个 [[scope]] 属性,当函数被创建的时候,会保存其所有父变量对象,也就是其父级的作用域,而此时的 [[scope]] 并不代表完整的作用域链。

在函数执行之后,会将活动对象(可以理解为函数内部的作用域)添加到作用域链的最前端,至此形成完整的作用域链。

通过下列代码理解:

function foo() {
    function bar() {
        ...
    }
}

在函数 foobar 被创建的时候,其内部的 [[scope]] 分别为

foo.[[scope]] = [globalContext]
bar.[[scope]] = [fooContext,globalContext]

当函数 foobar 执行时,创建作用域链,分别为:

foo.Scope = [fooContext].contact(foo.[[scope]])
bar.Scope = [barContext].contact(bar.[[scope]])