JavaScript运行环境、运行机制与运行过程

文/何其甚

写这篇小文之时找了好多参考资料,其中就有阮一峰的《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

http://www.ruanyifeng.com/blog/2014/10/event-loop.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 原文: https://github.com/ecomfe/spec/blob/master/javascript...
    zock阅读 3,343评论 2 36
  • 首先,JavaScript确实是一门编译型语言,与C等典型编译型语言相比,区别在于JavaScript的编译过程(...
    环零弦阅读 5,405评论 0 18
  • 五十三:请解释 JavaScript 中 this 是如何工作的。1.方法调用模式当一个函数被保存为一个对象的属性...
    Arno_z阅读 520评论 0 2
  • 每次看到“自律”两个字就觉得和自己无关,而三分钟热度大概就是本人。有一次话题是坚持的一件事,我想了很久想出来的就只...
    三月Zoro君阅读 238评论 0 2