JavaScript 中的执行上下文和执行栈实例讲解
JavaScript-原理系列
在日常开发中,每当我们接手一个现有项目后,我们总喜欢先去看看别人写的代码。每当我们看到别人写出很酷的代码的时候,我们总会感慨!写出这么优美而又简洁的代码的兄弟到底是怎么养成的呢?
我要怎样才能达到和大佬一样的水平呢!好了,废话不多说,让我们切入今天的主题。
一、执行上下文
简而言之,【执行上下文】就是JavaScript代码被解析和执行时所在环境的抽象概念,在JavaScript中运行任何的代码都是在它的执行上下文中运行。
在运行JavaScript代码时,每当需要执行代码时,执行代码会先进入一个环境(浏览器、Node客户端),这时就会为该环境创建一个执行上下文,它会在你运行代码前做一些准备工作,如确定作用域,创建全局、局部变量对象等。
执行上下文的分类
- 全局执行上下文:
这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。
它做了两件事:
- 创建一个全局对象,在浏览器中这个全局对象就是window对象。
将this指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
- 函数执行上下文:
每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论。
- Eval函数执行上下文:
运行在eval函数中的代码也获得了自己的执行上下文,但由于Javascript开发人员不常用eval函数,所以在这里不再讨论。
执行上下文的数量限制(堆栈溢出)
执行上下文可存在多个,虽然没有明确的数量限制,但如果超出栈分配的空间,会造成堆栈溢出。常见于递归调用,没有终止条件造成死循环的场景。
下面是示例代码:
//递归调用自身 functionfoo(){ foo(); } foo(); //报错:UncaughtRangeError:Maximumcallstacksizeexceeded
Tips:
JS是“单线程”的,每次只执行一段代码
二、执行栈
JS中的执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
当JavaScript引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
栈数据结构
现在让我们用一段代码来理解执行栈
leta='HelloWorld!'; functionfirst(){ console.log('Insidefirstfunction'); second(); console.log('Againinsidefirstfunction'); } functionsecond(){ console.log('Insidesecondfunction'); } first(); console.log('InsideGlobalExecutionContext');
下图是上面代码的执行栈
当上述代码在浏览器加载时,浏览器的JavaScript引擎会创建一个全局执行上下文并把它压入当前执行栈。当遇到函数调用时,JavaScript引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。
当从first()函数内部调用second()函数时,JavaScript引擎为second()函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当second()函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即first()函数的执行上下文。
当first()执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript引擎从当前栈中移除全局执行上下文。
TheCreationPhase
在JavaScript代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事:
- this值的决定,即我们所熟知的This绑定。
- 创建词法环境组件。
- 创建变量环境组件。
所以执行上下文在概念上表示如下:
ExecutionContext={ ThisBinding=, LexicalEnvironment={...}, VariableEnvironment={...}, }
This绑定:
在全局执行上下文中,this的值指向全局对象。(在浏览器中,this引用Window对象)。
在函数执行上下文中,this的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么this会被设置成那个对象,否则this的值被设置为全局对象或者undefined(在严格模式下)。例如:
letfoo={ baz:function(){ console.log(this); } } foo.baz();//'this'引用'foo',因为'baz'被 //对象'foo'调用 letbar=foo.baz; bar();//'this'指向全局window对象,因为 //没有指定引用对象
词法环境
官方的ES6文档把词法环境定义为
词法环境是一种规范类型,基于ECMAScript代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。
简单来说词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。
现在,在词法环境的内部有两个组件:(1)环境记录器和(2)一个外部环境的引用。
- 环境记录器是存储变量和函数声明的实际位置。
- 外部环境的引用意味着它可以访问其父级词法环境(作用域)。
词法环境有两种类型:
- 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是null。它拥有内建的Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如window对象)还有任何用户定义的全局变量,并且this的值指向全局对象。
- 在函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。
环境记录器也有两种类型(如上!):
- 声明式环境记录器存储变量、函数和参数。
- 对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。
简而言之
- 在全局环境中,环境记录器是对象环境记录器。
- 在函数环境中,环境记录器是声明式环境记录器。
注意
对于函数环境,声明式环境记录器还包含了一个传递给函数的arguments对象(此对象存储索引和参数的映射)和传递给函数的参数的length。
抽象地讲,词法环境在伪代码中看起来像这样:
GlobalExectionContext={ LexicalEnvironment:{ EnvironmentRecord:{ Type:"Object", //在这里绑定标识符 } outer:} } FunctionExectionContext={ LexicalEnvironment:{ EnvironmentRecord:{ Type:"Declarative", //在这里绑定标识符 } outer: } }
变量环境:
它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。
在ES6中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let和const)绑定,而后者只用来存储var变量绑定。
我们看点样例代码来理解上面的概念:
leta=20;constb=30;varc; functionmultiply(e,f){varg=20;returne*f*g;} c=multiply(20,30);
执行上下文看起来像这样:
GlobalExectionContext={ ThisBinding:, LexicalEnvironment:{ EnvironmentRecord:{ Type:"Object", //在这里绑定标识符 a: , b: , multiply: } outer: }, VariableEnvironment:{ EnvironmentRecord:{ Type:"Object", //在这里绑定标识符 c:undefined, } outer: } } FunctionExectionContext={ ThisBinding: , LexicalEnvironment:{ EnvironmentRecord:{ Type:"Declarative", //在这里绑定标识符 Arguments:{0:20,1:30,length:2}, }, outer: }, VariableEnvironment:{ EnvironmentRecord:{ Type:"Declarative", //在这里绑定标识符 g:undefined }, outer: } }
注意
只有遇到调用函数multiply时,函数执行上下文才会被创建。
可能你已经注意到let和const定义的变量并没有关联任何值,但var定义的变量被设成了undefined。
这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为undefined(var情况下),或者未初始化(let和const情况下)。
这就是为什么你可以在声明之前访问var定义的变量(虽然是undefined),但是在声明之前访问let和const的变量会得到一个引用错误。
这就是我们说的变量声明提升。
执行阶段
这是整篇文章中最简单的部分。在此阶段,完成对所有这些变量的分配,最后执行代码。
注意
在执行阶段,如果JavaScript引擎不能在源码中声明的实际位置找到let变量的值,它会被赋值为undefined。
结论
我们已经讨论过JavaScript程序内部是如何执行的。虽然要成为一名卓越的JavaScript开发者并不需要学会全部这些概念,但是如果对上面概念能有不错的理解将有助于你更轻松,更深入地理解其他概念,如变量声明提升,作用域和闭包。
参考文章:
https://juejin.cn/post/6844903682283143181
https://www.jianshu.com/p/6f8556b10379
https://juejin.cn/post/6844903704466833421
到此这篇关于JavaScript中的执行上下文和执行栈实例讲解的文章就介绍到这了,更多相关JavaScript中的执行上下文和执行栈内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。