细读《你不知道的JavaScript·上卷》1-1 做用域是什么?

墨言妹带你细读《你不知道的 JavaScript 》系列的世界,深刻 JavaScript 语言内部,弄清楚 JavaScript 每个零部件的用途,知其然更要知其因此然。git

做用域是什么

  • 问题1:变量存储在哪里?
  • 问题2:程序须要时如何找到它们?

1.1 编译原理

一般,把 JavaScript 归类为 “ 动态 ” 或 “ 解释执行 ” 的语言,可是事实上它是一门 编译语言,不提早编译,编译结果也不在分布式系统中进行移植。github

JavaScript 引擎进行编译的步骤和传统的编译语言很是类似,在某些环节比它要复杂。 编程

传统编译语言,在执行以前的三个步骤,统称为 “ 编译 ” 。数组

  • 分词/词法分析( Tokenizing/Lexingbash

    将有字符组成的字符串分解成(对编程语言来讲)有意义的代码块,这些代码块被称为词法单元( token )。编程语言

    var a = 2;
    复制代码

    被分解成词法单元:vara=2; 。空格在该语言中有意义,则会被当作词法单元,不然不是。分布式

  • 解析/语法分析( Parsing函数

    将词法单元流(数组)转换成一个由元素逐级嵌套所组成的表明了程序语法结构的 “ 抽象语法树 ”( Abstract Syntax Tree , AST )。post

    var a = 2;
    复制代码

    以上代码的抽象语法树以下所示:性能

    • VariableDeclaration 顶级节点
      • Identifer 子节点,值为 a
      • AssignmentExpression 子节点
        • NumericLiteral 子节点,值为 2
  • 代码生成

    AST 转换为可执代码的过程被称为代码生成。这个过程与语言、目标平台等相关。

    即经过某种方法,将 var a = 2 ;AST 转化为一组机器指令,用来建立一个变量 a ,并将值存储在 a 中。

    引擎,能够根据须要建立并存储变量。

1.2 理解做用域

1.2.1 演员表

  • 引擎,从头至尾负责整个 JavaScript 程序的编译及执行过程。
  • 编译器,负责语法分析及代码生成等脏活累活。
  • 做用域,负责收集并维护由全部声明的标识符(变量)组成一系列查询,并实施一套很是严格的规则,肯定当前执行的代码对这些标识符的访问权限。

1.2.2 对话

JavaScript 引擎是如何处理 JavaScript 代码的?

好比 var a = 2; 存在2个不一样的声明,变量的赋值操做会执行两个动做。

  • 遇到 var a ,在编译阶段,编译器先询问当前做用域,在做用域集合中是否存在变量 a ,若不存在则声明一个新变量名为 a;接着编译器会为引擎生成运行时所需的代码,处理 a = 2 这个赋值操做。
  • 遇到 ( a = 2 ),在执行阶段,引擎运行时先询问做用域,在做用域中查找该变量 a,若是找到就将值 2 赋值给变量 a,不然引擎就会举手示意并抛出一个异常。

1.2.3 编译器有话说

  • 如何理解引擎、编译器、做用域的关系

    • 代码先编译后执行,当编译器在编译过程的第二步中生成了代码,引擎执行它时,会经过查找变量 a 来判断它是否已经声明过。
    • 查找的过程由做用域进行协助,可是引擎执行怎样的查找,会影响最终的查找结果。对 JavaScript 引擎的性能要求很高。
  • 引擎查找的两种方式: RHSLHS

    • LHS 查询(左侧):找到变量的容器自己,而后对其赋值,即赋值操做的目标是谁。好比 a = 2; ,为 = 2 这个赋值操做找到一个目标。
    • RHS查询(非左侧):查找某个变量的值,理解为 retrieve his source value ,即谁是赋值操做的源头。好比: console.log( a ); ,须要获取到变量 a 的值,则对变量 aRHS 查询,并传值给 console.log(...)
function foo(a){
	console.log( a ); //2
}
foo(2);
复制代码

上述代码共有1处 LHS 查询,3处 RHS 查询。

  • LHS 查询有:
    • 隐式的 a = 2 中,在 2 被当作参数传递给 foo(...) 函数时,须要对参数 a 进行 LHS 查询
  • RHS 查询有:
    • 最后一行 foo(...) 函数的调用须要对 foo 进行 RHS 查询,意味着 “去找到 foo 的值,并把它给我 ” ,而且 (...) 意味着 foo 的值须要被执行,所以它最好真的示意函数类型的值。
    • console.log( a ); 中对 a 进行 RHS查询,而且将获得的值传给了 console.log(...)
    • console.log(...) 自己对 console 对象进行 RHS 查询,而且检查获得的值中是否有一个叫做 log 的方法。

1.3 做用域嵌套

做用域是一套规则,用于肯定在何处以及如何查找变量(标识符)。当一个块或函数嵌套在另外一个块或函数中时,就发生了做用域的嵌套。

  • 若是查找的目的是对变量进行赋值,那么就会使用 LHS 查询;若是目的是获取变量的值,就会使用 RHS 查询。
  • 赋值操做符会致使 LHS查询。= 操做符或调用函数时传入参数的操做都会致使关联做用域的赋值操做。

遍历嵌套做用域链的规则: 引擎从当前的执行做用域开始查找变量,若是找不到,就向上一级继续查找。当抵达最外层的全局做用域时,不管找到仍是没找到,查找过程都会中止。

1.4 异常

为何区分 LHSRHS ?变量尚未声明(在任何做用域中都没法找到该变量)的状况下,这两种查询的行为是不同的。

function foo(a){
	console.log(a + b);
	b = a;
}
foo(2);
复制代码

对一个 “未声明 ” 的变量 b 进行 RHS 查询时,在任何相关的做用域中都没法找到它。

ReferenceError 和做用域判别失败相关,而 TypeError 则表明做用域判别成功了,可是对结果的操做是非法或不合理的。

  • RHS 查询在做用域链中搜索不到所需的变量,引擎会抛出 ReferenceError 异常。
  • 非严格模式下,LHS 查询在做用域链中搜索不到所需的变量,全局做用域中会建立一个具备该名称的变量并返还给引擎。
  • 严格模式下( ES5 开始,禁止自动或隐式地建立全局变量), LHS 查询失败并不会建立并返回一个全局变量,引擎会抛出同 RHS 查询失败时相似的 ReferenceError 异常。
  • RHS 查询成功状况下,对变量进行不合理的操做,引擎会抛出 TypeError 异常。(好比对非函数类型的值进行函数调用,或者引用 nullundefined 类型的值中的属性)。

最后, 书读百遍其义自见,抱着以教为学的初衷,不断反思、刻意练习,若对你有帮助,请点个赞,谢谢您的支持与指教。

参考文献: 木易杨博客

历史文章: 【译】30 Seconds of ES6 (一)

相关文章
相关标签/搜索