【JS】作用域-1

君额上似可跑马

读《你不知道的JS》的笔记,有问题请指出。

简述作用域

每个编程语言的一个最基本的功能,就是可以声明变量,在变量里储存值,更改值,访问值。
随之一系列问题产生,这些变量存储在哪里,将来需要使用他们的时候如何获取他们?
这代表我们需要有一套设计良好的规则知道如何存储这些变量,并如何获取到他们,而这套规则,被称为作用域(Scope)。

编译和执行流程

不仅是在执行时会用到作用域,编译时也会用到作用域

编译

我们用 JS 写的代码称为源代码,是一种人类能看懂的语言,由于计算机只能读懂 0 和 1(二进制/机器语言),所以如果我们要在计算机上执行我们的代码,在执行代码之前,有个编译过程,目的是将源码编译成机器可以理解的机器码。

一般来说,编译分为 3 个步骤

  1. 分词/词法分析(Tokenizing/Lexing)
    将我们写的代码(字符串),分解成单独的、有意义的代码块,这种代码块也称为词法单元(token),只抽取有意义的部分


    注:分词≠词法分析,他们是有细微区别的,区分他们的一个最直接的方式就是,比如 var a = 2这行代码,当词法单元生成器(tokenizer)在判断 var 是单独的一个词法单元还是属于其他词法单元的一部分时,如果调用的是有状态(stateful)的解析规则,那么字符串被转换成词法单元的这个过程就被称为词法分析,否则,就是分词。
    待解决:这里面说的有状态是什么是什么意思?
  2. 解析/语法分析
    将上面已经转换好的词法单元转换成抽象语法树(AST)
  3. 代码生成
    将 AST 转换成机器可以识别的指令,让机器执行 var a = 2 这一系列任务。

编译完之后,就是执行代码了,对于 JS 来说,即使是 var a = 2 这行很简单的代码,在经过编译和执行这两个步骤时,也会涉及到作用域。

JS 的编译过程和其他语言的编译过程有点不同,在编译的第三步,也就是将抽象语法树(AST)转换成机器指令时,当编译器碰到声明操作时,如 var a = 2,编译器会询问作用域,在当前的作用域内,是否声明过 a,如果没有,就会让作用域在当前作用域内声明一个 a,否则,忽略该声明。

从上面可以看出,在 JS 引擎真正执行代码之前,编译过程中,编译器就会将变量先在作用域内声明好。

待解决:上面用的是 var,那么我用 let 声明变量的话,他也会帮我提前在作用域内声明好吗?

执行

代码编译完成后,就是执行步骤了,执行是由 JS 引擎执行,在执行 var a = 2 这行代码时,也会涉及到作用域:

便于理解,接下来我们把它们拟人化,每个人工作上都有自己的工作职责,他们也是一样,对于作用域来说,他的职责就是管理他这块区域的变量,这个区域里,存在哪些变量,变量分别存储的值是多少他都知道,也是他应该知道的,所以在之前的编译过程中,编译器在声明 a 之前,首先跑去问了作用域确认这块区域里还没有变量 a 后,才让作用域在这块区域内声明了变量 a。

而编译完成后,也就是代码执行过程,JS 引擎这时看到 var a = 2 后,也会先去问作用域,在当前作用域下,是否已经存在变量 a 了呢?

作用域检查了下当前的他这块区域内的变量,回答说,“嗯嗯,已经有了,是刚刚编译的时候编译器声明的”。

JS 引擎:“好嘞,既然有了,那我就不用重复声明了,我就赋个值就行了。”

LHS && RHS
JS 引擎在向作用域询问变量的时候,查询的方式还可以细分为 LHS 和 RHS,也就是让作用域查询这个变量是否存在,还是让作用域查询这个变量的值。

var a = 2 
console.log(a)

代码如上,还是以对话的形式

JS 引擎:作用域大哥,帮我看看你那里有没有变量 a 啊,我得给他赋个值。(查看变量容器本身是否存在,属于 LHS 查询方式)

作用域:找到了!这家伙在我这

JS 引擎: 谢谢了,再帮我看看 console 变量的值呢,我找找他里面有没有 log 这个方法(查询 console 变量的值,属于 RHS 查询方式)

作用域:有的,console 的值给你了,你看看吧

JS 引擎:好嘞,谢谢,我看一下。。有 log 这个方法!作用域,再帮我看下 a 变量的值呢,虽然我刚刚给他赋值了一个 2,但是我还是得确认一下(查询变量 a 的值,属于 RHS 查询方式)

作用域:嗯嗯,我看了下,他的值还是2

JS 引擎:好的,谢谢!

作用域链

有的时候,作用域是嵌套的

// 最外层为全局作用域
var a = 2 

function test() {
// 对于 JS 来说,一个函数会生成一个作用域(先不提 let 生成的块级作用域)
    console.log(a)
}

test()

对于 test 函数来说,他自己内部就有一个作用域 A,最外层有一个全局作用域。

JS 引擎在调用 test 函数的时候,由于需要打印变量 a,因此向 test 函数内部的作用域 A 求助,问他有没有看到变量 a,作用域 A 说没有看到,于是 JS 引擎向作用域 A 外层的作用域(全局作用域)求助,最终在全局作用域里找到了变量 a。

有时候并不一定当前作用域的外层作用域就是全局作用域,可能还嵌套有其他作用域,但是查询方式都是一样的,当前作用域找不到,就沿着嵌套的作用域往外找,直到找到全局作用域。

如果在全局作用域内也找不到这个变量呢?

那就可能会报错了,但是具体的报错信息还是有区别的:

  1. 查询变量本身(LHS 查询方式)

    • 在非严格模式下,找到全局作用域都没有找到这个变量,全局作用域就会直接帮我们在全局作用域内声明这个变量,也就是全局变量;
    • 在严格模式下,会抛出 referenceError 的错误,代表我们想引用的变量不存在。
  2. 查询变量的值(RHS 查询方式)
    如果我们对变量的值使用方式有错,比如 console.log 是一个函数,我们却想从里面获取一个不存在的属性,如 console.log.test.sss,从一个 undefined 的数据类型上获取 sss 属性,就会抛出 typeError 错误
阅读 494
36 声望
4 粉丝
0 条评论
你知道吗?

36 声望
4 粉丝
文章目录
宣传栏