当前位置: 首页 > Web前端 > HTML

【JS基础系列】吃透执行上下文和调用栈(下)

时间:2023-04-02 15:30:52 HTML

今天是系列的第七篇,有点晚了。上一篇讲了执行上下文和调用栈的一些知识点。但是后来发现还有一个blockscope的知识点没有提到,所以打算在这篇文章中补充这块的内容。另外,第六篇后面其实还有一些我到现在还不太明白的地方。和朋友商量后,终于得出结论。如有错误,请指正。让我们从块作用域开始。块作用域首先我们必须有一个疑问。我们的全局作用域和函数作用域都用的很好,为什么突然引入块作用域?这一切都得从变量提升这个知识点说起。如果对变量提升有疑惑,可以回去再看第六篇。在变量提升中经常会遇到这样的情况:varname="familyli"functionrun(){console.log(name)if(true){varname='ming'}}第一个问题是值被覆盖的问题.很显然,我们的初衷应该是想让这里打印的值为familyli,但是这里打印出来的结果是变量提升导致的undefined,显得十分奇怪和费解。再来看第二个问题,就是应该销毁的值没有销毁。for(vari=0;i<5;i++>){}console.log(i)这段代码的结果是5,我们本来想达到的效果是循环结束后i的值应该被销毁执行了,但实际情况是因为变量提升,i被提升为全局,循环执行完后保留。由于变量提升导致的上述问题,我们在写代码的时候经常会因此而烦恼。所以es6引入了关键字let和const来声明变量,并且引入了块作用域的概念。该范围解决了哪些问题?解决变量提升(不能进行变量提升)、声明变量前不能获取和操作指针(临时死区)、值覆盖问题(同一个块作用域内不能重复声明)。那我就举个例子来说说block作用域的整个执行过程。letname='familyli'functionrun(){letname='ming'console.log(name)}run()利用执行栈的知识来解释这个block的执行过程1.创建全局执行上下文,并调用栈,将执行上下文压入调用栈底部。进入编译阶段,编译完成后,全局执行上下文中的变量环境有run=function(){...}(变量提升的变量),词法环境有name=undefined(变量预编译期间创建的赋值位未定义)。2.在全局上下文中输入可执行代码。词法环境中的变量名被赋值为familyli,然后代码向下执行到run()3,创建函数运行执行上下文,并将执行上下文压入调用栈顶部。进入编译阶段,编译完成后,函数执行上下文中的变量环境为空,词法环境有name=undefined。4.输入函数运行的执行上下文的可执行代码。词法环境中的变量名被赋值为familyli,然后代码继续到console.log(name)。5、先在当前执行上下文中查找变量名,发现在词法环境中找到了,所以输出值ming。6.从调用栈中弹出函数运行的执行上下文,并销毁其执行上下文。7、代码向下执行,全局执行上下文保存在调用栈中。除非进程被主动杀死,否则全局执行上下文将一直存在于调用栈中。好了,回到最开始的值覆盖问题。如果在letname='ming'之前插入console.log(name),会不会因为变量提升而输出undefined?不行,会出现暂时死区,代码会报错。看下面的代码letname='familyli'{letname='ming'console.log(name)}console.log(name)上面代码的输出值为ming和familyli,不会相同之前因为变量提升的原因是为了覆盖全局中的变量。其实这里的名字是两个不同的变量。因为name='ming'在花括号里面,形成了一个块作用域,name的值离开花括号后就被销毁了,所以不会污染全局变量。嗯,说完blockscope,问了几个上篇没看懂的问题。遗留的几个问题1.执行上下文和作用域的区别,作用域的赋值过程是怎样的?js的作用域是词法作用域,在写代码结构的时候就确定了。函数执行阶段查找变量的过程就是通过作用域链查找变量,然后输出值。作用域链上的这个值是如何确定的?个人猜想:作用域链中只保存了变量之间的位置关系,并没有保存变量的值。变量的值存储在编译后的变量环境中。在代码执行阶段寻找变量的值时,首先确定变量在链上作用域链中的位置,然后利用这个位置关系对应执行上下文。变量环境中变量的值。所以:作用域在写代码的时候就确定了,但是在编译阶段就创建好了。因为如果不执行代码就理解作用域的话,变量访问的作用域其实是没有意义的。所以一切都说得通。编译后,每个执行上下文中都有一个变量outer,指向其上层作用域,从而形成一个作用域链。在执行打印值的代码时,通过作用域链找到变量的位置,然后在执行上下文中找到对应变量的值。2.blockscope的临时死区问题{letname='familyli'//[1]letage=20}我在位置1下了断点,此时的初始化值为name=undefiend,age=未定义;但是如果我在位置1后面加上console.log(age),那么位置1的断点初始化显示就是name=undefined,age就没有了。这是一个暂时的死区导致无法在词法环境中创建变量吗?这道题需要知道blockscope会有一个预编译的过程,blockscope中的所有变量在变量初始化之前都会存储在declareData中。然后发现console.log(age)中的age已经声明了,但是还没有初始化,会报错。另外,我对let(块作用域声明)、var(普通声明)和函数声明的变量提升的理解是:1、var的创建和初始化被提升,赋值不会被提升。2、let的创建被提升,初始化和赋值不会被提升。3、函数的创建、初始化、赋值都会被提升。3、下面代码的执行顺序vara=0;if(true){a=1;函数a(){};一=21;console.log(a)//21}console.log(a)//1分析:块作用域中存在函数提升(块作用域是预解析的),但不存在变量提升。而这里面的函数声明其实也可以看成是声明了一个变量,指向了函数体。所以这里的函数声明被提升到代码的开头,函数定义被提升到块作用域的前面。而这个变量属于函数级变量(我个人认为这里指的是全局作用域),所以当定义块级函数时,该变量会被同步到函数级作用域,可以被全局变量。所以实际上他的预解析是这样的:vara=0;if(true){console.log(a,window.a);//函数提升,块级作用域,输出函数aand0a=1;//取最近的块级作用域的函数a并重置为1,这本质上是另一个变量赋值。console.log(a,window.a);//a是指向块级作用域的a,输出1和0functiona(){}//函数声明,将执行函数的变量定义同步到功能级范围。console.log(a,window.a);//输出1和1a=21;//仍然是函数定义块级作用域中的a,重置为21console.log(a,window.a);//输出的是函数提升的块级作用域的a,输出21,1console.log("里面",a);}console.log("外面",a);stackoverflow解决方法(还没想好)之前看到递归调用经常容易出现stackoverflow。我看到的一些常用的解决方案是(1)使用事件循环操作函数而不是调用堆栈操作函数(2)循环调用函数(3)尾递归解决递归函数(原理是:函数调用会产生“调用records(存放函数的信息)”存放在栈中,当一个函数返回时,对应的调用记录就会消失;上面递归的普通函数并没有返回,所以调用记录会越来越多,导致栈溢出。尾递归是在函数中加入了返回函数本身的操作,使得调用记录在当前函数执行完后被删除,这样继续递归不会造成内存溢出。)示例见下面的代码。但是当我断点的时候,在review元素中看到很多ruunStack函数还是会在CallStack中被创建。不知道是不是验证的方式不对(小弟能力有限,一直没搞清楚问题,有知道的希望在这里指正,在下方评论区留言)//初始函数//functionrunStack(n){//if(n===0){//返回100//}//console.log(n)//runStack(n-10)//}//runStack(1000)//尾递归//functionrunStack(n){//if(n===0){//return100//}//console.log(n)//returnrunStack(n-10)//}//runStack(1000)//循环方式//functionrunStack(n){//while(n>0){//runStack(n-10)//}//console.log(n)//if(n===0){//return100//}//}//runStack(1000)//事件循环函数runStack(n){if(n===0){return100}console.log(n)setTimeout(runStack,0,n-10)}runStack(1000)总结因为在变量提升中有值覆盖和应该销毁的值,不存在销毁的问题,所以block作用域是介绍。全局作用域中的变量会被提升到全局作用域的最顶层,函数中声明的变量只会被提升到函数作用域的最顶层。块作用域不能重复声明,有一个暂时的死区,没有变量提升。参考文章https://mp.weixin.qq.com/s?__...