前言
提起JavaScript ,大家第一反应:脚本语言、解释性执行等,和java、 C这种编译性语言搭不上边。然而,事实上它确实是一门编译语言。只是区别在于JS并不会像其他的编译语言一样进行提前编译,他的编译过程(通常)是在实际执行前进行的,而且也不会产生可移植的编译结果。
通常的编译过程,会做以下几个步骤:首先是分词与词法分析,把输入的字符串分解为一些对编程语言有意义的代码块(词法单元)。第二步解析与语法分析,这一步的操作高级了许多,会将上一步的词法单元集合分析并最终转换为一个由元素逐级嵌套所组成的代表了程序语法结构的树,称为 抽象语法树
(Abstract Syntax Tree,AST)。第三步代码生成就是将上一步的AST转换为可执行代码。JavaScript引擎中的编译器做的事情与这个类似,但是因为JS引擎的编译过程就在代码执行前,对于“用户”来说是完全透明的。并且无法事先执行编译生成静态文件,因此JS的编译执行效率就要比一般静态语言敏感的多,故而也非常复杂。JS引擎在这一部分做了非常多的优化,一是针对语法分析和代码生成阶段进行优化(例如针对冗余元素进行优化等),目的是提高编译后的执行效率。二是针对编译过程进行优化(如JIT,延迟编译甚至重编译),目的是缩短编译过程,保证性能最佳。
介绍
- 引擎: 负责整个JS程序的编译及执行过程。
- 编译器: 负责语法分析及代码生成等工作。
- 作用域: 收集并维护由所有声明的标识符(变量)组成的一系列查询,实施一套非常严格的规则, 确定当前执行的代码对这些标识符的访问权限。
执行
下面我们以一个最简单的例子来进行分析:
var a = 2
- 编译器出马,先进行词法分析,将该赋值操作拆分: var a;a=2;。第一步 var a,编译器可以处理,他会先询问变量管家:作用域,是否存在一个该名称的变量?若存在,继续编译;若不存在,通知作用域声明一个新变量,命名为a。
- 编译器继续为引擎进行代码生成,这些代码主要用来处理 a=2这个赋值操作。
- 引擎拿到可执行代码,然后询问作用域:当前有没有一个叫a的变量啊? 如果有:使用这个变量,赋值给他;如果没有就继续往上级作用域查找,如果到根作用域仍然找不到,引擎直接报错抛异常。
这儿引入个关于变量查找的概念:
- LHS:赋值操作的左侧,试图查找到变量的容器本身,从而可以对其赋值,即找到复制操作的目标。
- RHS:另外一种查找,可以简单理解为复制操作的右侧,其查找目标为取到目标的源值,即找到这个变量具体的值而非容器。
var a; // LHS 寻找a,未找到,通知作用域声明一个新变量,命名为a
a = 2; // LHS 找到a并给其赋值2
console.log(a); // RHS找到a的值2,并将其输出
有了上面的基础知识,我们把三兄弟的合作再细化一下,例子也升级一下,用上面赋值并输出的例子。
- 编译器:作用域,我需要对a进行LHS查找,你见过么?
- 作用域:我这找到根都没看到啊,要不咱声明一个吧!
- 编译器:好,建好了,那我生成代码了,引擎,给你你要的代码。
- 引擎:收到,咦,需要一个a啊,作用域,帮我LHS找一下有没有?
- 作用域: 找到了,编译器已经帮忙声明了。
- 引擎:好的,那我对它赋值。
- 引擎:作用域,不要意思,我碰到一个console,需要RHS引用
- 作用域: 找到了,是个内置对象,拿走不谢。
- 引擎: 好的作用域,对了能在帮我确认一下a的RHS么?
- 作用域:确认好了,没变,拿去用吧,他的值是2
- 引擎:好咧,我把2传递给log(..)
疑问:为什么要这么啰嗦的区分LHS和RHS?其实细心的话,你应该已经发现了,这两种查找有一个很重要的区别,即在变量未找到的时候的行为不同:
- RHS未找到:引擎会抛出错误RefrenceError
- LHS未找到:引擎(或引擎中的编译器)会帮你在顶层作用域声明一个具有该名称的变量。(严格模式除外)
其他
词法作用域 :介绍作用域时,我们有讲过其根据一套规则来管理变量的查找与引用,词法作用域就是js使用的规则,在编译器进行词法化时,其会根据你写代码时将变量和块作用域写在哪里,来决定规则的内容。这其中又包含了块作用域这个概念,不展开讲,只要记住ES6之前没有块作用域,只有函数作用于,即:函数内部是一个独立的块作用域。(有个特例:catch语句块内也是独立的作用域)
变量提升: 明白了编译器和引擎执行之间的分工,其实你应该就不会觉得变量提升是如此之诡异了,因为引擎拿到代码的时候,编译器已经做了一些转换(引擎旁白:这尼玛真怪不得我啊/(ㄒoㄒ)/~)。编译器干嘛要干这个事情?因为他要在第一步就找到所有的声明,并且用合适的作用域将他们关联起来,这也正是词法作用域的核心。表现为: 包括变量和函数在内的所有声明都会在当前块作用域内被首先处理,即类似于提升到最前面声明,但是复制处理操作因为是在执行阶段,因此编译阶段他们原地待命等待执行 。不如留两个练习?
//第一个练习:
a = 2
var a;
console.log(a);
//第二个练习:
console.log(a);
var a = 2;