JavaScript系列之执行上下文和执行栈

若是你想成为一名优秀的JavaScript 开发者,那你就必须了解 JavaScript 程序内部的执行机制。而执行上下文和执行栈是其关键概念之一, 理解执行上下文和执行栈一样有助于理解其余的 JavaScript 概念如提高机制、做用域和闭包等。javascript

执行上下文和执行栈是JavaScript的难点之一,因此本人尽可能用通俗易懂的方式来阐述这些概念。java

执行上下文(Execution Context)

当 JavaScript 代码执行一段可执行代码(executable code)时,会建立对应的执行上下文(execution context)。执行上下文(可执行代码段)总共有三种类型:git

  • 全局执行上下文(全局代码):不在任何函数中的代码都位于全局执行上下文中,只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。
  • 函数执行上下文(函数体):只有调用函数时,才会为该函数建立一个新的执行上下文,能够存在无数个,每当一个新的执行上下文被创-建,它都会按照特定的顺序执行一系列步骤。
  • Eval 函数执行上下文(eval代码): 指的是运行在 eval 函数中的代码,不多用并且不建议使用。

执行上下文又包括三个生命周期阶段:建立阶段→执行阶段→回收阶段,本文重点介绍建立阶段。github

1.建立阶段浏览器

当函数被调用,但未执行任何其内部代码以前,会作如下三件事:闭包

  • 建立变量对象(Variable object,VO):首先初始化函数的参数arguments,提高函数声明和变量声明。后文会详细说明。
  • 建立做用域链(Scope Chain):在执行上下文的建立阶段,做用域链是在变量对象以后建立的。做用域链自己包含变量对象。做用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,若是最内层没有找到变量,就会跳转到上一层父做用域中查找,直到找到该变量。后文会详细说明。
  • 肯定this指向:包括多种状况,后文会详细说明。

在一段 JS 脚本执行以前,要先解析代码(因此说 JS 是解释执行的脚本语言),解析的时候会先建立一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来。变量先暂时赋值为undefined,函数则先声明好可以使用。这一步作完了,而后再开始正式执行程序。函数

另外,一个函数在被执行以前,也会建立一个函数执行上下文环境,跟全局上下文差很少,不过函数执行上下文中会多出thisarguments和函数的参数。ui

2.执行阶段this

进入执行上下文、执行代码spa

3.回收阶段

执行完毕后执行上下文出栈并等待垃圾回收

执行上下文栈(Execution Context Stack)

假如咱们写的函数多了,每次调用函数时都建立一个新的执行上下文,如何管理建立的那么多执行上下文呢?

因此 JavaScript 引擎建立了执行上下文栈(Execution context stack,ECS)来管理执行上下文,具备 LIFO(后进先出)的栈结构,用于存储在代码执行期间建立的全部执行上下文。

首次运行JS代码时,会建立一个全局执行上下文并Push到当前的执行栈中。每当发生函数调用,引擎都会为该函数建立一个新的函数执行上下文并Push到当前执行栈的顶部,浏览器的JS执行引擎老是访问栈顶的执行上下文。

根据执行栈LIFO规则,当栈顶函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文控制权将移到当前执行栈的下一个执行上下文,最终移回到全局执行上下文,全局上下文只有惟一的一个,它在浏览器关闭时Pop出。

看到目前为止,是否以为这两个概念仍是有点晦涩难懂呢?那...接下来经过几小段代码和图解来详细介绍并理解吧。

执行上下文是如何执行的呢?

让咱们先来看一下这段简单代码:

function b(){
}
function a(){
  b();
}
a();
复制代码

这段代码背后执行的逻辑是这样的:

首先,全局执行上下文(Global Execution Context)会被创建,这时候会一并创建thisglobal object (window),在函数开始执行的过程当中,function ab因为JS提高机制的缘故会先被创建在内存中,接着才会开始逐行执行函数。

接着,代码会执行到a( )这个部分,这时候,会创建a的执行上下文(execution context),而且被放置到执行栈(execution stack)中。在这个execution stack中,最上面的execution context会是正在被执行的a( )。以下图:

function a 的execution context创建后,便会开始执行function a中的内容。因为在function a( ) 里面有去执行function b ,所以,在这个execution stack中,接下来最上面会变成function b 的execution context。以下图:

function b 执行完以后,会从execution stack中离开,继续逐行执行function a。当function a 执行完以后,同样会从execution stack中抽离,再回到Global Execution Context逐行执行。以下图:

不一样执行上下文中的变量是不一样的

在了解了通常的函数其运做背后的逻辑后,让咱们来看一下这段代码:

function b(){
  var myVar;
  console.log(myVar);
}

function a(){
  var myVar = 2;
  b();
  console.log(myVar);
}

var myVar = 1;
console.log(myVar);
a();
复制代码

你能够想像,若是咱们在不一样的execution context中去把myVar这个变量打出来,会获得什么结果呢?结果以下:

咱们分别获得了一、undefined和2。为何会这样呢?

让咱们来看看这段代码背后执行的逻辑:

首先,全局执行上下文(Global Execution Context)会被创建,因为变量提高的缘故,myVarfunction ab都会被创建并储存在内存中,接着便开始逐行执行函数。一开始会碰到var myVar = 1因此,最外层的myVar便被给值为1,接着执行到了console.log(myVar),这是在global execution context执行的,因而获得了第一个1的结果:

而后执行到了a ( ),因而创建了a的execution context,这时候因为逐行执行的关系,会先执行到var myVar = 2,但由于这是在function a的execution context中,因此并不会影响到global execution context的myVar

在执行完function a中的var myVar = 2后,继续逐行执行,因而执行到了b ( ),这时候,function b的execution function便被创建,并且会先去执行function b里面的内容:

function b的execution function创建后,会开始逐行执行function b里面的内容,因而读到了var myVar;,这时候在function b这个execution context中的myVar变量被创建,可是还没被赋值,因此会是undefined。和上面提到的同样,因为这个myVar是在function b中的execution context所创建,因此并不会影响到其余execution context的myVar,这时候执行到了function b的 execution context中的console.log(myVar),因而获得了第二个看到的undefined

最后,function b执行完以后,会从execution stack中离开,继续回到function a中的b( )后逐行执行,也就是console.log(myVar),这时候是在function a的execution context加以执行的,所以也就获得告终果中看到的第三个2了。

最后因为b ( ) 后面已经没有内容,function a执行完毕,这时候,function a也会从execution stack中抽离。

最后回到Global Execution Context,若是函数中的a( )后面还有内容的话,会继续进行逐行执行。

由上面的例子,咱们能够知道,咱们是在不一样的execution context中分别去声明变量myVar的,所以在不一样的execution context,变量彼此之间不会影响,因此虽然这三个变量都叫作myVar,但实际上是三个不一样的变量。

因为咱们是在不一样的execution context中去声明变量,因此这实际上是位于三个不一样execution context中的变量,因此即便咱们是在执行完a( )后再去调用一次myVar,同样会获得" 1"的结果:

function b(){
  var myVar;
  console.log(myVar);
}

function a(){
  var myVar = 2;
  b();
  console.log(myVar);
}

var myVar = 1;
console.log(myVar);
a();
console.log(myVar);  // 同样会获得"1"
复制代码

注意

最后须要注意的是,若是是在function里面直接使用myVar这个变量,而没有经过var从新声明它的话,就会获得不一样的结果!由于在函数做用域内加 var 定义的变量是局部变量,不加 var 定义的就成了全局变量。在未声明新的变量的状况下,在该execution context中JavaScript 引擎找不到这个变量,它就会往它的外层去寻找,最后会获得,1 ,2 ,2 ,2 的结果:

function b(){
  myVar;
  console.log(myVar);
}

function a(){
  myVar = 2;
  b();
  console.log(myVar);
}

var myVar = 1;
console.log(myVar);
a();
console.log(myVar);

/* 打印出 1 2 2 2 */
复制代码

若是以为文章对你有些许帮助,欢迎在个人GitHub博客点赞和关注,感激涕零!

相关文章
相关标签/搜索