前言
一种直译式脚本语言,是一种动态类型、弱类型、基于原型的语言,内置支持类型。它的解释器被称为JavaScript引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在HTML网页上使用,用来给HTML网页增加动态功能。JavaScript兼容于ECMA标准,因此也称为ECMAScript。
一、代码块
JS中的代码块是指由<script>标签分割的代码段。JS是按照代码块来进行编译和执行的,代码块间相互独立(即就算代码块1出错,但不影响代码块2的加载和执行),但变量和方法共享。
在加载HTML页面的时候,当浏览器遇到内嵌的JS代码时会停止处理页面,先执行JS代码,然后再继续解析和渲染页面。同样的情况也发生在外链的JS文件中,浏览器必须先花时间下载外链文件中的代码,然后解析并执行它,在这个过程中,页面的渲染和用户互交完全被阻塞。由于现代浏览器都允许并行下载JS文件,因此<script>标签在下载外部资源时不会阻塞其他的<script>标签,但仍然会阻塞其他资源的下载。
为了安全起见,我们一般都在页面初始化完毕之后才允许JavaScript代码执行,这样可以避免网速对JavaScript执行的影响,同时也避开了HTML文档流对于JavaScript执行的限制。如果在一个页面中存在多个windows.onload事件处理函数,则只有最后一个才是有效的
二、预编译
在执行JS代码的时候,JS引擎并不是按照我们书写的顺序从上到下顺序编译并且执行的,首先是按照自己的规则对我们的代码先进行编译,然后从上到下执行编译后的代码。
在全局作用域中,JS首先会对我们的函数或者变量进行声明,就是我们经常听到的变量提升机制,然后才是按照我们书写代码的顺序,来进行编译,然后再执行编译的代码。
对于函数的变量提升,如果是函数式声明,那么直接提升(即可以在声明前调用),而如果是变量式声明,则是只有变量提升,值为undefined,当执行到赋值语句的时候,才能够调用函数。
wrap(); // 1
function wrap(){
console.log(1);
}
foo(); // undefined
var foo = function (){
console.log(2);
}
在函数作用域中的编译顺序为:首先对函数的存在的参数进行声明,然后是内部的函数,在然后是变量,然后在顺序编译我们书写的代码
三、执行期
在对JS代码进行预编译后,就按照编译后的代码进行从上到下的执行,遇到赋值的语句才对之前提升的变量进行赋值。同时在同一作用域中对变量进行赋值的时候,函数的赋值要快于一般常量的赋值。
在赋值的时候就需要注意到一点——作用域,这点主要体现在函数内的变量赋值。
var a = "134";
function b(){
alert(a); // undefined
var a="123";
alert(a); // "123"
}
从上面的例子中可以看出,在对访问变量的时候,首先会在当前作用域里查找,如果没有,则会顺着作用域链向上直到找到全局的作用域链
四、执行机制
JS是单线程语言,异步执行,JS的执行机制是事件循环Event Loop。
首先了解一下JS的任务,分为同步任务和异步任务。注意,只有主线程空了,才会去读取"任务队列",这就是JS的运行机制,这个过程会不断重复。
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕了,才会执行后一个任务。
异步任务:在主线程之外,还存在一个“任务列队”,异步任务就是不进入主线程,而是进入“任务列队”的任务,只有“任务列队”通知主线程,某个异步任务可以执行了并且同步任务执行完毕,该任务才会进入主线程执行。
一个浏览器环境,只能有一个事件循环,而一个事件循环可以多个任务队列,队列之间可有不同的优先级,同一队列中的任务按先进先出的顺序执行,但是不保证多个任务队列中的任务优先级,具体实现可能会交叉执行
运行机制:
- 所有同步任务直接按照顺序在主线程上执行,形成一个执行栈(execution context stack)
- 如果是异步任务,直接调用浏览器的Web API,依赖浏览器的多线程机制进行计算,当异步任务有运行结果后,将事件推到主线程之外的“任务队列”(task queue)
- 一旦“执行栈”中的所有同步任务执行完毕,系统就会自动的读取“任务队列”中的事件,进入执行栈,然后执行
但如果异步任务里面包含异步任务怎么办呢?请看下面的例子
setTimeout(function(){
console.log('定时器开始啦')
});
new Promise(function(resolve){
console.log('马上执行for循环啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('执行then函数啦')
});
console.log('代码执行结束');
如果按照上面的分析,首先遇到异步任务setTimeout,推入“任务队列”;然后同步任务Promise,进入“执行栈”,执行console.log,然后遇到回调函数then,异步任务,推入“任务队列”;最后执行console.log,执行完毕后,执行栈空,马上读取任务队列,任务队列是先进先出,按道理应该先执行setTimeout,然后执行回调函数then。
那么此时的打印结果应该是:马上执行for循环啦---代码执行结束---定时器开始啦---执行then函数啦,然而实际的结果却是:
这个时候估计就要问为什么了?难道异步任务的执行顺序,不是先后顺序?事实上,按照异步和同步的划分方式,并不准确。
而准确的划分方式是:
macro-task(宏任务):包括整体代码script,setTimeout,setInterval
micro-task(微任务):Promise的回调,process.nextTick
这里需要注意一下await标志,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。
按照这种分类方式,JS的执行机制是:
- 执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的“事件队列”里
- 当目前的宏任务执行完成后,会查看微任务的“事件队列”,并将里面全部的微任务依次执行完,然后接着执行下一个宏任务
- 重复以上2步骤,结合两种event loop,就是更为准确的JS执行机制了
尝试按照刚学的执行机制,去分析刚才的例子:
- 首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的“队列”里
- 遇到 new Promise直接执行,打印"马上执行for循环啦"
- 遇到then方法,是微任务,将其放到微任务的“队列”里。
- 打印 "代码执行结束"
- 本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数,打印"执行then函数啦"
- 到此,本轮的event loop 全部完成。
- 下一轮的循环里,先执行一个宏任务,发现宏任务的“队列”里有一个setTimeout里的函数,执行打印"定时器开始啦"
所以最后的执行顺序是:马上执行for循环啦---代码执行结束---执行then函数啦---定时器开始啦