【共读】你不知道的js 上 (1)作用域是什么?

1,571 阅读9分钟

前言

本文会用思维导图的形式列出本书该部分的知识点(剔除案例),构建知识脉络。由于是导读,正文部分只会列举部分的内容。本文适合未读过此书的同学参考,另外读过此书的同学,如果能纯熟得答出文初的问题,那么相信您对于这部分的内容可以说是记忆深刻了。

建议在阅读前了解作者的生平,背景,核心贡献及思想。相信会对理解本书以及后续的选书读书会有所帮助。

豆瓣读书

问题

  1. 谈谈你对作用域的理解。
  2. 引擎,编译器,作用域分别是什么?它们如何共同协作?
  3. 介绍一下 ReferenceError 异常类型 和 TypeError 异常类型。
  4. 谈谈你对作用域链的理解。

作用域是什么?

谈谈你对作用域的理解。(个人理解,求拍砖)

作用域收集并且维护由所有声明的标识符组成的查询,有自己非常严格的规则确定当前执行代码对标识符的访问权限。

JavaScript 是一门编译语言,在执行代码前的编译中,编译器需要和作用域沟通是否存在某个变量来决定创建还是忽略。

接着引擎需要为变量赋值,它会通过 LHS查询 或者 RHS查询 查找变量,在当前作用域找不到时还要沿着作用域链一直往上往上找,如果在最外层的全局作用域也找不到,那么抛出叫做 ReferenceError 的异常

这里如果是使用 LHS查询 当全局作用域也不存在查找的变量时会自动创建并返还给引擎。

一、简单介绍编译原理

JavaScript 是一门编译语言,但是它不像传统语言那样仅仅只经历编译的三个步骤,分词/词法分析,解析/语法分析,代码生成。

我们的 JavaScript 引擎要复杂的多,JavaScript 会用尽各办法(比如用JIT)来保证性能最佳。并且我们要记住的是任何 JavaScript 代码片段在执行前都要进行编译,大部分情况下编译发生在代码执行前的几微秒(甚至更短)。

  • 分词/词法分析:这个过程会将字符串分解成对编程语言来说有意义的词法单元(代码块)

    var a = zhengyang;
    

    以上代码会被分解成 var、a、=、; 空格是否会被当成语法单元取决于它是否在此处具有意义。

    分词和词法分析其实是一件事,词在这里指的是带有某种归类的字符串,词通过词法来划分,分词是目的,词法分析是手段。

  • 解析/语法分析:这个过程会将词法单元流转化成一个由元素逐级嵌套所组成的代表程序语法结构的树,这个树叫做抽象语法树(Abstract Syntax Tree,AST)。

    var a = zhengyang;
    

    经过分词/词法分析,我们把划分好的代码块组成抽象语法树,它有一个 VariableDeclaration(变量声明) 的顶级节点,下面是一个 Identitier (值为a)的子节点和一个叫做 AssignmentExpression(赋值表达式)的子节点,AssignmentExpression 有一个叫做 NumericLiteral(数值文字)的值为 2 的子节点。

  • 代码生成:将 AST 转化为课执行代码的过程被称为代码生成。

    简单来说就是将 var a = 2AST 转化为一组机器指令来创造一个叫做 a 的变量,并将一个值存储在 a 中。

二、引擎,编译器,作用域分别是什么?它们如何共同协作?

  1. 引擎:从头到尾负责整个 JavaScript 的编译及执行过程。

  2. 编译器:引擎的同事负责语法分析及代码生成等脏活累活。

  3. 作用域:引擎的另一位同事,负责收集并且维护所有声明的标识符组成的一些列咨询。它由一套非常严格个规则,确定当前执行的代码对这些标识符的访问权限。

  4. 三位一体工作流:

var a = zhengyang
  • 遇到 var a,编译器会咨询作用域是否已经存在该名称的变量存在于同一个作用域集合中。是,就忽略 var a 继续编译;否则就会要求在当前作用域集合中生命一个新的变量,命名为 a
  • 编译器会为引擎生成运行时所需的代码,用来处理 a = 2 这个赋值操作。引擎在运行时会先咨询作用域,当前的作用域集合中是否存在一个叫做a的变量。是,就会使用这个变量;否,引擎就会继续查找该变量。
  • 如果最后引擎找到了 a 变量会将 2 赋值给它;否,引擎会抛出一个异常。
  1. 编译器在第二步中生成了代码,引擎执行它的过程中会查找 a 判断是否声明过,这个查找方式会影响最终的查找结果
  2. LHS查询RHS查询:简单来说 LHS 查询就是当变量出现在赋值操作的左侧时进行的查询, RHS查询 就是变量出现在赋值操作的右侧时进行的查询。要注意,查找只会在当前作用域进行。
console.log(a) 

以上代码就是 RHS查询 ,我们可以看到变量 a 出现在右侧。

a = 2

以上代码就是 LHS查询, 变量 a 出现在左侧。 6. 看一个具体例子

function foo(a) {
    console.log(a);//2
}
foo (2)

以上代码首先进行的声明 foo 函数,变量在右所以使 RHS查询

然后是隐式的 a = 2 这里采用 RHS查询 ,变量在左所以使用 LHS查询

console.log console是内置对象,找 log 变量在右使用 RHS查询

console.log(a) 同上变量在右使用 RHS查询

三、作用域链是什么?

作用域是根据名称查找变脸的一套规则,当一个块或函数嵌套在另一个块或函数中时就发生了作用域的嵌套。在当前作用域无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量为止。

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

上面的代码中 console.log(a + b) 我们在函数作用域中找不到 b 只能在上层的全局作用域中找

遍历嵌套作用域作用域链的规则:引擎从当前的执行作用于开始查找变量,如果找不到就去上级继续查找。当抵达最外层的全局作用域时,如果还没有找到,那么查找就会停止。

如上图的一条作用域链,我们在当前作用域要找到 a 、b、c 当前作用域没有就去外层作用域找在,找到了 b ,c没找到继续往外找,然后在全局作用域找到了 c 如果到此时还没有找到,那么查找就会停止。

四、ReferenceError 异常类型 和 TypeError 异常类型

  1. ReferenceError 异常类型
function foo(a) {
    console.log( a + b);
    b = a;
}

以上代码会报 ReferenceError的异常,因为我们通过 RHS 查询 在所有的嵌套作用域中都找不到 b。

相比之下如果是用 LHS查询 非严格模式下,如果在全局作用于中也找不到就会帮你创建一个具有该名称的变量,并且返还给引擎。

  1. 严格模式

严格模式禁止自动或隐式地创建全局变量。因此在严格模式中 LHS查询 失败时并不会创建并返回一个全局变量,而是会抛出 ReferenceError 异常。

  1. TypeError

如果通过 RHS查询 找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如对一个非函数类型进行函数调用那么就会抛出 TypeError 异常。

五、小结

  1. JavaScript 是一门编译语言,它的编译过程不仅仅是传统的三步,分词/词法分析,解析/语法分析,代码生成。还需要经过大量JIT这样的优化过程来保证性能。
  2. JavaScipt 引擎没有大量时间用来优化,他的编译过程不是发生在构建之前的而是在代码执行前的几微妙。
  3. 作用域是根据名称查找变量的一套规则,在 JavaScript 中如果在当前作用域找不到某个变量时,就会到外层嵌套的作用域中继续查找,如果在最外层的全局作用域当中也找不到那么查找就会停止。作用域链就是这一层层往外找的一条路径。
  4. 引擎从头到尾负责整个 JavaScrip t的编译及执行过程。编译器负责语法分析及代码生成。作用域负责收集并且维护由所有生命的标识符(变量)组成的一系列查询,并实施一套非常严格的规则确定当前执行的代码对这些标识符的访问权限。
  5. 引擎,编译器,作用域的合作过程。
var a = 'zhengyang'
  • 执行前的编译中,第一步,编译器会看变量是否在作用域中已经存在。是,忽略;不是,创建并命名为a。
  • 第二步,为引擎生成运行时所需的代码来处理 a = 'zhengyang' 这个赋值操作。
  • 衔接第二步,看变量在赋值操作的左边还是右边引擎会使用 LHS查询 或者 RHS查询 在当前作用域查找变量。如果找到了引擎就会使用这个变量将 'zhengyang' 这个值赋给它。
  • 如果在当前作用域找不到就会通过作用域链向外找。如果最外层的全局作用域也找不到就会报 ReferenceError 异常。
  • 注意非严格模式下如果使用 LHS查询 ,当最外层的全局作用域也不存在要查找的变量时会自动创建并且返回该变量给引擎,严格模式下则不可,因为严格模式禁止自动或隐式创建全局变量。