《你不知道的javascript》打卡学习笔记📒(1)

137 阅读5分钟

作用域

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 3 天,点击查看活动详情

《你不知道的javascript》 读书学习笔记打卡📒

1.什么是作用域?

书上描述作用域是一种规则。

几乎所有的编程语言都具备一种能力,也就是能够存储变量当中的值,并且能在之后对其进行访问和更改,但是这些变量是存储在哪里?并且之后程序又是如何找到它们? 这个如何存储决定了之后程序会以哪种规则去找到这个变量,而这种规则就是作用域

所以下面就来看下程序源代码在执行前,会对变量在哪里存储,以及如何进行存储(也就是如何制定某个变量得作用域)

1.1. 了解编译原理

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

截屏

这里讨论javascript,javascript引擎进行编译的步骤和传统的编译语言是很相似的,但是在有些地方可能要复杂的多,比如词法分析和代码生成阶段由特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等

接着详细介绍下上面的三个步骤

var a = 2;
  • 分词/词法分析 在这个阶段会将字符组成的字符串分解成有意义的代码快,这些代码快会被成为词法单元(token)。像var a = 2;。这段程序会被分解成 var、 a、=、2、; (空格取决于在语言中是否起到作用)

  • 解析/语法分析 parsing 这个过程是将词法单元流(是个数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。(抽象语法树 AST)

tokens[数组]

[
    {
        "type": "Keyword",
        "value": "var"
    },
    {
        "type": "Identifier",
        "value": "a"
    },
    {
        "type": "Punctuator",
        "value": "="
    },
    {
        "type": "Numeric",
        "value": "2"
    },
    {
        "type": "Punctuator",
        "value": ";"
    }
]

AST [转换成对象更清楚些]

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "value": 2,
            "raw": "2"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "script"
}
  • 代码生成 将AST转换成可执行的代码得过程叫做代码生成

var a = 2;的AST转化成一组机器指令,用来创建一个a的变量,并为它分配内存,最后将一个值也就是2,存到a中。

1.2. 理解作用域

上面我们了解了编译语言在执行前会有的三个步骤,但是一直没说到作用域那块。最开始说的在哪里存储,如何存储,如何查找,现在让我们来了解一下作用域吧。

首先了解下几个重要的组成部分:

  • 引擎 复杂javascript程序的编译和执行过程
  • 编译器 负责编译前的准备工作,也就是前面说的编译流程
  • 作用域 负责收集并维护由所以声明的标志符(变量)组成的一系列查找,并执行非常严重的规则---去确定当前执行的代码对这些标志符(变量)的访问权限。

再次拿下面这句代码作为例子

var a = 2;

当引擎看到这句代码时,它会认为这句代码是有两个完全不一样的声明

  • 一个是由编译器在编译时处理
  • 一个则是由引擎自己处理

来看下具体协同流程:

  1. 首先当然是编译器会将这段diam分解成词法单元,然后生成AST树结构。

  2. 在生成代码得过程中,首先会遇到var a,如果已经由一个该名称的变量存在于同一个作用域集合中,那么编译器会忽略该声明,继续编译。如果没有声明,那么它会要求在当前作用域的集合里面声明一个新的变量,命名为a

  3. 接着编译器将完成代码生成这一步骤,这些代码会被用来处理a = 2,也就是这个赋值的操作。

  4. 最后交给引擎来执行,引擎首先也会查找,在当前作用域集合内是否存在一个叫a的变量,如果有,就使用。如果没有就继续查找 (这边是查找哦,那么如何查找呢?等会会说到),如果最终找到了a变量,就为它赋值,否则就会抛出一个错误异常。

总结下:变量得赋值操作会有两步操作

  • 编译器会在当前作用域中声明一个变量(存在就不用)
  • 运行时引擎再去作用域中找,如果找到就赋值,没找到报错

1.3 引擎查找变量过程

上面说了,引擎在执行代码得时候,会去作用域里面查找看下是否有这个变量,如果没有它会继续查找,那么它是如何查找的呢?

查询分成两种,一种是LHS,一种的RHS,是一个赋值的左侧或者右侧。

当变量出现在赋值操作的左侧进行LHS查询,当变量出现在赋值操作的右侧的时候,就采用RHS查询。

更准确的说法:RHS查询与简单的查找某个变量得值是相似的,但是LHS是找到变量本身,从而对其赋值

console.log(a) // 我们只是需要查找到a的值,而并非a的本身,因此用的LHS
a = 2;  // 我们需要找到a的本身,并且为它赋值成2,因此用的就是LHS

1.4 作用域嵌套

刚才说到的,如果引擎没有在当前作用域下找到比那里,会继续往外面的作用域找,举个例子。

function foo(a) {
    console.log(a + b);
}
var b = 3;
foo(2);

在函数foo的作用域中,对b进行RHS查询是找不到的,因此引擎会往外找,找到全局作用域var b = 3,找到了,就会拿去使用。

作用域的最外层是全局作用域,如果还没找到,就会报异常。

简单了解了一下变量赋值的一个流程,从编译阶段,到执行,到查找作用域中的变量,大致了解了一些。