浅谈Js的执行机制

412 阅读7分钟

前言

众所周知,在 js 中 var 定义的变量是存在声明提升的,它的声明提升并不是单纯的将变量提升到最上面,而是与js的执行机制有关,下面让我们从声明提升开始浅谈一下js的执行机制。

1.声明提升

概念:在 JavaScript 代码执行过程中,JavaScript 引擎把通过var所定义的变量和函数的声明放到它作用域的头部。将var定义的变量默认赋值undefined,将函数的声明放到最前面。

ps:let和const声明的变量不存在声明提升

我们看下面这个例子:

show()
console.log(a)

var a = 1
function show() {
    console.log('show被执行')
}

//输出:
//show被执行
//undefined

上述代码执行的时候可以看成以下代码:

var a
function show() {
    console.log('show被执行')
}

show()
console.log(a)
a = 1

代码执行结果之所以会这样,这就关系到JavaScript的执行机制了

2. JavaScript 代码的执行机制

image.png 以上图片所展示的就是js的执行机制,可以概括为三个阶段读取代码、编译和执行。当一段js代码被V8引擎读取的时候会先进行预编译,然后创建上下文对象并且放入调用栈中,最后再执行。在这个过程中,上下文对象大家可以理解为一个储存数据的集合,它其中包含了变量环境、词法环境。而调用栈可以理解为存放函数调用关系的一种栈结构,它具有先进后出的特点,先调用的函数先入栈,后调用的函数先出栈。

变量环境:主要用来储存var定义的变量和函数的声明

词法环境:主要用来储存letconst定义的变量和函数的词法作用域信息

调用栈:用来管理函数调用关系的一种数据结构 JS利用这种栈结构来管理执行上下文,通常把这种用来管理执行上下文的栈称为执行上下文栈

下面我们来了解一下js的编译阶段以及执行阶段

2.1编译阶段

js在进行编译时主要进行以下几个步骤:

  1. 创建上下文对象
  2. 查找变量声明,将变量名作为key,value为undefined
  3. 统一形参和实参的值,用实参的值覆盖形参 (全局作用域没有该步骤)
  4. 查找函数声明,将函数名作为key,value为函数体。如果有变量的key与函数的key则会用函数的value覆盖变量的value

js的代码在编译阶段不同的作用域在编译时过程有些不同,我们主要了解全局作用域和函数作用域是如何进行编译以及它们的区别。

全局作用域

在全局作用域中进行代码编译时会进行如下步骤:

  1. 创建上下文对象
  2. 查找变量声明,将变量名作为key,value为undefined
  3. 查找函数声明,将函数名作为key,value为函数体。如果有变量的key与函数的key则会用函数的value覆盖变量的value

下面用一段代码来演示该过程:

console.log(a)
var a = 1
function a() {
  console.log(1);
}

//输出:
//[Function: a]

上述代码的编译过程为先创建全局上下文并且压入调用栈内,然后进行变量以及函数的查找,最后再执行代码。该过程可以看成以下代码片段:

var a
function a() { 
    console.log(1); 
}
console.log(a)
a = 1

编译过程如以下图片

image.png

函数作用域

在函数作用域中进行编译时会进行如下过程:

  1. 创建上下文对象
  2. 找形参和变量声明,将形参和声明的变量名作为 key, value 为undefined(先找形参再找变量声明)
  3. 统一形参和实参的值,用传入的实参的值覆盖形参
  4. 找函数声明,将函数名作为 key, value 为函数体(如果有变量名字与函数名相同,将变量值变成函数体)

下面用一段代码来演示该过程:

function add(a) {
    console.log(a)
    function a() {}
    var a = 3
    console.log(a)
}
add(1)

//输出:
//[Function: a]
//3

可能有些人会觉得有点奇怪,函数明明传入实参值为1,最后打印结果应该是1和3,可是为什么最后是先输出个函数体,然后输出3呢,这就关系到在函数作用域中的编译了。

在上述代码进行编译的时候,首先创建一个上下文对象并且压入调用栈中,然后对全局进行查找发现函数add将其进行声明,然后执行add函数。在函数add中查找变量。首先查找到形参a,然后查找到函数体内有个var变量声明,这时候创建a = undefined存在变量环境中。

然后统一形参与实参的值,因为函数传入的实参值为1,所以将1赋给a,此时的a = 1

最后查找函数的声明,在这个函数体中找到个函数function a() {},因为与变量a同一个key,所以再次将a的值进行改变,让a = function,最后再到函数体内从上到下执行一遍。

以下是过程图解:

1731584956279.jpg

2.2执行阶段

在了解完编译阶段在全局作用域和函数作用域的编译过程之后,最后我们来了解一下从代码的编译到执行这一完整步骤。

下面提供一段代码用以演示过程:

var a = 2
function add(b, c) {
  return b + c
}

function addAll(b, c) {
  var d = 10
  var result = add(b, c)
  return a + result + d 
}

addAll(3, 6)

//输出:
//21

在不了解js的执行机制之前可能很简单就看出来最后的输出结果是21,下面在了解了js的执行机制之后我们来看一下代码是如何执行的。

首先对全局代码进行编译,先查找变量和函数发现变量var定义的变量a、函数addAll和add,对这三个进行声明a = undefined、add = function、addAll = function。然后执行代码并且更新变量和函数,a = 2、函数addAll执行。

image.png

在执行函数addAll时,创建函数addAll上下文并且压入调用栈内。这时来到了函数作用域内,此时用函数作用域的编译规则对代码进行编译,经过编译后生成了变量b = 3, c = 6, d = undefined, result = undefined,然后执行函数体的内容。

image.png

在执行的过程中发现了函数add的调用,然后创建函数add上下文对象压入栈中,并且对其进行编译b = 3,c = 6,然后执行函数add。 以上过程在执行完后从栈顶依次弹出销毁,最后完成执行过程。 在调用栈中的顺序在最右侧的call stack下(在浏览器检查中可查看):

1731500449658.jpg

栈中每个上下文所储存的数据:

image.png

总结

  1. js运行机制为先编译再执行,编译发生在执行的前一刻
  2. 在js代码的编译过程中var声明的变量存在声明提升会先定义为undefined,函数也会先提升
  3. 代码执行时,变量才会被赋予具体的值
  4. 当某个函数被调用时就会有新的上下文为其创建,并且赋予其对应的具体函数体
  5. 当一个函数执行完毕之后,它的上下文就会被出栈销毁

3.检验成果

function fn(a) {
  console.log(a);
  var a = 123
  console.log(a);
  function a() {}
  console.log(a)
  var b = function () {}
  console.log(b);
  function d() {}
  var d = a
  console.log(d);
}
fn(1)

自行分析一下上述代码的运行结果,结果如下:

image.png