JS中的作用域是什么 | JS基础

2,511 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

关于整理的一些笔记,分别记录在专栏👉 【函数式编程】【前端工程化】【JS 基础】


js 中,每个变量是有状态的,比如分为全局变量或局部变量,变量声明或变量未声明。 如果变量没有状态,或许可以执行简单的操作,但是复杂操作会受到限制。

变量存储在哪里?程序需要时,如何找到它?

于是,就需要设计一套良好的规则来存储变量,方便以后更容易找到变量。

这套规则就是 “ 作用域 ” 。


编译原理

通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它 是一门编译语言

与传统的编译语言不同,JavaScript 不是提前编译的。大部分情况下编译发生在代码执行前的几微秒(甚至更短)的时间内。

传统编译语言的流程

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”

  • 分词/词法分析(Tokenizing/Lexing)
    • 这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。
  • 解析/语法分析(Parsing)
    • 这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract SyntaxTree, AST)。
  • 代码生成
    • 将AST转换为可执行代码的过程被称为代码生成。这个过程与语言、目标平台等息息相关。

JS中编译原理

比起那些编译过程只有三个步骤的语言的编译器,JavaScript引擎要复杂得多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。

JS 中的编译原理.png

🎈举个例子:

var name = 'wuyanfeiying';

  1. 分词/词法分析 将整个代码字符串分割成最小语法单元数组, 每一个元素都是一个最小的语法单元。 Tokens
[    {        "type": "Keyword",        "value": "var"    },    {        "type": "Identifier",        "value": "name"    },    {        "type": "Punctuator",        "value": "="    },    {        "type": "String",        "value": "'wuyanfeiying'"    }]
  1. 解析/语法分析 在分词基础上建立分析语法单元之间的关系, 把分词的结果按照相互的关系组成一个树形结构 SyntaxTree
{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "name"
          },
          "init": {
            "type": "Literal",
            "value": "wuyanfeiying",
            "raw": "'wuyanfeiying'"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "script"
}
  1. 代码生成 将AST转换为可执行代码的过程被称为代码生成。

var name = 'wuyanfeiying';的AST转化为一组机器指令,用来创建一个叫作 name 的变量(包括分配内存等),并将一个值储存在 name 中。


理解作用域

先来理解几个概念:

引擎

负责整个JavaScript程序的编译及执行过程。

编译器

负责语法分析及代码生成等工作。

作用域

负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

🎈当程序中有这么一段代码的时候: var name = 'wuyanfeiying';,会进入以下两个阶段:

  • 阶段1,主角 编译器
    • 编译时 ,会在同一个作用域的集合中,寻找是否有该名称的变量。
      • 有,编译器会忽略这个声明,继续编译。
      • 没有,当前作用域会声明一个新的名为 name 的变量。 紧接着,编译器会为引擎生成运行时所需的代码, 这些代码被用来处理 var name = 'wuyanfeiying'; 这个赋值操作。
  • 阶段2,主角 引擎
    • 引擎在运行时,首先会在当前的作用域集合中寻找是否存在一个叫作name的变量。
      • 如果是,引擎就会使用这个变量;
      • 如果否,引擎会继续查找该变量。

如果引擎最终找到了name变量,就会将'wuyanfeiying'赋值给它。 否则引擎就会抛出一个异常!

LHS 和 RHS

在执行一段代码 var name = 'wuyanfeiying'; 的时候,编译器会为引擎生成运行时所需的代码, 引擎执行它时,会通过查找变量 name 来判断它是否已声明过。 查找的过程由作用域进行协助,但是 引擎执行怎样的查找,会影响最终的查找结果

LHS,RHS 这两个术语就是出现在引擎对变量进行查询的时候。

有两种查询方式:

  • LHS(Left-hand Side): 赋值操作的目标是谁
  • RHS(Right-hand Side): 谁是赋值操作的源头

《你不知道的JavaScript(上卷)》中的🌰 寻找其中所有的 LHS 和 RHS:

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

🎈自己的理解:

001.png

如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。

作用域链

前面已经讲了,作用域是根据名称查找变量的一套规则

如果引擎在当前作用域中没有找到某个变量时,它咋办?

此时引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域为止。此时就形成了 作用域链

这里说的查找方式就是前面讲的:LHS和 RHS 查询

LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域,最后到达全局作用域,无论找到或没找到都将停止。

异常处理

  1. 引擎执行LHS查询时
  • 1.1 非 严格模式 下,如果在当前作用域没找到,就会继续往上一层作用域查找,如果在最终全局作用域也没找到。就会 创建一个具有该名称的变量
  • 1.2 严格模式下面, 和RHS一样,会抛出异常 ReferenceErroe 异常。因为 严格模式 禁止自动或隐式地创建全局变量
  1. 引擎执行 RHS 查询时
  • 2.1 如果找不到,也是会顺着 作用域网上找,最终没找到的话,会抛出异常 ReferenceErroe 异常。
  • 2.2 如果找到了,但是你对整个变量进行一个“不合理操作”,也会报错 TypeError.

002.png

在努力看书学习中,参考如下,仰望大佬们 ~ 🙆‍♂️

参考