你的JavaScript代码都经历了什么

从语言类型说起

要知道你写的代码接下来是交给谁的,先要明白解释型语言和编译型语言。

解释型语言:这种类型的编程语言,会将代码一句一句直接运行,不需要像编译语言(Compiled language)一样,经过编译器先行编译为机器码,之后再运行。这种编程语言需要利用解释器,在运行期,动态将代码逐句解释(interpret)为机器码,或是已经预先编译为机器码的的子程序,之后再运行。

编译型语言:是一种以编译器来实现的编程语言。它不像解释型语言一样,由解释器将代码一句一句运行,而是以编译器,先将代码编译为机器码,再加以运行。理论上,任何编程语言都可以是编译式,或直译式的。它们之间的区别,仅与程序的应用有关。

那么,JavaScript就是典型的解释型语言,那么要运行JavaScript程序就必须要有响应的执行环境,也就是要通过JavaScript引擎解析执行JS代码。JavaScript引擎的基本工作是把开发人员写的JavaScript代码转换成高效、优化的代码,这样就可以通过浏览器进行解释甚至嵌入到应用中。比如著名的V8引擎。

JavaScript的解析过程分为两个阶段:预编译期(预处理)执行期。在预编译期,JavaScript解释器完成对JavaScript代码的预处理,转换为字节码。执行期间,JavaScript解释器把字节码转换成二进制码,按照顺序执行。

预编译期:

正常的编译型语言编译期,其过程可分为6步:词法分析、语法分析、语义分析、源代码优化、代码生成、目标代码优化。对于JavaScript来说,通过词法分析和语法分析得到语法树后,就会进入到执行期,执行代码。

词法分析:在词法分析阶段,JavaScript解释器先把代码的字符流转换为记号流,例如:

a = (b -c)
复制代码

转换为记号流:

NAME "a"  
EQUALS  
OPEN_PARENTHESIS  
NAME "b"  
MINUS  
NAME "c"  
CLOSE_PARENTHESIS  
SEMICOLON 
复制代码

词法分析阶段可以实现的是:1、去掉注释,生成文档;2、记录错误信息;3、完成预处理

语法分析:

语法分析阶段就是把词法分析阶段产生的记号,生成语法树,即把从程序中收集的信息存储到数据结构中,数据结构在此处为两种:1、符号表:记录变量、函数、类;2、语法树:程序结构的树形表示,将此树形结构生成中间代码。例如:

 if(typeof a == "undefined" ) { 
    a = 0
 } else { 
    a = a
 } 
 alert(a)
复制代码

生成的语法树为:

当构建语法树的过程中,无法构造,则报出语法错误,并结束整个代码块的解析。 词法分析和语法分析阶段是交错进行的,每取一个词法记号,就送入语法分析器进行分析。 词法、语法分析是有规则的,其中ECMAScript262这份文档,就是对JavaScript这门语言定义了一整套完整的标准。语法分析就依靠这套标准,当然也有不按照标准来实现的,比如IE的JS引擎。这也是为什么JavaScript会有兼容性的问题。

执行期

经过编译阶段的准备,代码在内存中已经构建成语法树,JavaScript引擎会根据这个语法树结构边解释边执行。解释过程中,引擎严格按照作用域机制执行。JavaScript采用的词法作用域,简单说就是变量和函数的作用域在定义时决定,取决于源代码结构。

var value = 1;
function foo() {
    console.log(value);
}
function bar() {
    var value = 2;
    foo();
}
bar();
复制代码

就像这段代码,并不会像动态作用域一样,输出2. 引擎解释执行每个函数时,先创建一个执行环境,在这个环境中创建一个调用对象,这个对象内存储着当前域中所有局部变量、参数、嵌套函数、引用函数和父级列表。调用对象声明周期与函数一致,当函数调用完毕且没有外部引用的情况下,被垃圾回收机制回收。

同时解释器通过作用域链把多个嵌套的作用域串在一起,并借助这个链,由内而外查找变量值,直到全局对象,如果没有找到,返回"undefined"。作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

闭包

在执行环境创建的过程中,会有一个特殊的情况——闭包。它由两部分组成。执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)。当B执行时,如果访问了A中变量对象中的值,那么闭包就会产生。在大多数理解中,包括许多著名的书籍,文章里都以函数B的名字代指这里生成的闭包。而在chrome中,则以执行上下文A的函数名代指闭包。

function A() {
    var a = 20;
    var b = 30;
    function B() {
        return a + b;
    }
    return B;
}
var B = A();
B();
复制代码

首先有执行上下文A,在A中定义了函数B,而通过对外返回B的方式让B得以执行。当B执行时,访问了A内部的变量a,b。因此这个时候闭包产生。JavaScript拥有自动的垃圾回收机制,关于垃圾回收机制,有一个重要的行为,那就是,当一个值,在内存中失去引用时,垃圾回收机制会根据特殊的算法找到它,并将其回收,释放内存。正常来讲,当A执行完毕后,生命周期结束,A函数的执行上下文就会失去引用。其占用的内存空间很快就会被垃圾回收器释放。可是B函数的存在,会阻止这一过程,使B函数常驻内存。

单线程&&事件循环

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准? 所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。 为了利用多核CPU的计算能力,HTML5提出WebWorker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。单个线程使得运行代码很容易,因为你不必处理在多线程环境中出现的复杂场景——例如死锁。但是在一个线程上运行也非常有限制。由于JavaScript、只有一个调用堆栈,当某段代码运行变慢时会发生什么?

既然是单线程的,在某个特定的时刻只有特定的代码能够被执行,并阻塞其它的代码。而浏览器是事件驱动的(Event driven),浏览器中很多行为是异步的,会创建事件并放入执行队列中。JavaScript引擎是单线程处理它的任务队列。当异步事件发生时,如mouse click, a timer firing, or an XMLHttpRequest completing(鼠标点击事件发生、定时器触发事件发生、XMLHttpRequest完成回调触发等),将他们放入执行队列,等待当前代码执行完成再从执行队列按序拿出事件执行。Event Loop只做一件事情,负责监听Call Stack和Callback Queue。当Call Stack里面的调用栈运行完变成空了,Event Loop就把Callback Queue里面的第一条事件(其实就是回调函数)放到调用栈中并执行它,后续不断循环执行这个操作。

也就是说JS只有一个调用栈。调用栈是一种数据结构,它记录了我们在程序中的位置。如果我们运行到一个函数,它就会将其放置到栈顶。当从这个函数返回的时候,就会将这个函数从栈顶弹出,这就是调用栈做的作用。栈内的任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。

  • macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)。
  • setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
setTimeout(function() {
    console.log('xxxx'); // 这段代码才是进入任务队列的任务
})
// setTimeout作为一个任务分发器,这个函数会立即执行,而它所要分发的任务,也就是它的第一个参数,才是延迟执行
复制代码
  • 来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
  • 事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
  • 其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。
垃圾回收

垃圾回收机制有好多种,这里简单说下标记清除算法 为了决定一个对象是否被需要,这个算法用于确定是否可以找到某个对象。 其包含以下步骤。

  1. 垃圾回收器生成一个根列表。根通常是将引用保存在代码中的全局变量。在JavaScript中,window对象是一个可以作为根的全局变量。
  2. 所有的根都被检查和标记成活跃的(不是垃圾),所有的子变量也被递归检查。所有可能从根元素到达的都不被认为是垃圾。
  3. 所有没有被标记成活跃的内存都被认为是垃圾。垃圾回收器就可以释放内存并且把内存还给操作系统。 这个算法可以有效的避免循环依赖问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值