深入学习 JavaScript 笔记 (一) 作用域

问题:变量存储在哪里?更重要的是,在需要的时候程序如何找到他们?

 

1.1 编译原理:

尽管通常将JavaScript归类为“动态”或者“解释执行”语言,但事实上它是一门编译语言。

但与传统的编译语言不同,他不是提前编译的,编译结果也不能在分布式系统中进行移植。         ----《你不知道的JavaScript》

传统编译语言的编译过程

(以 var a = 2 为例):

1、分词/词法分析(Tokenizing/Lexing):

将 var a = 2 分解成var、a、=、2、; 、等词法单元(token)(空格是否被当做单元格取决于语言)。

2、解析/语法分析(Parsing):

将词法单元流(数组)转换为一个“抽象语法树” (AST);

顶节点 VariableDeclaration(个人理解为var,变量声明);

一个 Identifier 子节点(值为a);

一个 NumericLiteral 子节点(值为2,个人理解为number类型)。

3、代码生成

将第二部生成的AST转换为代码,简单的说就是讲AST转化为一组机械指令,创建叫做a的变量(包括分配内存),并将一个值存储在a中。

JavaScript引擎的编译

与其他语言不同,JavaScript的编译过程不是发生在构建之前的。

对于JavaScript来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短)时间内。

 

1.2 理解作用域

参与程序处理过程的几个部分:

1、引擎:

从头到尾负责整个JavaScript程序编译及执行过程

2、编译器:

负责分词、语法分析及代码生成

3、作用域:

负责收集并维护由变量组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些变量的访问权限。

(可以记忆成 查找变量的一套规则 

 

程序处理具体过程:

(以 var a = 2 为例):

事实上,引擎会认为这里有两个完全不同的声明(var a,a = 2),一个由编译器在编译时处理,一个由引擎在运行时处理。

编译器首先会进行编译(分词,解析成树结构,生成代码)

编译器生成代码分为两步:

1、var a,编译器会先询问作用域,如果当前作用域集合中已存在 a 变量则忽略,否则声明一个新变量并命名为 a。

2、a = 2,编译器为引擎生成运行时所需代码,引擎运行时再次也会询问作用域是否存在 a 变量,若存在则使用,找不到则抛出异常。

总结:变量赋值操作会执行两个动作,编译器在作用域中声明变量(若之前没声明过),运行时引擎在作用域中查找变量(找到则赋值)。

为了进一步理解,介绍一些编译器的术语

引擎执行编译器的代码时会通过查找变量 a 来判断是否声明过,查找的过程由作用域协助,但是引擎执行怎样的查找,会影响最终结果,在我们的例子中,引擎会为变量 a 进行LHS查询,另一个查询的类型叫做RHS。

  • LHS查询 (赋值操作的左侧):  查找变量的容器本身,从而赋值。
  • RHS查询 (赋值操作的右侧):查找变量的值。

注:LHS和RHS的含义是赋值操作的左右侧,然而赋值操作还有其他几种形式,因此最好在概念上理解为“赋值操作的目标(LHS)”和“谁是赋值操作的源头(RHS)”。

还有一个值得注意的地方,函数声明 function foo(a){... 并不能简单的理解为 var foo、foo=function(a){... 。如果这样理解的话,这个函数声明需要对 foo 进行LHS查询,但是并不会有线程专门用来将一个函数值“分配给”foo。因此,函数声明不适合理解成前面的LHS查询和赋值的形式。

1.3 作用域嵌套

我们说过,作用域是根据名称查找变量的一套规则。

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。

因此在当前作用域无法找到某个变量时,就会在外层嵌套的作用域继续找,直到找到该变量,或抵达最外层的作用域(全局作用域)为止。

1.4 异常

为什么区分LHS和RHS是一件重要的事?

因为在变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行为是不一样的。

RHS查询 查找不到会抛出 ReferenceError 异常。

LHS查询 如果在全局作用域中无法找到目标变量时,就会在全局作用域中创建一个具有该名称的变量,并返回给引擎(非严格模式)

ES5中引入了严格模式,同正常模式(宽松/懒惰模式)相比,严格模式禁止自动或者隐式创建全局变量,会抛出同RHS类似的ReferenceError 异常。

如果RHS找到了一个变量,但是你尝试对该变量进行不合理操作,比如视图对一个非函数类型的值进行函数调用,或者引用null或undefined类型中的值中的属性,那么会抛出 TypeError 异常。

ReferenceError 同作用域判别失败相关,TypeError 则代表作用域判别成功了,但是对结果的操作是非法或者不合理的。

1.5 小结

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。

赋值操作符会导致LHS查询,=操作符或者调用函数时传入参数的操作都会导致关联作用域的赋值操作。

JavaScript引擎首先会在代码执行前对其编译,在这个过程中,像var a = 2 这样的声明会被分解成两个独立的步骤

  1. 首先,var a 在其作用域中声明新变量。这会在开始阶段,也就是代码执行前进行。
  2. 接下来,a = 2 会查询(LHS查询)变量 a 并对其赋值

LHS和RHS查询都会在当前执行作用域中开始,如果有需要,就会向上级作用域继续查找目标标识符,直到抵达全局作用域(顶层)

不成功的RHS引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError 异常(严格模式下)。

 

猜你喜欢

转载自blog.csdn.net/Aproducer/article/details/82080945