首先声明本文内容主要来自《You Don’t Know Js: Scope & Closures》中文版《你不知道的Javascript 上卷》,本人对于Javascript引擎、编译器和作用域的理解也主要源于该书与自己在控制台的调试,欢迎和大家一同学习交流。

编译原理

一般分为三个步骤:

1. 分词/词法分析
把代码片段分成对语言有意义的词法单元
2. 解析/语法分析将词法单元转换为AST(抽象词法树)
3. 代码生成将AST转化为可以执行的代码
具体细节可以参看原书。总的来说代码片段会在代码执行前极短的时间进行编译

Javascript引擎

负责Javascript的编译和执行全过程

编译器

Javascript引擎的好朋友,负责解析和代码生成过程

作用域

Javascript的另一个好朋友,负责收集、维护、生成声明的变量,并且它有一套自己的规则,确保当前执行的代码对这些变量的访问权限。
以书上的示例var a = 2为例,这一句声明,在js执行时,对引擎来说是分两部分的,第一部分是在在编译过程中完成,第二部分是在引擎运行时处理。1. 编译器编译过程 var a
编译器会询问作用域是否已经存在一个该名称的变量,如果有就直接忽略当前的声明,如果没有,就要求作用域声明一个变量a

var a;就被忽略了,所以打印出来还是1
[]

var b;就被忽略了,所以打印出来还是函数这就是所谓的“变量提升”吧2. 编译器生成完代码供引擎执行过程 a = 2引擎会先问作用域是否存在变量a,作用域会告知引擎:有的(编译器刚刚已经要求作用域声明了),这时引擎就会直接用到变量a,并将2赋值给a;如果当前作用域没有找到变量a的话,作用域会一直向上查找,知道全部作用域找不到则报错为止。
左查询和右查询
这里的左右是相对于赋值操作而已的,并非是按照=的左右的。通俗的说,我们要进行赋值操作,所有要找到目标,也就是赋值给谁,然后就是取到将要把什么值赋值给该目标,就是要取到赋值的数据来源。需要特别提到的是在函数执行时,参数赋值的过程。比如

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

代码中隐式的a=2的过程容易被忽略,这个操作发生在2被当作参数传递给foo(...)函数式,2会被分配给参数a,这是就存在一个左查询。
函数声明、普通的变量声明+赋值的区别

// 代码a
function foo(a) {

}

是不同于

<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
     (adsbygoogle = window.adsbygoogle || []).push({
          google_ad_client: "ca-pub-3013839362871866",
          enable_page_level_ads: true
     });
</script>
// 代码b
var foo = function (a) {

}

的,如果按照代码b的话,函数声明将需要一个左查询。实际上代码a和代码b是有个细微但很重要的差别,编译器可以在生成代码的同时处理声明和值的定义,所以在引擎执行代码的时候并不会有个线程来专门用来将一个函数值分配给foo,所以将函数声明理解成左查询并赋值并不合适。

可以看到function c(){}和var c都被“忽略”了,如上所述遇到function c(){}编译器是同时处理声明和值的定义的,函数提升是优先于变量提升的。
上面的顺序实际是

function c(){}
var c;
c = 3;
console.log(c);
console.log(c);
var c;
console.log(c);
var c = 323;
console.log(c);

根据打印的结果可以看出,在声明被忽略时,赋值也被忽略了,我们可以把它看成一个操作来理解,要忽略就一起忽略。


发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据