作用域和作用域链

297 阅读5分钟

作用域是什么

几乎所有编程语言最基本的功能之一,就是在变量中存储值,并且在稍后取出或者修改这些值,如果没有这种功能,我们无非只能对一些值做一些即时的操作,比如说

1 + 1 = 2;
console.log('1');

在这里,我们引出了编程语言中最具有生命力的概念——变量。那么,这些变量是如何存储在计算机中的?它存储在哪里?更重要的是我们如何才能找到他们?这些问题需要通过一系列的规则才能够回答,这些规则规定了如何查找变量(也就是确定当前执行代码对变量对访问权限),这一系列规则就是——作用域

常见作用域

作用域通常有两种工作模型,词法作用域动态作用域,所谓词法作用域指的是程序源代码中定义变量的区域,当词法分析器处理代码时,会保持作用域不变,动态作用域是在代码执行时才会明确。JavaScript 就是使用词法作用域,举个例子看下:

var value = 1;
function foo() {
    console.log(value);
}
function bar() {
    var value = 2;
  foo();
}
bar(); // ??
  • 如果 JavaScript 采用词法作用域,那么执行到 foo 函数,根据变量书写位置,会找到全局定义的 value 1
  • 如果 JavaScript 采用动态作用域,那么执行到 foo 函数,根据运行的位置,会找到 bar 定义的 value 2JavaSript 中具有欺骗词法作用域的方法,比如说 evalwith 语句(不在本文梳理线中)。

我们在 JavaScript 中所熟知的函数作用域全局作用域都是基于词法作用域而形成的一种变量访问权限的定义。在函数作用域就是函数内部定义的变量只能在函数内部可以访问,全局作用域则在整个程序生命周期都可以访问。函数作用域的存在弥补了 ES6 出现之前, JavaScript 没有块级作用域的问题。类似 C 的编程语言中,花括号内的每一段代码都具有各自的作用域,而变量在声明它们的代码段之外都是不可见的,称之为块级作用域。在 JavaScript 中,取而代之的是函数作用域。

执行上下文

那么词法作用域到底是怎么生效的呢?这就要从执行上下文说起,所谓执行上下文就是当前当前程序运行的环境,它具有三种类型,全局执行上下文、函数执行上下文、 eval 执行上下文。所有的执行上下文都包括两个阶段,创建阶段执行阶段

创建阶段

上述提到的每个上下文包括以下几部分组成

  • 变量对象
  • 作用域链
  • this 绑定 在《JavaScript高级程序设计》中介绍到,每个执行环境(执行上下文)都有一个与之关联的变量对象( VO ,存储了上下文中定义的变量和函数声明),变量对象对于程序而言是不可读的,只有编译器才有权访问变量对象。为了能够清楚地看到这一切,变量对象便以某种具象的、程序可以接触的方式存在着。

比如对于全局执行上下文来说,全局变量对象被具象成 window 对象

console.log(this === window)

而对于函数执行上下文来说,程序不可访问变量对象,因此出现了活动对象的概念。在函数被激活的时候,变量对象便转化为活动对象( Activation Object ),这个 AO 由特殊对象 Arguments 对象进行初始化,举个例子

function foo(a) {
  var b = 2;
  function c() {}
}
foo(1);

初始化得到的 AO 就是

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
}

执行阶段

完成在创建阶段初始化出来的值的修改,并执行代码

作用域链

正如上文提到的,每个执行环境除了创建变量对象以外,还会创建作用域链,保证了有权访问的变量和函数的有序访问。每个执行上下文的作用域链由当前环境的变量对象以及父级环境的作用域链构成。那么作用域链是如何构建起来的呢?从代码来进行详细解释

function test(num){
    var a = "2";
    return a+num;
}
test(1);
  • JS 在执行的第一步会创建一个全局执行上下文,由于全局上下文没有父级环境,因此全局作用域链只包含变量对象 test 函数
global scopeChain = variable object // global scopeChain = [test]
  • 当代码执行到 test(1) 时,开始了函数执行上下文的第一个阶段——创建阶段,函数会初始化一个内部属性 [[scope]] ,该属性指向全局作用域链
test[[scope]] = global scopeChain
  • 在执行阶段,会开始构建函数的作用域链,第一步要做的事情就是复制 [[scope]] 属性的值
test.scopeChain = [test.[[scope]]]
  • 将变量对象压入作用域链
test.scopeChain = [test.variableObject, test.[[scope]]];

至此整个作用域链构建完毕。

参考资料