你所不知道的Javascript(1)- 作用域是啥来着??你对作用域真的了解了嘛?🤔️

58 阅读7分钟

前言

身为一个刚毕业入行初出茅庐的臭小子,回看自己的前端学习生涯,感慨颇多。刚开始学习那会主要都是通过b站大学和查看博客来学习前端,这样的学习方法虽说一开始学习效果会很显著,但在后续深入会出现基础不稳固,会存在难理解的问题,虽然说现在行业不景气,但我也想输出一些文章来为一些真正想要入行的新人们学习和参考,同时也为自己的学习做一段记录。

这个系列主要是作为我在对《你不知道的Javascript》这本书的学习做了一些个人的理解,当然可能会存在一些理解错误或者误差,也欢迎大家一起在评论区进行讨论。

1.编译原理

在介绍作用域前,先来讲讲Javascript的编译原理,方便我们后面理解,对于这方面比较熟悉的同学可以跳过这一段。

在我们学习Javascript的时候,我们常听到一句话,Javascript是一门动态或解释执行语言,但其实Javascript更严格来说是一门编译语言,一般的编译语言在执行前跟普通语言肯定会有些差别,那就是需要编译为可以识别的语言。(感觉是像在说废话 😓)

简单来说 Javascript对于计算机来说就是一门外语,我们如果要让计算机去理解他,会对Javascript做一段编译的过程。

传统的编译语言在编译的时候会执行以下三个步骤:

  1. 分词/词法分析(Tokenizing/Lexing)
  2. 解析/语法分析(Parsing)
  3. 代码生成

这些概念其实会比较好理解,我们还是拿翻译来做比喻:

  1. 首先我们要翻译一段中文为英文,需要将这段话拆成好几个词语,例如她要吃饭,我们会拆成 吃饭 三个词语,这段过程在编译过程中就是 分词/词法分析(Tokenizing/Lexing),例如 var a = 10;这段代码中,我们会拆成 vara=10四个词法单元。

  2. 之后我们如果要对这几个词语进行拼凑,再拼凑这些词语的时候我们也要遵循一些规则,比如上面那段句子中,我们在翻译的时候 会变成 "She wants to eat.",而不是 "She want to eat.",同样我们在解析编译语言时,也会像以上翻译一样有一些特定的规则来进行解析和分析,也就是我们在学习Webpack常听到的AST语法树,当然如果不了解也没关系,我下面会简单对AST语法树做一个介绍:

  • 我们同样根据 var a = 10 这段代码来做解析,这段代码的抽象语法树中可能会有一个叫作VariableDeclaration的顶级节点,接下来是一个叫作Identifier(它的值是a)的子节点,以及一个叫作AssignmentExpression的子节点。AssignmentExpression节点有一个叫作NumericLiteral(它的值是10)的子节点。(这段直接照搬书里的原话,后面有机会会对这块进行深入了解。)
  1. 生成语法树后,我们就可以根据该语法树去生成对应的机器指令,去操控我们的系统给a分配一个变量来存储2的值。这段我们只需要简单了解就好。

通过以上我们就可以了解到一个基本的编译语言在他执行前会发生些什么,但我们的Javascript能作为一门需要在浏览器上随时就可以执行的语言,就可以确定来说,我们在执行的时候只会消耗很短的时间在编译上面,但这其中是如何去实现在几微秒,甚至更短的时间就实现了编译,这其中包含了大量的知识,这就需要后话去谈了。

2.作用域的作用

了解完以上概念后,我们就进入我们真正的主题: 作用域,作用域作为一个平时我们在开发非常常见的一个东西,要想了解作用域的作用,我们首先得知道,作用域的作用在哪,那想要知道他的作用在哪,我们得知道在编译里,它扮演了是一个什么样的角色。 现在我们先来介绍下引擎和他的两位好朋友:

  • 引擎:从头到尾负责整个JavaScript程序的编译及执行过程。
  • 编译器:引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。
  • 作用域:引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

当我们编写一段代码时,这三位好朋友就会来一段对话,确保咱们的代码能够变正确“翻译”为机器能够识别的语言。

还是拿 var a = 2;这段代码来做我们的例子,如果我们简单来说,只是单纯声明了a这个变量,并且给它赋值2,也就是让编译器来给这段代码解析为AST语法树并生成代码,然后引擎编译后执行,但其实不对,这里面还需要作用域的协助,总共会经历以下的步骤:(这里的原文非常好理解,我直接搬了)

  1. 遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。
  2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量(查看1.3节)。

简单来说,编译器会让作用域先声明一个变量,在赋值时,引擎会在作用域里找到这个变量并赋值。

3.作用域的域

了解完作用域的作用后,我们来看看作用域的“”体现在哪些地方?

作用域的嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

这段话非常好理解,如果我们有一定的开发经验也可以发现到,有时候在函数内部如果找不到一个变量,就会在外部的作用域里继续找对应的变量。

    const x = 2333
    function test(){
        console.log(x)
    }
    test() // 打印结果: 2333

原文也给了一个十分清晰的图片来给我们解释,这其中每一个小方块就是一个单独的作用域,他们互不侵犯,互不干涉,但如果我们在当前作用域里找不到对应的变量,我们就会到外部的作用域里去寻找。

image.png

总结

以上内容均取自《你不知道的Javascript》这本书,这本书对于一些需要去恶补基础或者基础不牢固的人都很有帮助,如果看完本篇文章有兴趣的可以去看看这本书的内容,里面的知识写得通俗易懂,我也不止被多位前辈们推荐去看这本书。 如果你是个懒人的话,也可以跟着我一块学习,之后我也会继续整理和总结知识,并继续发布文章,下一篇我们来聊聊引擎的查询(LHS和RHS)和我们在访问变量时,会遇到哪些类型的异常。