当前位置: 首页 > 后端技术 > Node.js

深入理解JavaScript执行上下文和执行栈

时间:2023-04-04 00:39:25 Node.js

前言如果你是一名JavaScript开发者,或者想成为一名JavaScript开发者,那么你必须了解JavaScript程序的内部执行机制。执行上下文和执行栈是JavaScript中的关键概念之一,也是JavaScript中的难点之一。了解执行上下文和执行堆栈还有助于理解其他JavaScript概念,例如提升、作用域和闭包。本文试图以通俗易懂的方式介绍这些概念。想阅读更多优质文章,请戳GitHub博客1.执行上下文(ExecutionContext)1.什么是执行上下文?简而言之,执行上下文是当前JavaScript代码解析和执行环境的抽象概念。任何代码都在执行上下文中运行2.执行上下文的类型执行上下文分为三种类型:全局执行上下文:这是默认的和最基本的执行上下文。不在任何函数中的代码在全局执行上下文中。它做了两件事:1.创建一个全局对象,也就是浏览器中的window对象。2.将this指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都有自己的执行上下文,但它仅在函数被调用时创建。程序中可以存在任意数量的函数执行上下文。每当创建新的执行上下文时,它都会按特定顺序执行一系列步骤,这将在本文后面讨论。eval函数执行上下文:在eval函数中运行的代码也会得到自己的执行上下文,但是由于eval函数不是Javascript开发者常用的,这里就不展开讨论了。2.执行上下文的生命周期执行上下文的生命周期包括三个阶段:创建阶段→执行阶段→回收阶段。本文重点介绍创建阶段。1.在创建阶段,当函数被调用时,但在任何内部代码执行之前,会做以下三件事:创建变量对象:首先初始化函数的形参参数,促进函数声明和变量声明.下面将对其进行详细描述。创建作用域链(ScopeChain):在执行上下文的创建阶段,在变量对象之后创建作用域链。作用域链本身包含变量对象。作用域链用于解析变量。当要求解析一个变量时,JavaScript总是从代码嵌套的最内层开始。如果在最内层没有找到该变量,它将跳转到上一个父作用域,直到找到该变量。确定这个的方向:包括多种情况,下面会详细说明,在执行一个JS脚本之前,首先要解析代码(所以JS是解释和执行的脚本语言)。解析时,首先会创建一个全局执行上下文。代码中要执行的变量和函数声明都取出来了。先把变量临时赋值给undefined,函数先声明,准备使用。这一步做完了,接下来开始正式执行程序。另外,一个函数在执行之前,也会创建一个函数执行上下文,这个和全局上下文类似,只是函数执行上下文中会多一些this参数和函数形参。2.执行阶段执行变量赋值和代码执行3.回收阶段执行上下文出栈,等待虚拟机回收执行上下文3.变量提升和this指向的细节1.变量声明提升大多数编程语言在使用变量之前声明变量。但是在JS中,情况有点不同:console.log(a)//undefinedvara=10上面的代码正常输出undefined,而不是报UncaughtReferenceError:aisnotdefined,这是因为提升,相当于以下代码:vara;//Declaredefaultvalueisundefined"Preparation"console.log(a);a=10;//Assignment2.函数声明提升我们都知道创建函数有两种方式,一种是通过函数声明functionfoo(){},另一种是使用函数表达式varfoo=function(){},那么两者在功能提升上有什么区别呢?console.log(f1)//functionf1(){}functionf1(){}//函数声明console.log(f2)//undefinedvarf2=function(){}//函数表达式接下来我们传递一个Example给说明问题:functiontest(){foo();//未捕获的类型错误“foo不是一个函数”bar();//“这将运行!”varfoo=function(){//分配给局部变量'foo'的函数表达式alert("thiswon'trun!");}functionbar(){//函数声明,给定名称'bar'alert("thiswillrun!");}}测试();上面的例子,调用foo()时报错,但是bar可以正常调用。我们之前说过,变量和函数都会被引发。当遇到函数表达式varfoo=function(){}时,varfoo会先被提升到函数体的顶部。但是此时foo的值是undefined,所以执行foo()报错。对于函数bar(),改进了整个函数,可以顺利执行bar()。有一个细节必须注意:当一个函数和一个变量同名并被提升时,函数声明的优先级更高,因此变量声明会被函数声明覆盖,但可以重新赋值.alert(a);//输出:functiona(){alert('我是一个函数')}functiona(){alert('我是一个函数')}//vara='我是一个变量';警报(一);//Output:'Iamavariable'函数声明的优先级高于var声明,也就是说当两个同名变量同时被function和var声明时,函数声明会覆盖var宣言。此代码等效于:functiona(){alert('Iamafunction')}vara;//提升警报(a);//输出:functiona(){alert('Iamafunction')}a='Iamavariable';//赋值alert(a);//Output:'Iamavariable'最后,我们来看一个更复杂的例子:functiontest(arg){//1.形参arg为"hi"//2.因为函数声明的优先级高于变量声明,所以arg是functionconsole.log(arg);vararg='你好';//3.vararg变量声明被忽略,arg='hello'被执行functionarg(){console.log('helloworld')}console.log(arg);}测试('嗨');/*Output:functionarg(){console.log('helloworld')}hello*/这是因为函数执行时,会先形成一个新的私有作用域,然后按照以下步骤执行:如果有形参的,先在私有作用域给形参赋值进行预解释,函数声明的优先级高于变量。声明高,最后后者会被前者覆盖,但是private范围内的代码可以重新赋值,从上到下执行3.确定这个的方向,首先要明白一个很重要的概念——this的值在执行时只能在定义的时候才能确定,定义的时候不能确定!为什么-因为这是执行上下文的一部分,执行上下文需要在代码执行之前确定,而不是在定义时确定。看下面的例子://情况1functionfoo(){console.log(this.a)//1}vara=1foo()//情况2functionfn(){console.log(this);}varobj={fn:fn};obj.fn();//this->obj//情况3functionCreateJsPerson(name,age){//this是当前类的一个实例p1this.name=name;//=>p1.name=namethis.age=age;//=>p1.age=age}varp1=newCreateJsPerson("尹华志",48);//情况4functionadd(c,d){returnthis.a+this.b+c+d;}varo={a:1,b:3};add.call(o,5,7);//1+3+5+7=16add.apply(o,[10,20]);//1+3+10+20=34//情况5箭头函数this接下来我们对以上几种情况一一进行说明。直接调用foo,不管foo函数放在什么地方,this都必须是window。对于obj.foo(),我们只需要记住,调用函数的就是this,所以这个场景下foo函数中的this就是是obj对象在构造函数模式下,类中(函数体中)出现的this.xxx=xxx是当前类调用的实例,apply和bind:this是第一个参数this指向的箭头函数:arrow函数没有自己的this,查看外层有没有函数,如果有,外层函数的this就是内层箭头函数的this,如果没有,那么this就是window4、执行上下文栈(ExecutionContextStack)函数太多了,有多个函数执行上下文,每次调用一个函数都会创建一个新的执行上下文,那么创建的这么多执行上下文如何管理呢?JavaScript引擎创建执行上下文堆栈来管理执行上下文。执行上下文栈可以看做是一个存储函数调用的栈结构,遵循先进后出的原则。从上面的流程图中,我们需要记住几个关键点:JavaScript是单线程执行的,所有代码都是排队等待执行的。浏览器一开始执行全局代码时,首先会创建一个全局执行上下文,并将其压入执行栈的顶部。每当进入函数的执行时,都会创建函数的执行上下文并将其压入执行堆栈的顶部。当前函数执行完成后,将当前函数的执行上下文弹出栈,等待垃圾回收。浏览器的JS执行引擎总是访问堆栈顶部的执行上下文。只有一个全局上下文,当浏览器关闭时,它会从堆栈中弹出。让我们看另一个例子:varcolor='blue';函数changeColor(){varanotherColor='red';函数swapColors(){vartempColor=anotherColor;另一种颜色=颜色;颜色=临时颜色;}swapColors();}changeColor();上述代码的运行步骤如下:当浏览器加载上述代码时,JavaScript引擎会创建一个全局执行上下文,并将其压入当前执行栈,以调用changeColor函数。此时,changeColor函数的内部代码还没有执行,js执行引擎立即创建一个changeColor执行上下文(简称EC),然后将执行上下文压入执行栈(简称ECStack)。在执行changeColor函数期间,将调用swapColors函数。同样,在执行swapColors函数之前,会创建swapColors的执行上下文并将其压入执行堆栈。swapColors函数执行完成后,将swapColors函数的执行上下文从栈中弹出并销毁。changeColor函数执行完成后,将changeColor函数的执行上下文从栈中弹出并销毁。向大家推荐一款好用的BUG监控工具Fundebug,欢迎免费试用!欢迎关注公众号:前端工匠,让我们一起见证你的成长!如果你觉得有所收获,欢迎打赏我,以鼓励我输出更多优质的开源内容。参考文章理解JavaScript的执行上下文深入理解JavaScript中的作用域和上下文前端基础进阶(二):执行上下文详图深入理解JS中的声明提升、作用域(链)和this关键字