你的浏览器不支持canvas

Enjoy life!

javascript - 作用域与作用域链

Date: Author: JM

本文章采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。

一、作用域(scope)

1.1 什么是作用域

  • 我们可以将作用域视为一套规则 — 用来管理引擎如何在 当前作用域 以及 嵌套的子作用域 中根据 标识符名称 进行 变量查找

1.2 作用域的分类

  • 作用域有:全局作用域函数作用域

1.3 作用域与执行上下文的辨析

  • 作用域 与 执行上下文 是完全不同的两个概念。
  • javascript 代码的整个执行过程分为两个阶段:
    • 代码编译阶段:编译阶段由 编译器 完成 —- 将代码翻译成可执行代码,在这个阶段,作用域规则会确定
    • 代码执行阶段:执行阶段由 引擎 完成 —- 主要任务是执行可执行代码,执行上下文 在这个阶段创建。

1.4 作用域与作用域链之间的关系

  • 作用域链是作用域这套规则的实现。

二、标识符解析(Identifier Resolution)

  • 标识符: 变量名或者函数名。
  • 标识符解析:是决定变量或者函数声明属于哪一个变量对象的过程。
  • 标识符解析过程包含了与变量名对应属性的查找。
    • 即:作用域中变量对象的连续查找,从作用域链最深的上下文开始,一直到作用域链最上层(其实就是 Global Scope Chain,全局作用域链)。
  • 如果两个变量是同名的,那么同名的局部变量的查找比在父环境中的同名的变量查找有更高的优先级。 请看下例:
function foo() {
  
  var y = 20;
  
  function bar() {
    var y = 30;
    
    console.log(y);
  }
  
  bar();
}
  
foo(); // 30

三、作用域链介绍

2.1 什么是作用域链

  • 作用域链是内部上下文所有变量对象【包括父变量对象】的列表

2.2 作用域链创建时间

  • 作用域链在执行上下文的创建阶段被创建。【即:基于函数被调用时创建

2.3 作用域链的组成成分

  • 作用域链由当前环境与上层环境的一系列变量对象组成。

2.4 作用域链的作用

  • 作用域链的作用:保证对执行环境有权访问的所有变量和函数的有序访问

四、与作用域链相关的[[scope]]属性

4.1 定义

  • [[scope]]属性是 所有父变量对象的层级链, 处于当前函数上下文之上,在函数被创建时存于其中
  • 定义作用域链的式子:Scope = AO|VO + [[Scope]]
  • 其实我们也可以将 Scope 看作一个数组:var Scope = [VO1, VO2, ..., VOn]; // scope chain
  • 活动对象(AO)必须是作用域链数组的第一个元素:Scope = [AO].concat([[Scope]])。这点对于标识符解析很重要。

4.2 [[scope]]属性创建时间

  • [[scope]]属性在函数创建时存于其中

4.3 用途

  • 通过这个函数的内部属性我们 可以访问相对于当前执行上下文来说更高一层的变量对象

4.4 注意

  • [[Scope]]属性我们不能使用,仅供javascript引擎使用。
  • [[Scope]]在函数创建时被存储 —- 它是静态的,不可变的,直至函数被销毁。
    • 即:函数可以永远不被调用,但是 [[Scope]]属性已经存在了,并存储在函数对象中。
  • 通过new Function() 创建的函数,其[[Scope]] 属性只能包含全局活动对象。请看下面代码 — 全局变量对象中并不存在 y 变量,所以 y is not defined
var x = 10;
function test() {
  var y = 10;
  
  var newFn = new Function('console.log(x, y)');
  
  newFn(); // 10, y is not defined
}

test();

4.5 实例

var x = 10;

function foo () {
  var  y = 20;
  
  function bar () {
    var z = 30;
    
    console.log(x + y + z);
  }
  
  bar();
}

foo(); // 60
  • 大概过程:
    1. 全局上下文对象创建
    2. foo 创建时,foo函数的 [[scope]] 属性被创建。
    3. foo 函数被激活,生成foo上下文的活动对象。
    4. 形成 foo 上下文的作用域链。
    5. 内部函数 bar 创建时,bar[[scope]] 属性被创建。
    6. bar 函数被激活时, 生成bar 函数的上下文活动对象。
    7. 可获取 bar 函数的作用域链。
    8. xyz 等标识符进行解析的过程。
  • 详细过程如下图所示:

relationship-map

  • foo 函数的作用域链【可看 [[Scopes]] 属性】:

relationship-map

  • bar 函数的作用域链【可看 [[Scopes]] 属性】:

relationship-map

五、[[Scope]] 与闭包的关系

5.1 例1

  • 请看下述代码
var x = 10;
 
function foo() {
  console.log(x);
}
 
(function () {
  var x = 20;
  foo(); // 10, but not 20
})();
  • 上述代码输出的值是 10 而不是 20,原因如下:
    • 我们知道,[[Scope]] 这个函数的内在属性在函数被创建时就已经被创建了,它是不可变的,是静态的;直到函数对象被销毁为止它才会消失。
    • 因此, 在 foo 函数被创建时,它内部的 [[Scope]] 属性已经被创建,可从下面代码看出 foo 函数的 [[Scope]] 属性的值 为 globalContext.VO, 根本不可能读取闭包中 x的值 20,所以最终输出值为 10
      • 即:x 变量在 foo 函数的 [[Scope]] 中找到,即变量查找在函数创建时定义的词法(闭包)链,而不是 调用的动态链 变量将被解析为20)被使用。
// 全局变量对象

globalContext.VO === Global = {
    x: 10,
    foo: <reference to function>
}

// foo 函数的 [[Scope]]
foo.[[Scope]] = [
  globalContext.VO
]
  • 从下图你也可以看出,[[Scope]] 属性里就只有 Global

relationship-map

5.2 例2

function foo() {
 
  var x = 10;
  var y = 20;
 
  return function () {
    alert([x, y]);
  };
 
}
 
var x = 30;
 
var bar = foo(); // anonymous function is returned
 
bar(); // [10, 20]

5.3 总结

  • 闭包与[[Scope]] 属性有着十分紧密的联系:闭包产生后,可以根据每个函数的 [[Scope]] 属性找到其父级链,父级链再找其父级链,直到全局链为止。

对于本文内容有问题或建议的小伙伴,欢迎在文章底部留言交流讨论。