Skip to main content

代码执行原理

js 是解释型语言

js 引擎执行流程

分为两个阶段:

  1. 语法分析
  2. 执行阶段

执行阶段涉及的数据结构:

  • 调用栈。处理执行上下文和执行代码
  • 内存堆。给对象分配内存
  • 任务队列。暂存待执行的任务,分为宏任务队列和微任务队列

语法分析

词法分析 > 语法分析 > 代码生成(字节码)

执行阶段

代码生成后 js 引擎会先创建执行上下文(也叫预编译),再逐块(执行上下文)逐行执行代码

执行上下文

分类:

  • 全局执行上下文
  • 函数执行上下文
  • eval 函数执行上下文(下文暂不提及)

第一次读取 js 脚本时会生成全局执行上下文,有且只有一个,始终位于调用栈底部。当函数被调用时,会创建一个函数执行上下文并推入当前栈顶,执行完函数会出栈。栈顶是当前活动的执行上下文

image.png

每次创建执行上下文主要有以下几个步骤:

  1. 初始化作用域链
  2. 创建变量对象
  3. 创建 arguments 对象,检查参数上下文,初始化名称和值,并创建引用副本
  4. 扫描上下文中函数的声明
    • 对于找到的每个函数,在变量对象中创建一个属性,该属性是确切的函数名,该函数在内存中有一个指向该函数的引用指针
    • 如果函数名已经存在,指针将会被覆盖
  5. 扫描变量的声明
    • 对于找到的每个变量,在变量对象中创建一个属性,该属性是确切的变量名,该变量的值是 undefined
    • 如果变量名已经存在,将不会做任何处理继续执行
  6. 确定 this 的指向

JavaScript 执行上下文——JS 的幕后工作原理

变量、函数提升

函数和变量声明提升是在创建变量中进行的,举个例子:

function foo(a) {
console.log(b);
console.log(foo2);
console.log(c);
var b = 2;
function foo2() {}
var c = function () {};
b = 3;
}

foo(1);

创建执行上下文时,此时的 AO 是

AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
foo2: reference to function foo2(){},
c: undefined
}

最终的代码执行顺序其实就类似下面的代码:

function foo(a) {
var b;
function foo2() {}
var c;
console.log(b);
console.log(foo2);
console.log(c);
b = 2;
function foo2() {}
c = function () {};
b = 3;
}

为什么 let/const 不存在变量提升?

js 变量的生命周期如下:

  • 声明阶段(Declaration phase)。在作用域中注册一个变量
  • 初始化阶段(Initialization phase)。分配内存并为作用域中的变量创建绑定
  • 赋值阶段(Assignment phase)。为初始化的变量赋值

let/const 在进入块级作用域后,会因为提升的原因先注册到作用域中,但不会被初始化,直到声明语句执行的时候才被初始化,初始化的时候如果使用 let 声明的变量没有赋值,则会默认赋值为 undefined,而 const 必须在初始化的时候赋值。而声明阶段到初始化之间的代码片段就形成了暂时性死区。更详细的内容可以参考 https://blog.csdn.net/weixin_39902608/article/details/112721083

函数声明和变量声明哪个优先?

从上面执行上下文的过程中可以看到,函数声明优先级更高,可以看下面这个例子

function func() {
foo();
function foo() {
console.log("foo1");
}
var foo = function () {
console.log("foo2");
};
}
func();

作用域和作用域链

参考 通过动图了解 JS 中的 ECStack、EC、VO 和 AO

作用域指的是变量的可访问性,是在 js 编译阶段就已经确定的。作用域可分为以下几种:

  • 全局作用域
  • 函数作用域
  • 块级作用域

通常在函数中,查找变量会先从当前执行上下文的 VO 中查找,如果没有就到其父级执行上下文的 VO 查找,直至全局执行上下文的 VO。这流程所形成的链表就是作用域链(Scope Chain)

[[scope]]

  • 函数创建时,会生成的一个 JS 内部的隐式属性
  • 函数存储作用域链的容器
    • 作用域链:
      • AO - 函数的执行期上下文
      • GO - 全局的执行期上下文
  • 函数执行完成以后,AO 会被销毁,再执行会重新创建一个新的 AO
  • 全局执行的前一刻 GO -> 函数声明已经定义
  • 存储闭包变量 Closure

作用域和执行上下文的区别是什么?

执行上下文是在函数调用的时候才会产生,并且会确定 this 的指向

this 指向

  • 默认绑定全局对象
  • 隐式绑定:this 指向调用它的对象
  • 显式绑定:apply/call/bind
  • new 绑定:指向新创建的对象
  • 箭头函数:指向定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象

闭包

闭包 = 函数 + 访问了其他函数内部的变量,即使创建变量的上下文已经销毁,它仍然存在,可以理解为作用域链的延伸。对于基本类型变量如果存在于闭包当中,那么将存储在堆中

滥用闭包可能会造成内存泄漏

执行代码

image.png

执行代码的过程其实就是把调用栈里面的调用帧(一个执行上下文)依次执行,同时在内存堆中给新生成的变量分配空间。结合事件循环(Event loop),执行流程如下:

  • 执行同步代码(可以看做第一个宏任务)
  • 更新队列。遇到新的宏任务后将其 push 到宏任务队列,遇到新的微任务将其 push 到微任务队列
  • 执行完同步代码后,要执行完所有的微任务,清空为任务队列(可能还会继续更新队列)
  • 执行下一个宏任务