前端进阶之你真的了解JS的作用域跟上下文吗?

2,517 阅读5分钟
  • 作者:陈大鱼头
  • github: KRISACHAN
  • 链接:github.com/YvetteLau/S…
  • 背景:最近高级前端工程师 刘小夕 在 github 上开了个每个工作日布一个前端相关题的 repo,怀着学习的心态我也参与其中,以下为我的回答,如果有不对的地方,非常欢迎各位指出。

词法环境

词法环境(Lexical Environment) 就是 ECMAScript 的代码环境。

它由 环境记录(Environment Record) 和 外部引用 组成。

它会根据 ECMAScript 代码来动态创建。

环境记录绑定了其关联的词法环境。其一共分了5类,分别如下:

  • 声明环境记录(Declarative Environment Records) :声明环境记录(Declarative Environment Records) 绑定变量声明,其中包含:var、let、const、class、module、 import、function 。
  • 对象环境记录(Object Environment Records):对象环境记录(Object Environment Records) 记录每个对象增删改查。
  • 函数环境记录(Function Environment Records):函数环境记录(Function Environment Records) 每个 函数作用域 以及 上下文环境的具体情况。
  • 全局环境记录(Global Environment Records): 全局环境记录(Global Environment Records) 用于表示所有 ECMAScript Script元素 共享的 最外层作用域 。全局环境记录提供内置全局变量,全局对象的属性以及脚本中发生的所有顶级声明的绑定。
  • 模块环境记录(Module Environment Records):模块环境记录(Module Environment Records) 用于表示 ECMAScript模块 的外部范围以及绑定情况。

词法环境分了三类:

  • 全局环境(global environment):全局环境(global environment) 是一个没有外部环境的 词法环境。全局环境(global environment)的外部环境引用为 null。全局环境(global environment) 的EnvironmentRecord(环境记录) 可以绑定变量,并关联对应的 全局对象 。
  • 模块环境(module environment) :模块环境(module environment) 也是一个 词法环境 ,它包含模块顶级声明的绑定。它还包含模块显式导入的绑定。模块环境的外部环境是 全局环境(global environment) 。
  • 函数环境(function environment) :函数环境(function environment) 也是一个 词法环境 ,对应于 ECMAScript 函数对象的调用。函数环境(function environment) 可以建立新的此绑定。函数环境还支持 super 调用所需的状态。

执行上下文(Execution Contexts)

执行上下文(Execution Contexts) 是 ECMAScript 代码 运行时(runtime) 的上下文环境。

在同一时间内,每次只能有一个 执行上下文(Execution Contexts) 运行。这称为 运行执行上下文(running execution context)。

执行上下文堆栈(execution context stack) 用于跟踪 执行上下文(Execution Contexts) 。

正在运行的 执行上下文(Execution Contexts) 始终是此堆栈的顶级元素。

每当控制从与当前运行的执行上下文相关联的可执行代码转移到与该执行上下文无关的可执行代码时,就创建新的执行上下文。

新创建的执行上下文被压入堆栈并成为正在运行的 执行上下文 。

所有执行上下文的状态组件(State Components)

组件 作用
code evaluation state 用来判断当前执行上下文的状态
Function 判断当前执行上下文状态,如果当前上下文是函数,则上下文环境为此函数对象,否则则为null
Realm 相关代码从中访问 ECMAScript 代码的领域(Realm)记录。
ScriptOrModule 判断当前代码环境,如果没有所处的脚本或模块环境,就像在 InitializeHostDefinedRealm(原生方法) 中创建的原始执行上下文的情况一样,使上下文环境为 null。

作用域链(Scope chain)

作用域 负责收集和维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。-- 《你不知道的JavaScript(上卷)》

从上面两个话题,我们可以知道,除 全局作用域(global scope) 外,每个作用域始终连接到其背后的一个或多个作用域,从而形成 作用域链(scope chain) 。全局作用域(global scope) 没有任何父级,这也是有意义的,因为它位于层次结构的顶部。

我们看下面的代码:

const bestAvenger = "Iron man";
function a() {
  const bestActor = "Neymar";
  console.log(bestAvenger); // output:Iron man
    
  function c() {
    const bestProgrammingLanguage = "Html";
    console.log(bestActor); // output:Neymar
    b();
  }
  c();
}
function b() {
  console.log(bestProgrammingLanguage); //**not defined error**
}
a();

上面的代码会报错如下:bestProgrammingLanguage is not defined.

如上所述,作用域链(Scope chain) 始终是 词法 创建的。作用域 的父节点由 执行上下文(函数) 在代码中的词法或物理位置定义。

上面代码的 作用域链(Scope chain) 如下:

词法环境 作用域链
全局作用域下的G G = G
G下的A A = A + G
G下的B B = B + G
G下的A,A下的C C=C+A => C+A+G

其实总结起来就是:

  • 每当编译器遇到变量或对象时,它都会遍历当前执行上下文的整个 作用域链(Scope chain) ,如果没有在那里找到它,就遍历 原型链(prototype chain),如果也没有找到,它会抛出未定义的错误。
  • 编译器通过查看函数在代码中的位置来创建函数的作用域。
  • 编译器创建 作用域链(Scope chain),全局作用域(Global Scope) 位于此层次结构的顶部。
  • 当代码中使用变量时,编译器会向后查看作用域链,如果找不到,则抛出未定义的错误。

参考资料

  1. ECMA文档
  2. Javascript Scope Chain and Execution Context simplified

如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。 鱼头的微信号是:krisChans95 也可以扫码关注公众号,订阅更多精彩内容。

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/base/qrcode-all.png