读李老课程引发的思考之JS从栈、堆、预解析来解释闭包原理-|真 · 奥义|

本文通过分析JavaScript中栈、堆的管理以及预解析、惰性解析的过程,深入探讨了闭包的原理和特性。通过实例解释了为何某些代码会导致栈溢出、页面卡死,以及如何处理闭包带来的问题。重点讨论了预解析器在处理闭包引用外部变量时的角色,揭示了闭包并不完全依赖内部函数是否被return出去的观点。
摘要由CSDN通过智能技术生成

1.下面三段代码会执行结果什么不同

function foo() {
  foo() // 是否存在堆栈溢出错误?
}
foo()
function foo() {
setTimeout(foo, 0) // 是否存在堆栈溢出错误?
}
function foo() {
return Promise.resolve().then(foo)
}
foo()

A:

  • 第一段:V8就会报告 栈溢出的错误
  • 第二段:正确执⾏
  • 第三段:没有栈溢出的错误,却会造成⻚⾯的卡死

2.为什么第一段会栈溢出

由于foo函数内部嵌套调⽤它⾃⼰,所以在调⽤foo函数的时候,它的栈会⼀直向上增⻓,但是由于栈空间在
内存中是连续的,所以通常我们都会限制调⽤栈的⼤⼩,如果当函数嵌套层数过深时,过多的执⾏上下⽂堆
积在栈中便会导致栈溢出,最终如下图所⽰:

3.为什么第二段会正常

setTimeout的本质是将同步函数调⽤改成异步函数调⽤,
这⾥的异步调⽤是将foo封装成事件,并将其添加进 消息队列中,然后主线程再按照⼀定规则循环地从消息队列中读取下⼀个任务。

⾸先,主线程会从消息队列中取出需要执⾏的宏任务,假设当前取出的任务就是要执⾏的这段代码,这时候
主线程便会进⼊代码的执⾏状态。这时关于主线程、消息队列、调⽤栈的关系如下图所⽰

接下来V8就要执⾏foo函数了,同样执⾏foo函数时,会创建foo函数的执⾏上下⽂,并将其压⼊栈中,最终
效果如下图所⽰:

当V8执⾏执⾏foo函数中的setTimeout时,setTimeout会将foo函数封装成⼀个新的宏任务,并将其添加到
消息队列中,在V8执⾏setTimeout函数时的状态图如下所⽰:

等foo函数执⾏结束,V8就会结束当前的宏任务,调⽤栈也会被清空,调⽤栈被清空后状态如下图所⽰


当⼀个宏任务执⾏结束之后,忙碌的主线程依然不会闲下来,它会⼀直重复这个取宏任务、执⾏宏任务的过
程。刚才通过setTimeout封装的回调宏任务,也会在某⼀时刻被主线取出并执⾏,这个执⾏过程,就是foo
函数的调⽤过程。具体⽰意图如下所⽰:

因为foo函数并不是在当前的⽗函数内部被执⾏的,⽽是封装成了宏任务,并丢进了消息队列中,然后等待
主线程从消息队列中取出该任务,再执⾏该回调函数foo,这样就解决了栈溢出的问题。

4.为什么第三段会卡住页面

理解微任务的执⾏时机,你只需要记住以下两点:

  • ⾸先,如果当前的任务中产⽣了⼀个微任务,通过Promise.resolve()或者Promise.reject()都会触发微任务,触发的微任务不会在当前的函数中被执⾏,所以执⾏微任务时,不会导致栈的⽆限扩张;
  • 其次,和异步调⽤不同,微任务依然会在当前任务执⾏结束之前被执⾏,这也就意味着在当前微任务执⾏结束之前,消息队列中的其他任务是不可能被执⾏的

因此在函数内部触发的微任务,⼀定⽐在函数内部触发的宏任务要优先执⾏。

当执⾏foo函数时,由于foo函数中调⽤了Promise.resolve(),这会触发⼀个微任务,那么此时,V8会将该微任务添加进微任务队列中,退出当前foo函数的执⾏。

然后,V8在准备退出当前的宏任务之前,会检查微任务队列,发现微任务队列中有⼀个微任务,于是先执⾏微任务。由于这个微任务就是调⽤foo函数本⾝,所以在执⾏微任务的过程中,需要继续调⽤foo函数,在执⾏foo函数的过程中,⼜会触发了同样的微任务。

那么这个循环就会⼀直持续下去,当前的宏任务⽆法退出,也就意味着消息队列中其他的宏任务是⽆法被执⾏的,⽐如通过⿏标、键盘所产⽣的事件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值