在执行一段代码时,JS 引擎会首先创建一个执行栈
然后JS引擎会创建一个全局执行上下文,并push到执行栈中, 这个过程JS引擎会为这段代码中所有变量分配内存并赋一个初始值(undefined),在创建完成后,JS引擎会进入执行阶段,这个过程JS引擎会逐行的执行代码,即为之前分配好内存的变量逐个赋值(真实值)。
如果这段代码中存在function的声明和调用,那么JS引擎会创建一个函数执行上下文,并push到执行栈中,其创建和执行过程跟全局执行上下文一样。但有特殊情况,即当函数中存在对其它函数的调用时,JS引擎会在父函数执行的过程中,将子函数的全局执行上下文push到执行栈,这也是为什么子函数能够访问到父函数内所声明的变量。
还有一种特殊情况是,在子函数执行的过程中,父函数已经return了,这种情况下,JS引擎会将父函数的上下文从执行栈中移除,与此同时,JS引擎会为还在执行的子函数上下文创建一个闭包,这个闭包里保存了父函数内声明的变量及其赋值,子函数仍然能够在其上下文中访问并使用这边变量/常量。当子函数执行完毕,JS引擎才会将子函数的上下文及闭包一并从执行栈中移除。
最后,JS引擎是单线程的,那么它是如何处理高并发的呢?即当代码中存在异步调用时JS是如何执行的。比如setTimeout或fetch请求都是non-blocking的,当异步调用代码触发时,JS引擎会将需要异步执行的代码移出调用栈,直到等待到返回结果,JS引擎会立即将与之对应的回调函数push进任务队列中等待被调用,当调用(执行)栈中已经没有需要被执行的代码时,JS引擎会立刻将任务队列中的回调函数逐个push进调用栈并执行。这个过程我们也称之为事件循环。
异步代码js是如何执行的:
遇到setTimeout,promise等,js引擎会将异步代码移出调用栈,当达到特定情形,比如定时器时间到了,比如请求数据到了,js引擎会立即将与之对应的回调函数push到宏任务队列或者微任务队列中,当调用栈中没有需要执行的代码时,js引擎会先全部执行微任务队列中的所有任务,再调用宏任务队列中的第一个任务,这个过程我们称为事件循环。
执行上下文:JavaScript 代码的执行环境
当 Javascript 代码在运行的时候,它都是在执行上下文中运行。
执行上下文类型:
常见的执行上下文有两种:
全局执行上下文 — 它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this
的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
执行上下文将经历创建阶段。在创建阶段会发生三件事:
this 值的决定,即我们所熟知的 This 绑定。
创建词法环境和变量环境组件。var let const function声明的变量;
闭包内存泄漏出现的原因的IE9以下的使用的引用计数回收策略而导致的一种现象;
某个变量的引用数量大于等于1,所以不会被回收。
而现代浏览器采用的是标记清除的方法,
存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量的标记和被环境中的变量引用的变量的标记,此后,如果变量再被标记则表示此变量准备被删除。
当然,当闭包被赋值给全局变量,并执行完毕以后,及时释放该全局变量的内存,以免造成内存上的浪费,因为全局变量不会被回收,它还自带一个执行环境;
优点是减少了全局变量,让一些变量仅在某个函数内使用,不会被污染。
标记清除
工作原理
当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
工作流程
垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记。
去掉环境中的变量以及被环境中的变量引用的变量的标记。
再被加上标记的会被视为准备删除的变量。
垃圾回收器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间
不再需要的内存没有及时释放,我们称为内存泄漏。内存泄漏可能会造成内存溢出,可能会导致页面卡顿,长时间无响应。
v8采用了一种分代回收策略,新生代和老生代,新生代内存空间主要用来存放存活时间较短的对象,老生代内存空间主要用来存放存活时间较长的对象。对于垃圾回收,新生代和老生代有各自不同的策略,下面依次进行介绍。
新生代将内存空间一分为二,闲置空间from和活动空间to,
在垃圾回收运行时时,会检查 From 中的对象,当某个对象需要被回收时,将其留在 From 空间,剩下的对象移动到 To 空间,然后进行反转,将 From 空间和 To 空间互换。进行垃圾回收时,会将 To 空间的内存进行释放。
新生代晋升:
1.一个对象经过多次复制,依然存活,那么就被升级为老生代内存中,
2.在新生代垃圾回收中,如果to空间内存使用量超过25%;那么将from空间中的对象升级为老生代;
老生代垃圾回收机制:
老生代内存空间,是一个连续的结构
老生代内存空间中的垃圾回收有标记清除(Mark Sweep)和标记合并(Mark Compact)两种方式。
Mark Sweep 标记清除,进行垃圾回收会产生一个问题,就是垃圾回收后内存会出现不连续的情况,为了解决这个问题,出现了 Mark Compact 方案。
Mark Compact 的思想有点像新生代垃圾回收时采取采用的机制:将存活的对象移动到一边,然后对需要被回收的对象区域进行整体的垃圾回收。