文/何其甚
写这篇小文之时找了好多参考资料,其中就有阮一峰的《JavaScript 运行机制详解:再谈Event Loop》和网友转载的《【朴灵评注】JavaScript 运行机制详解:再谈Event Loop》,其中具体细节有不同理解有不同,但应该并不妨碍整体运行机制的理解。
一、运行环境
JavaScript是伴随着浏览器的诞生而诞生,所以JavaScript的执行最多还是在浏览器环境之内。但是JavaScript作为服务端脚本的概念在诞生之初就有,1995年网景公司就提出了服务端JavaScript的概念,并研发了 Netscape Enterprise Server;1996年微软发布的JScript也可以运行在服务端。随着技术的发展各种JavaScript引擎出现,2009年5月Node.js的发布将JavaScript作为服务端脚本推向了一个高潮。关于JavaScript服务端的实现可以参看wikipedia,https://en.wikipedia.org/wiki/List_of_server-side_JavaScript_implementations。
JavaScript的运行不像C语言等其他编译型语言编译后直接在操作系统上运行,因为它是脚本语言,运行时必须要借助引擎(解释器)来运行,所以它可以在封装了引擎的环境下运行。封装了JavaScript引擎的环境可以分为两类,一类是浏览器环境;一类是非浏览器环境,比如Node.js、MongoDB。我没有采用wikipedia中clent-side和server-side的直接翻译,因为JavaScript既可以编写服务端脚本也可以编写shell脚本,甚至图形界面应用程序。
把运行环境分为浏览器环境和非浏览器环境是因为他们提供了截然不同的操作模块。浏览器环境下JavaScript由三部分组成,分别是ECMAScript、DOM和BOM,BOM和DOM是针对浏览器环境所扩展的操作方法。非浏览器环境,比如Node.js,也是以ECMAScript为基础,扩展出了I/O操作、文件操作、数据库操作等等;在MongoDB中则是可以作为shell脚本操作数据库;在Eclipse e4中可以编写扩展。
二、运行机制
了解了JavaScript的运行环境,我们来看看运行机制。这里我们不再谈微软的JScript,一方面写本文时我没有找到详尽的介绍JScript的资料,另一方面JScript的应用现在不常见。
JavaScript是个什么样子,取决于它初始应用于哪里,它是作为浏览器的脚本出现,主要用途是解决网页中的用户交互。页面中的用户交互行为会让页面中的DOM元素产生变化,比如用户输入信息后的反馈提示等等。JavaScript在浏览器环境中操作DOM,为避免复杂的同步问题,决定了它采用单线程。如果同时有多个线程,有的在DOM节点上添加内容,有的修改了整个节点,甚至有的删除了整个节点,这个时候很难判断到底采用哪个线程的结果。
JavaScript最大的特点就是单线程,在浏览器环境中中是,在非浏览器环境中同样也是。单线程也就意味着JavaScript在同一时间只能进行一项任务,如果有多项任务的话,需要对任务进行排队,完成一个才能继续下一个。
不同的浏览器、不同的引擎、不同的执行环境,执行JavaScript的细节会有差异,但是不变的是单线程和队列。
三、运行过程
在浏览器环境中,JavaScript引擎按<script>标签代码块从上到下的顺序加载并立即解释执行。
我们在这里不探究引擎的详尽解释执行细节,比如词法分析、语法分析以及语法树的构造等等,只说它解释执行过程中非常重要的两个时期预编译期(预解析期)和执行期。理解这两个阶段十分有助于理解JavaScript中的一些“奇特”的现象。
在预编译期JavaScript会对var和function的声明在其所在作用域内进行提升,提升的位置相当于所在作用域开始位置。预编译期需要注意下面几个问题:
1.预编译首先是全局预编译,函数体在未调用时不进行预编译
2.只有var和function声明会提升
3.注意是在所在作用域内提升,不会扩展到其他作用域
4.预编译后顺序执行
先看var变量声明。以下示例在firefox中测试运行。
console.log(a);//undefined
var a = 1;
console.log(a);//1
代码中第一输出的undefined代表的意思是变量已经存在,只是没有初始化。这段代码预解析的等价结果是:
var a ;
console.log(a);
a = 1;
console.log(a);
再来看看非var变量的定义,全局变量定义。
console.log(a);//ReferenceError: a is not defined
a = 1;
console.log(a);
在这里可以看到var定义和非var定义的区别,在未定义之前调用提示变量没有定义。
再来看let变量定义。
console.log(a);//ReferenceError: can't access lexical declaration `a' before initialization
let a = 1;
console.log(a);
let定义之前调用变量,firefox的错误提示很明确:在声明之前不能调用。
接下来看函数function的定义。
foo();//this is function foo
function foo(){
console.log("this is function foo");
}
在定义之前调用函数,在许多语言中是错误的,但是在JavaScript中它却是正确的,执行了在后面定义的函数,这其实就预编译其的函数声明提前,上面这段相当于下面这段代码:
function foo(){
console.log("this is function foo");
}
foo();
JavaScript中定义函数还有另一种使用变量的方式,结合上面说到的var变量声明预编译前置,可以理解下面这段代码的执行结果:
console.log(foo);//undefined
foo();//TypeError: foo is not a function
var foo = function (){
console.log("this is var foo");
}
可以看出来foo的声明被前置,但是没有初始化,所以foo的值是undefined,自然它也就不是函数。
console.log(foo);//
var foo = function (){
console.log("this is var foo");
}
foo();//this is var foo
函数有两种常用定义方式var和function,两种方式在预编译期都会前置,但到底哪一种优先生效呢?看下面的代码。
foo();//this is function foo
var foo = function (){
console.log("this is var foo");
}
function foo(){
console.log("this is function foo");
}
foo();//this is var foo
利用我们上面的前置规则,我们来整理下思路。第一行的foo执行的是function定义的函数,最后的foo执行的是var定义的函数,那么它的等价顺序应该是这样的:
function foo(){
console.log("this is function foo");
}
var foo ;
foo();
foo = function (){
console.log("this is var foo");
}
foo();
等价的顺序中你可能会疑惑var foo的位置。首先确定一点是var声明一定是前置的,function定义也是前置的,它们两者都会前置到调用之前,也就是第一次调用foo()之前。至于var foo和function的前后位置它们两个互换是等价的,无论var foo在function之前还是之后都是一样的。
下面我们再来看下函数体内的预编译情况。
console.log(a);//ReferenceError: a is not defined
function foo(){
var a = 1;
}
fcc();//ReferenceError: fcc is not defined
function foo(){
function fcc(){}
}
函数体内的声明不会前置到外部作用域。要注意一点就是函数体的预解析发生在函数被调用之时,被调用时先进行函数体的预编译,然后按顺序进行执行。
参考内容:
https://en.wikipedia.org/wiki/JavaScript#Server-side_JavaScript
https://en.wikipedia.org/wiki/List_of_server-side_JavaScript_implementations
网友评论