本篇文章给大家谈谈javascript常见编译器,以及javascript 编译原理,希望对各位有所帮助,不要忘了收藏本站喔。
1、js的编译与执行、事件循环
单线程语言
JavaScript是单线程语言,即在浏览器中一个页面只有一个线程在执行js代码。
进程和线程
假设我们有一家工厂(进程),那么 工厂所拥有的独立资源就相当于系统给我们分配的内存(这是独立的)快码论文。
如果我们有多个工厂,每个工厂做不一样的事情,那么也就意味着工厂间是互相独立的,也就是说 进程间互相独立。
工厂里有一个或多个工人(线程),那么: 一个进程包含了一个或多个线程。
多个工人在一个工厂里工作,享受的是这个工厂拥有的独立资源,也就是说: 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
工人们之间互相协作完成工厂布置的任务,也就说明: 多个线程在进程中协作完成任务的特点。
总结:
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
WebWorker
H5新增:
可以理解为是 浏览器 申请开辟的一个新线程来帮助JS引擎完成复杂的计算来减少由于JS执行时间过长而导致的阻塞时长
注⚠️: 需要注意这并不影响JS引擎是单线程的。
JS引擎执行过程
解析器:负责将js代码转换为AST抽象语法树
解释器:负责将AST抽转换为字节码,并收集编译器需要的优化编译信息
编译器:利用解释器收集到的信息,将字节码转换为优化的机器码
JavaScript通过 词法分析 和 语法分析 得到语法树后,js引擎开始执行代码,但是JS引擎并不是逐条解释java代码,而是通过一个个标签分隔开的代码段进行的解释执行。
简单来说,它就是能够将 JavaScript代码处理并执行的运行环境,不同的浏览器有不同的JS引擎,而我们常接触的V8正是采用C++编写在Chrome浏览器中被使用的解析引擎。
JS引擎执行过程分成三个阶段:
1. 语法分析
2. “预编译”阶段(解释阶段)
3. 执行阶段
浏览器首先按顺序加载由<></>
标签分割的代码块,加载代码块完毕后,立刻进入以上三个阶段。然后再按顺序查找下一个代码块,再继续执行以上三个阶段,无论是外部脚本文件(不异步加载 即会停止加载后面的内容,停下来解析脚本并对页面进行渲染)还是内部脚本代码块,都是一样的原理,并且都在同一个全局作用域中。
1、语法分析
在js代码加载完毕以后,就会进入到语法分析阶段,在这个阶段中js将会检查代码块的语法是否正确。
-
若有错误语法则直接抛出错误,并停止接下来的执行,而后继续向后查找并加载下一个代码块。
-
若语法都没有问题,那么就进入接下来的预编译阶段。
2、“预编译”阶段
在理解这个阶段之前,我们需要优先了解一下js的几种运行环境:
- 全局环境
- 函数环境
- eval
每进入一个不同的运行环境都会创建一个相应的执行上下文(Execution Context),
在一段JS程序中我们会创建很多的执行上下文,而这些执行上下文在JS引擎中会 以栈的方式 执行处理
而这时候形成的栈我们叫它 函数调用栈(Call Stack) ,其中调用栈的 栈底 永远是我们的 全局执行上下文(Global Execution Context) , 栈顶 则永远是 当前的执行上下文 。
我们从一个简单的例子出发:
function bar() {
var B_context = "Bar EC";
function foo() {
var f_context = "foo EC";
}
foo()
}
bar()
- 首先我们进入全局环境,创建全局执行上下文,这时候推入函数调用栈中
- 当我们调用bar时,进入bar的运行环境,创建bar的执行上下文推入函数调用栈中
- 在运行bar的时候,内部调用foo函数,因此我们再进入foo的运行环境,创建foo的函数执行上下文推入函数调用栈栈中
- 此刻栈底是全局执行上下文,栈顶是foo函数执行上下文,由于foo函数内部没有再调用其他函数,因此无需再创建多余的函数执行上下文
- foo函数执行完毕后,栈顶foo函数执行上下文首先出栈
- 接着bar函数接下来也没有别的语句需要执行,因此也是执行完毕的状态,所以bar函数执行上下文出栈
- 最后全局执行上下文则是在浏览器或者该标签页关闭的时候出栈
“预编译”阶段——创建执行上下文
1.创建变量对象(Variable Object)
2.建立作用域链(Scope Chain)
3.确定this的指向
创建变量对象(Variable Object)
- 在函数环境(非箭头函数)中进行创建arguments对象,检查当前上下文中的参数,建立该对象的属性与属性值(全局环境没有此过程!!)
- 检查当前上下文的函数声明,按代码顺序查找,将找到的函数提前声明,如果当前上下文的变量对象没有该函数名属性,则在该变量对象以函数名建立一个属性,属性值则为指向该函数所在堆内存地址的引用,如果存在,则会被新的引用覆盖。
- 检查当前上下文的变量声明,按代码顺序查找,将找到的变量提前声明,如果当前上下文的变量对象没有该变量名属性,则在该变量对象以变量名建立一个属性,属性值为undefined;如果存在,则忽略该变量声明
注:在全局环境中,window对象就是全局执行上下文的变量对象,所有的变量和函数都是window对象的属性方法。
例如:
function fun(a, b) {
var num = 1;
function test() {
console.log(num)
}
}
fun(2, 3)
当执行fun函数并传入参数2和3时:(暂时不讲解作用域链以及this指向)
funEC = { //fun的执行上下文
VO: { //变量对象
arguments: { //arguments对象
a: undefined,
b: undefined,
length: 2
},
test: <test reference>, //test函数,在堆内存中地址的引用
num: undefined //num变量
},
scopeChain:[], //作用域链
this: window //this指向
}
解析:
-
创建变量对象是发生在“预编译”阶段,还并没有进入执行阶段,因此这里的变量对象是不可访问的,此时值还是undefined
-
当我们进入执行阶段开始对其变量属性赋值,变量对象转变为活动对象,此时才可以进行访问,而这个过程就是VO->AO的过程,其中AO就是Active Object活动对象。
-
注:函数声明提前和变量声明提升是在创建变量对象中进行的,且 函数声明优先级高于变量声明
通过例子来说明:
var a = 1;
function b() {
a = 10;
return;
function a() {}
}
b();
console.log(a);
- 解析: 输出: 1,在函数b中优先声明函数a,此后给a赋值为10,这里的a是函数b内的声明变量a,和全局作用域下的a不是同一个变量,因此最后输出a不变
function foo(){
function bar() {
return 3;
}
return bar();
function bar() {
return 8;
}
}
alert(foo());
- 解析:输出: 8,执行foo函数,先声明返回3的bar函数,再覆盖了一个返回8的bar函数,执行时即输出被覆盖后的值
alert(foo());
function foo() {
var bar = function() {
return 3;
};
return bar();
var bar = function() {
return 8;
};
}
解析:输出: 3,优先声明函数foo,因此可以alert出foo函数,此时再foo函数内,先声明变量bar其次,给bar赋值返回值为3的匿名函数,此时执行return语句直接执行输出3,在return后的赋值语句不会再执行
注:不同的运行环境执行都会进入 代码预编译 和 执行 两个阶段,而语法分析则 在代码块加载完毕时 统一检验语法
2 建立作用域链(Scope Chain)
作用域链由当前执行环境的变量对象(也就是未进入执行阶段前)与**上层环境的一系列活动对象(AO)**组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
JS引擎在解释过程中,是严格按照作用域机制来执行的, JS的变量及函数 在定义时 就已经决定了它们的作用域范围,
所以解释JS只要通过静态分析就可以确认每个变量,函数的作用域,因此这些作用域也叫作 静态作用域。
那么举个具体的例子:
var num = 30;
function test() {
var a = 10;
function innerTest() {
var b = 20;
return a + b
}
innerTest()
}
test()
此时:
innerTestEC = {
VO: { b: undefined }, //变量对象
scopeChain: [VO(innerTest), AO(test), AO(global)], //作用域链:注意顺序,链头是当前作用域,链尾一定是全局作用域
this: window //this指向
}
3 确定this的指向:
在全局执行上下文中,this是始终指向全局对象的。
而在执行函数的过程中,就需要考虑函数的调用方式,而函数共有4中调用方式:
1. 当它被作为对象的方法调用时,函数的指向为该对象
2. 当它直接作为函数被调用时,this通常指向的就是全局对象
3. 当它被以构造函数的方式调用时,会将新建的对象作为该函数的this值
4. 当它被间接调用也就是被call/bind/apply这样的函数绑定时,this值就是他们要绑定的那个对象。
3、执行阶段——Event Loop
事件循环机制 可以 完成异步操作,事件循环的执行机制,这里涉及到的概念包括:
- JS引擎线程、定时器线程、事件触发线程、异步http请求线程
- 回调队列Callback Queue
- 调用栈Call Stack
- macrotask 与 microtask
我们先来了解一下以下几个线程:
JS引擎线程:
- 也称为JS内核,负责处理Java脚本程序。(例如V8引擎)
- JS引擎线程负责解析Java脚本,运行代码。
事件触发线程:
- 归属于浏览器而不是JS引擎,用来控制事件循环。
- 当JS引擎执行代码块如setTimeOut或是来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等会将对应任务添加到事件线程中,当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列(回调队列)的队尾,等待JS引擎的处理。
定时触发器线程:
- 传说中的setInterval与setTimeout所在线程,浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的, 如果处于阻塞线程状态就会影响计时的准确,因此通过单独线程来计时,当计时完毕后将其回调函数添加到回调队列中,等待JS引擎空闲后从回调队列队首取出函数执行。
异步http请求线程:
- 通过XMLHttpRequest连接后,通过浏览器新开一个线程请求,将检测到readyState状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中,再由JS引擎执行。
注:永远只有JS引擎线程在执行JS脚本程序,以上提及的三个线程只负责将满足触发条件的处理函数推进回调队列中,等待JS引擎线程执行。
事件循环图解如下:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
Event Loop只做一件事情,那就是负责监听Call Stack和Callback Queue。
当Call Stack里面的内容运行完变成空了, Event Loop就把Callback Queue里面的第一条事件(回调函数)放到调用栈中并执行它,后续不断循环执行这个操作。
⚠️:异步任务又分为:宏任务和微任务(优先级:微>宏)
4、总结:
js执行过程:词法分析 -> 预编译 -> 执行
预编译:js引擎会首先把整个文件进行预处理,以消除一些歧义,这个预处理的过程就叫做预编译
全局预编译:
- 产生windows对象
- 查找变量的声明,把变量做为GO对象的属性名,属性值为undefined
- 查找函数的声明,把函数名作为GO对象的属性名,属性值是function
函数预编译:
- 在函数被调用时,为当前函数产生AO对象
- 找到形参和变量声明,并且将它们赋值为
undefined
; - 找到形参对应的实参,并且将实参的值赋予形参;
- 找函数声明并且将函数体赋给函数声明;
⚠️:优先级:局部函数 > 实参 > 形参和局部变量