# JavaScript运行机制 (opens new window)
# JavaScript运行三部曲
- 语法分析
- 预编译
- 解释执行
语法分析很简单,就是引擎检查你的代码有没有什么低级的语法错误; 解释执行顾名思义便是执行代码了; 预编译简单理解就是在内存中开辟一些空间,存放一些变量与函数 ;
# JS预编译什么时候发生
预编译到底什么时候发生? 误以为预编译仅仅发生在script内代码块执行前 这倒并没有错 预编译确确实实在script代码内执行前发生了 但是它大部分会发生在函数执行前
先来区分理解一下这2个概念: 变量声明 var ... 函数声明 function(){}
<script>
var a = 1;
console.log(a);
function test(a) {
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
var b = function() {}
console.log(b);
function d() {}
}
var c = function (){
console.log("I at C function");
}
console.log(c);
test(2);
</script>
# 分析过程如下:
- 页面产生便创建了GO全局对象(Global Object)(也就是window对象);
- 第一个脚本文件加载;
- 脚本加载完毕后,分析语法是否合法;
- 开始预编译 查找变量声明,作为GO属性,值赋予undefined; 查找函数声明,作为GO属性,值赋予函数体;
# 注意:
预编译阶段发生变量声明和函数声明,没有初始化行为(赋值),匿名函数不参与预编译 ; 只有在解释执行阶段才会进行变量初始化 ;
# 预编译小结
- 预编译两个小规则
- 函数声明整体提升-(具体点说,无论函数调用和声明的位置是前是后,系统总会把函数声明移到调用前面)
- 变量 声明提升-(具体点说,无论变量调用和声明的位置是前是后,系统总会把声明移到调用前,注意仅仅只是声明,所以值是undefined)
- 预编译前奏
- imply global 即任何变量,如果未经声明就赋值,则此变量就位全局变量所有。(全局域就是Window)
- 一切声明的全局变量,全是window的属性; var a = 12;等同于Window.a = 12;
- 函数预编译发生在函数执行前一刻。
# 作用域链
全局作用域: 代码在程序的任何地方都能被访问,window 对象的内置属性都拥有全局作用域
函数作用域: 在固定的代码片段才能被访问
每个
js函数都是一个对象,对象中有些属性我们可以访问,有些不可以,这些属性仅供js引擎存取,[[scope]]就是其中一个,指的就是我们所说的作用域,其中存储了执行期上下文的集合。这个集合呈链式链接,我们把这种链式链接叫做作用域链,通俗点说就是在函数作用域中查找变量时,在当前作用域找不到则向上级作用域查找,直到全局作用域的过程。function fn() { console.log(111) } fn() //fn defined [[scope]] 0 GO //fn running [[scope]] 0 fnAO // 1 GO //fn定义时作用域链只有GO,执行时会生成自己的AO,在作用域链顶端词法作用域:函数的作用域在函数定义的时候就决定了
var value = 1; function foo() { console.log(value); } function bar() { var value = 2; foo(); } bar() // 1
# JS 堆栈内存释放
- 堆内存:存储引用类型值,对象类型就是键值对,函数就是代码字符串。
- 堆内存释放:将引用类型的空间地址变量赋值成
null,或没有变量占用堆内存了浏览器就会释放掉这个地址 - 栈内存:提供代码执行的环境和存储基本类型值。
- 栈内存释放:一般当函数执行完后函数的私有作用域就会被释放掉。
但栈内存的释放也有特殊情况:① 函数执行完,但是函数的私有作用域内有内容被栈外的变量还在使用的,栈内存就不能释放里面的基本值也就不会被释放。② 全局下的栈内存只有页面被关闭的时候才会被释放
# 堆,栈、队列
# 堆(Heap)
堆是一种数据结构,是利用完全二叉树维护的一组数据,堆分为两种,一种为最大堆,一种为最小堆,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。 堆是线性数据结构,相当于一维数组,有唯一后继。
如最大堆
# 栈(Stack)
栈在计算机科学中是限定仅在表尾进行插入或删除操作的线性表。 栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。 栈是只能在某一端插入和删除的特殊线性表。
# 队列(Queue)
特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。
进行插入操作的端称为队尾,进行删除操作的端称为队头。 队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)
# Event Loop
Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
在JavaScript中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)。
# MacroTask(宏任务)
script全部代码、setTimeout、setInterval、setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN(opens new window))、I/O、UI Rendering。
# MicroTask(微任务)
Process.nextTick(Node独有)、Promise、Object.observe(废弃)、MutationObserver(具体使用方式查看这里 (opens new window))
# 浏览器中的Event Loop
Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。
# JS调用栈
JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。
我们看个例子:
function sum(a,b){
return a+b
}
function printSum(a,b){
let s = sum(a,b)
console.log(s)
}
printSum(1,2)
# 同步任务和异步任务
Javascript单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
导图要表达的内容用文字来表述的话:
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
# 执行进入microtask检查点时,用户代理会执行以下步骤:
- 设置microtask检查点标志为true。
- 当事件循环
microtask执行不为空时:选择一个最先进入的microtask队列的microtask,将事件循环的microtask设置为已选择的microtask,运行microtask,将已经执行完成的microtask为null,移出microtask中的microtask。 - 清理IndexDB事务
- 设置进入microtask检查点的标志为false。
事件循环,宏任务,微任务的关系如图所示:
执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务(microTask)队列是否为空,如果为空的话,就执行Task(宏任务),否则就一次性执行完所有微任务。
每次单个宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环。
# 举个例子
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
以上执行帧动画可以查看Tasks, microtasks, queues and schedules (opens new window) 或许这张图也更好理解些。
# NodeJS的Event Loop (opens new window)
# 闭包 (opens new window)
了解闭包前先来了解一下上级作用域和堆栈内存释放问题。
# 上级作用域的概念
- 函数的上级作用域在哪里创建创建的,上级作用域就是谁
var a = 10
function foo(){
console.log(a)
}
function sum() {
var a = 20
foo()
}
sum()
/* 输出
10
/
函数 foo() 是在全局下创建的,所以 a 的上级作用域就是 window,输出就是 10
# 闭包是什么
在 JS 忍者秘籍(P90)中对闭包的定义:闭包允许函数访问并操作函数外部的变量。红宝书上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。 MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。这里的自由变量是外部函数作用域中的变量。
概述上面的话,闭包是指有权访问另一个函数作用域中变量的函数
# 形成闭包的原因
内部的函数存在外部作用域的引用就会导致闭包。
var a = 0
function foo(){
var b =14
function fo(){
console.log(a, b)
}
fo()
}
foo()
复制代码
这里的子函数 fo 内存就存在外部作用域的引用 a, b,所以这就会产生闭包
# 闭包的作用
- 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
- 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化
# 闭包经典使用场景
return回一个函数
var n = 10
function fn(){
var n =20
function f() {
n++;
console.log(n)
}
return f
}
var x = fn()
x() // 21
这里的 return
f,f()就是一个闭包,存在上级作用域的引用。
- 函数作为参数
var a = '林一一'
function foo(){
var a = 'foo'
function fo(){
console.log(a)
}
return fo
}
function f(p){
var a = 'f'
p()
}
f(foo())
/* 输出
* foo
/
使用 return
fo返回回来,fo()就是闭包,f(foo())执行的参数就是函数fo,因为fo() 中的 a的上级作用域就是函数foo(),所以输出就是foo
- IIFE(自执行函数)
var n = '林一一';
(function p(){
console.log(n)
})()
/* 输出
* 林一一
/
同样也是产生了闭包
p(),存在window下的引用n。
- 循环赋值
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}
因为存在闭包的原因上面能依次输出1~10,闭包形成了10个互不干扰的私有作用域。将外层的自执行函数去掉后就不存在外部作用域的引用了,输出的结果就是连续的 10。为什么会连续输出10,因为 JS 是单线程的遇到异步的代码不会先执行(会入栈),等到同步的代码执行完
i++到 10时,异步代码才开始执行此时的i=10输出的都是 10。
- 使用回调函数就是在使用闭包
window.name = '林一一'
setTimeout(function timeHandler(){
console.log(window.name);
}, 100)
# 使用闭包需要注意什么
容易导致内存泄漏。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。
# 经典面试题
- for 循环和闭包(号称必刷题)
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]()
/* 输出
3
3
3
/
这里的
i是全局下的i,共用一个作用域,当函数被执行的时候这时的i=3,导致输出的结构都是3。
- 使用闭包改善上面的写法达到预期效果,写法1:自执行函数和闭包
var data = [];
for (var i = 0; i < 3; i++) {
(function(j){
setTimeout( data[j] = function () {
console.log(j);
}, 0)
})(i)
}
data[0]();
data[1]();
data[2]()
- 写法2:使用
let
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]()
let具有块级作用域,形成的3个私有作用域都是互不干扰的。