本文3356字,阅读大约须要9分钟。
总括: 本文深刻的讲解了Javascript中的执行上下文和执行栈。javascript
流水在碰到底处时才会释放活力。前端
若是你是或者想成为一名Javascript开发者,那就必需要知道Javascript内部是如何执行的。正确的理解Javascript中的执行上下文和执行栈对于理解其它Javascript概念(好比变量提高,做用域,闭包等)相当重要。java
正确的去理解Javascript执行上下文和执行栈将会是你成为一名更好的Javascript开发者。编程
很少废话,咱们如今就开始:)数组
简单的来讲,执行上下文是一种对Javascript代码执行环境的一种抽象概念,也就是说只要有Javascript代码运行,那么它就必定是运行在执行上下文中。浏览器
Javascript一共有三种执行上下文:数据结构
window
对象(浏览器环境下),并将this
的值设置为该全局对象,另一个程序中只能有一个全局上下文。eval
函数中执行的代码也会有本身的执行上下文,但因为eval
函数不会被常常用到,这里就不作讨论了。(译者注:eval
函数容易致使恶意攻击,而且运行代码的速度比相应的替代方法慢,由于不推荐使用)。执行栈,在其余编程语言中也被称为“调用栈”,这是一种后进先出(LIFO)的数据结构,被用来储存在代码运行阶段建立的全部的执行上下文。闭包
当Javascript引擎(译者注:Javascript引擎是执行Javascript代码的解释器,通常被内嵌在浏览器中)开始执行你第一行Javascript脚本代码的时候,它就会建立一个全局执行上下文而后将它压到执行栈中。每当引擎碰到一个函数的时候,它就会建立一个函数执行上下文,而后将这个执行上下文压到执行栈中。(译者注:这种结构相似弹夹,执行栈就是弹夹,执行上下文是子弹,子弹被一个个压入弹夹,当子弹发射的时候,最后一个进弹夹的子弹会被最早射出)。编程语言
引擎会执行位于执行栈栈顶的执行上下文(通常是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,而后控制流程到达执行栈的下一个执行上下文。ide
结合下面的代码来理解下:
let a = 'Hello World!'; function first() { console.log('Inside first function'); second(); console.log('Again inside first function'); } function second() { console.log('Inside second function'); } first(); console.log('Inside Global Execution Context');
<div align="center">上述代码的执行栈</div>
当上述代码在浏览器中加载时,Javascript引擎首先建立一个全局执行上下文并将其压入执行栈中,而后碰到first()
函数被调用,此时再建立一个函数执行上下文压入执行栈中。
当second()
函数在first()
函数中被调用时,引擎再针对这个函数建立一个函数执行上下文将其压入执行栈中,second
函数执行完毕后,对应的函数执行上下文被推出执行栈销毁,而后控制流程到下一个执行上下文也就是first
函数。
当first
函数执行结束,first函数执行上下文也被推出,引擎控制流程到全局执行上下文,直到全部的代码执行完毕,全局执行上下文也会被推出执行栈销毁,而后程序结束。
如今咱们已经了解了Javascript引擎是如何去处理执行上下文的,那么,执行上下文是如何建立的呢?
执行上下文的建立分为两个阶段:
执行上下文是在建立阶段被建立的,建立阶段包括如下几个方面:
所以执行上下文能够抽象为下面的形式:
ExecutionContext = { LexicalEnvironment = <ref. to LexicalEnvironment in memory>, VariableEnvironment = <ref. to VariableEnvironment in memory>, }
ES6的官方文档 把词法环境定义为:
词法环境(Lexical Environments)是一种规范类型,用于根据ECMAScript代码的词法嵌套结构来定义标识符与特定变量和函数的关联。词法环境由一个环境记录(Environment Record)和一个可能为空的外部词法环境(outer Lexical Environment)引用组成。
简单来讲,词法环境就是一种标识符—变量映射的结构(这里的标识符指的是变量/函数的名字,变量是对实际对象[包含函数和数组类型的对象]或基础数据类型的引用)。
举个例子,看看下面的代码:
var a = 20; var b = 40; function foo() { console.log('bar'); }
上面代码的词法环境相似这样:
lexicalEnvironment = { a: 20, b: 40, foo: <ref. to foo function> }
每个词法环境由下面三部分组成:
this
;所谓的环境记录就是词法环境中记录变量和函数声明的地方。
环境记录也有两种类型:
window
对象)。所以,对于对象的每个新增属性(对浏览器来讲,它包含浏览器提供给window
对象的全部属性和方法),都会在该记录中建立一个新条目。注意:对函数而言,环境记录还包含一个arguments
对象,该对象是个类数组对象,包含参数索引和参数的映射以及一个传入函数的参数的长度属性。举个例子,一个arguments
对象像下面这样:
function foo(a, b) { var c = a + b; } foo(2, 3); // argument 对象相似下面这样 Arguments: { 0: 2, 1: 3, length: 2 }
(译者注:环境记录对象在建立阶段也被称为变量对象(VO),在执行阶段被称为活动对象(AO)。之因此被称为变量对象是由于此时该对象只是存储执行上下文中变量和函数声明,以后代码开始执行,变量会逐渐被初始化或是修改,而后这个对象就被称为活动对象)
对于外部环境的引用意味着在当前执行上下文中能够访问外部词法环境。也就是说,若是在当前的词法环境中找不到某个变量,那么Javascript引擎会试图在上层的词法环境中寻找。(译者注:Javascript引擎会根据这个属性来构成咱们常说的做用域链)
this
在词法环境建立阶段中,会肯定this
的值。
在全局执行上下文中,this
值会被映射到全局对象中(在浏览器中,也就是window
对象)。
在函数执行上下文中,this
值取决于谁调用了该函数,若是是对象调用了它,那么就将this
值设置为该对象,不然将this
值设置为全局对象或是undefined
(严格模式下)。例如:
const person = { name: 'peter', birthYear: 1994, calcAge: function() { console.log(2018 - this.birthYear); } } person.calcAge(); // 上面calcAge的 'this' 就是 'person',由于calcAge是被person对象调用的 const calculateAge = person.calcAge; calculateAge(); // 上面的'this' 指向全局对象(window),由于没有对象调用它,或者说是window调用了它(window省略不写)
词法环境抽象出来相似下面的伪代码:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符在这里绑定 } outer: <null>, this: <global object> } } FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符在这里绑定 } outer: <Global or outer function environment reference>, this: <depends on how function is called> } }
其实变量环境也是词法环境的一种,它的环境记录包含了变量声明语句在执行上下文中建立的变量和具体值的绑定关系。
如上所述,变量环境也是词法环境的一种,所以它具备词法环境全部的属性。
在ES6中,词法环境和变量环境的不一样就是前者用来存储函数声明和变量声明(let
和const
)绑定关系,后者只用来存储var
声明的变量绑定关系。
在这个阶段,将完成全部变量的赋值操做,而后执行代码。
咱们看几个例子来理解上面的概念:
let a = 20; const b = 30; var c; function multiply(e, f) { var g = 20; return e * f * g; } c = multiply(20, 30);
当上面的代码被执行的时候,Javascript引擎会建立一个全局执行上下文去执行全局的代码。因此全局执行上下文在建立阶段看起来会像下面这样:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符在这里绑定 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null>, ThisBinding: <Global Object> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符在这里绑定 c: undefined, } outer: <null>, ThisBinding: <Global Object> } }
在执行阶段,将完成变量的赋值操做,所以在执行阶段全局执行上下文看起来会像下面这样:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符在这里绑定 a: 20, b: 30, multiply: < func > } outer: <null>, ThisBinding: <Global Object> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符在这里绑定 c: undefined, } outer: <null>, ThisBinding: <Global Object> } }
当调用multiply(20, 30)
时,将为该函数建立一个函数执行上下文,该函数执行上下文在建立阶段像下面这样:
FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符在这里绑定 Arguments: { 0: 20, 1: 30, length: 2 }, }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined>, }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符在这里绑定 g: undefined }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined> } }
而后,执行上下文进入执行阶段,这时候已经完成了变量的赋值操做。该函数上下文在执行阶段像下面这样:
FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符在这里绑定 Arguments: { 0: 20, 1: 30, length: 2 }, }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined>, }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符在这里绑定 g: 20 }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined> } }
函数执行完成后,返回值存储在变量c
中,此时全局词法环境被更新。以后,全局代码执行完成,程序结束。
注意:你可能已经注意到上面代码,let
和const
定义的变量a
和b
在建立阶段没有被赋值,但var
声明的变量从在建立阶段被赋值为undefined
。
这是由于,在建立阶段,会在代码中扫描变量和函数声明,而后将函数声明存储在环境中,但变量会被初始化为undefined
(var
声明的状况下)和保持uninitialized
(未初始化状态)(使用let
和const
声明的状况下)。
这就是为何使用var
声明的变量能够在变量声明以前调用的缘由,但在变量声明以前访问使用let
和const
声明的变量会报错(TDZ)的缘由。
这其实就是咱们常常听到的变量声明提高。
注意:在执行阶段,若是Javascript引擎找不到let
和const
声明的变量的值,也会被赋值为undefined
。
如上,咱们讲解了Javascript代码是如何执行的,虽说成为一名优秀的Javascript开发者并不须要彻底搞懂这些概念,但对上面的概念有深刻的理解有助于咱们去学习和理解其它概念,好比:变量声明提高,闭包,做用域链等。
能力有限,水平通常,欢迎勘误,不胜感激。
订阅更多文章可关注公众号「前端进阶学习」,回复「666」,获取一揽子前端技术书籍