0%

浏览器原理学习笔记-浏览器中 js 执行机制(上)

变量提升

指在 js 代码执行过程中,js 引擎把变量声明部分和函数的声明部分提到代码开头的“行为”。变量被提升之后,会被设置默认值 undefined。

js 代码执行流程

实际上变量提升这一过程中,变量和函数声明在代码中的位置不会被改变,而是在编译阶段被 js 引擎放入内存之中。一段 js 代码在执行之前需要被 js 引擎编译,编译完成之后,才会进入执行阶段。

image-20200531230502237

1.编译阶段

这一过程输入一段 js 代码在经过编译之后会被分成两部分内容:执行上下文 和 可执行代码。

执行上下文是 js 执行一段代码时的运行环境,调用一段函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数。

执行上下文具体细节后面会介绍,现在只需知道,执行上下文中存在一个变量环境的对象,该对象中保存了变量提升的内容。

下面代码为例子:

1
2
3
4
5
6
showA()
console.log(a)
var a = 123;
function showA () {
console.log('function start')
}
  • 前面两行没有声明语句,因此 js 引擎不会做任何处理
  • 第 3 行,由于这行经过 var 声明,因此 js 引擎会在环境对象中创建一个名为 a 的属性,并使用 undefined 来初始化
  • 第 4 行,js 引擎发现了一个通过 function 定义的函数,所以它将函数定义存储到堆(HEAP) 中,并在环境对象中创建一个 showA 属性,然后将该属性指向堆中函数位置。

这样就生成了变量环境对象。接下来 js 引擎就会把声明以外的代码全部编译成字节码

2.执行阶段

js 引擎开始执行“可执行代码”,按照顺序一行行执行。

  • 执行到 showA 函数,js 引擎开始在变量环境对象中查找这个函数,变量环境对象中存在该函数的引用,所以 js 引擎便开始执行这个函数,并输出其结果
  • 然后打印 a 内容,js 引擎继续在变量环境对象中找该对象,由于变量里面有 a 变量,并且其值为 undefined,所以就输出这个
  • 然后执行 第三行,把 123 的值赋值给 a 变量,赋值之后变量环境中的 a 属性值变为 ‘123’

此时的变量环境为:

1
2
3
VariableEnviroment:
a -> '123',
showA -> function : { console.log('function start') }

那么以上就是一段代码的编译和执行过程了。实际上,这个过程是很复杂的,包括了词法分析 -> 语法解析->代码优化 -> 代码生成等。

如果在代码中出现同名的变量或者函数,会在编译阶段被最终的所覆盖掉。

调用栈:为什么 js 代码会出现栈溢出

一般这样一些代码会在执行之前就被编译并创建执行上下文:

  • js 执行全局代码时,编译全局代码并创建全局执行上下文,而且整个页面的生存周期之内,全局执行上下文只有一份
  • 调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束后会被销毁
  • 使用 eval 函数的时候,代码会被编译并创建执行上下文

调用栈是用来管理函数调用关系的一种数据结构

什么是函数调用

函数调用是运行一个函数。例如:

1
2
3
4
5
6
var a = 2;
function add () {
var b = 10;
return a + b;
}
add()

这段代码很简单,先创建一个 add 函数,接着在代码的最下面又调用了该函数。

下面利用这段简单的代码来解释函数调用的过程。

在执行到函数 add() 之前,js引擎为为其创建全局执行上下文,包含了声明的函数和变量,如图:

image-20200531234226222

代码中全局变量和函数都保存在全局上下文的变量环境中。

执行上下文准备好之后,便开始执行全局代码,执行到 add 这里的时候,js 判断这是个函数调用,会执行以下操作:

  • 从全局执行上下文中,取出 add 函数代码
  • 对 add 函数进行编译,并创建该函数的执行上下文可执行代码
  • 执行代码,输出结果

image-20200531235232379

执行到 add 函数的时候,我们会有两个执行上下文——全局执行上下文和 add 函数执行上下文。

js会使用栈来管理这些执行上下文。

js 调用栈

js 引擎利用栈来管理执行上下文。在创建好上下文之后, js 引擎会把执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈

以下面一段代码作为例子:

1
2
3
4
5
6
7
8
9
10
var a = 2;
function add (b, c) {
return b + c;
}
function addAll (b, c) {
var d = 10;
result = add(b, c)
return a + result + d
}
addAll(3, 6)
  1. 第一步创建全局上下文,并将其压入栈底。

    image-20200601000520600

变量 a, 函数 add 和 addAll 都保存到了全局上下文的变量环境对象中了。

全局执行上下文压入到调用栈后,js 引擎就开始执行全局代码了。首先会执行 a = 2 的赋值操作,执行该语句会将上下文变量环境中 a 的值设置为 2。设置后的全局上下文状态为:

image-20200601002451275

第二步是调用 addAll 函数。调用此函数的时候,js 引擎会编译该函数,并为其创建一个上下文,最后还将该函数的执行上下文压入栈中,如下图:

image-20200601003203316

先创建该函数的执行上下文,创建好之后,进入函数代码的执行阶段,先执行 d = 10 的赋值操作,执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成 10。

第三步执行到 add 函数同样会创建其的执行上下文环境,并将其压入栈。

image-20200601003507754

当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9。

image-20200601003617312

紧接着 addAll 执行最后一个相加操作后并返回, addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。

image-20200601003805613

至此,整个 js 流程就执行结束了。

调用栈是 js 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能追踪到哪个函数正在被执行以及各个函数之间的调用关系。

开发中使用调用栈

  1. 浏览器查看调用栈信息

在第三行处打上断点,然后刷新页面。会发现执行到 add 的时候,执行流程就暂停了,这时可以通过右边的 call stack 来查看当前的调用栈情况。

image-20200601004243038

Call Stack 那里可以清晰看到函数之间的调用关系。这在分析代码结构以及找 bug 时是非常关键的。

除了打断点,还可以通过 console.trace() 来查看函数之间的调用关系。

  1. 栈溢出

当我写一个没有边界条件的递归函数时:

1
2
3
4
function add (a, b) {
return add (a, b)
}
console.log(add(1, 2))

这时候 js 引擎执行这一段代码的时候,首先会调用函数 division,并创建执行上下文,压入栈中;然后这个函数时递归的,并且没有任何终止条件,所以它会一直创建新的函数执行上下文,并反复将其压入栈中,但是栈容量时有限的,所以就会出现栈溢出的错误。