浏览器工作原理与实践--调用栈:为什么JavaScript代码会出现栈溢出

news2024/12/28 18:18:24

在上篇文章中,我们讲到了,当一段代码被执行时,JavaScript引擎先会对其进行编译,并创建执行上下文。但是并没有明确说明到底什么样的代码才算符合规范。

那么接下来我们就来明确下,哪些情况下代码才算是“一段”代码,才会在执行之前就进行编译并创建执行上下文。一般说来,有这么三种情况:

  1. 当JavaScript执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。

  2. 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。

  3. 当使用eval函数的时候,eval的代码也会被编译,并创建执行上下文。

好了,又进一步理解了执行上下文,那本节我们就在这基础之上继续深入,一起聊聊调用栈。学习调用栈至少有以下三点好处:

  1. 可以帮助你了解JavaScript引擎背后的工作原理;

  2. 让你有调试JavaScript代码的能力;

  3. 帮助你搞定面试,因为面试过程中,调用栈也是出境率非常高的题目。

比如你在写JavaScript代码的时候,有时候可能会遇到栈溢出的错误,如下图所示:

栈溢出的错误

那为什么会出现这种错误呢?这就涉及到了调用栈的内容。你应该知道JavaScript中有很多函数,经常会出现在一个函数中调用另外一个函数的情况,调用栈就是用来管理函数调用关系的一种数据结构。因此要讲清楚调用栈,你还要先弄明白函数调用和栈结构。

什么是函数调用

函数调用就是运行一个函数,具体使用方式是使用函数名称跟着一对小括号。下面我们看个简单的示例代码:

var a = 2
function add(){
var b = 10
return  a+b
}
add()

这段代码很简单,先是创建了一个add函数,接着在代码的最下面又调用了该函数。

那么下面我们就利用这段简单的代码来解释下函数调用的过程。

在执行到函数add()之前,JavaScript引擎会为上面这段代码创建全局执行上下文,包含了声明的函数和变量,你可以参考下图:

全局执行上下文

从图中可以看出,代码中全局变量和函数都保存在全局上下文的变量环境中。

执行上下文准备好之后,便开始执行全局代码,当执行到add这儿时,JavaScript判断这是一个函数调用,那么将执行以下操作:

  • 首先,从全局执行上下文中,取出add函数代码。

  • 其次,对add函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码。

  • 最后,执行代码,输出结果。

完整流程你可以参考下图:

函数调用过程

就这样,当执行到add函数的时候,我们就有了两个执行上下文了——全局执行上下文和add函数的执行上下文。

也就是说在执行JavaScript时,可能会存在多个执行上下文,那么JavaScript引擎是如何管理这些执行上下文的呢?

答案是通过一种叫栈的数据结构来管理的。那什么是栈呢?它又是如何管理这些执行上下文呢?

什么是栈

关于栈,你可以结合这么一个贴切的例子来理解,一条单车道的单行线,一端被堵住了,而另一端入口处没有任何提示信息,堵住之后就只能后进去的车子先出来,这时这个堵住的单行线就可以被看作是一个栈容器,车子开进单行线的操作叫做入栈,车子倒出去的操作叫做出栈。

在车流量较大的场景中,就会发生反复的入栈、栈满、出栈、空栈和再次入栈,一直循环。

所以,栈就是类似于一端被堵住的单行线,车子类似于栈中的元素,栈中的元素满足后进先出的特点。你可以参看下图:

栈示意图

什么是JavaScript的调用栈

JavaScript引擎正是利用栈的这种结构来管理执行上下文的。在执行上下文创建好后,JavaScript引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。

为便于你更好地理解调用栈,下面我们再来看段稍微复杂点的示例代码:

var a = 2
function add(b,c){
  return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return  a+result+d
}
addAll(3,6)

在上面这段代码中,你可以看到它是在addAll函数中调用了add函数,那在整个代码的执行过程中,调用栈是怎么变化的呢?

下面我们就一步步地分析在代码的执行过程中,调用栈的状态变化情况。

第一步,创建全局上下文,并将其压入栈底。如下图所示:

全局执行上下文压栈

从图中你也可以看出,变量a、函数add和addAll都保存到了全局上下文的变量环境对象中。

全局执行上下文压入到调用栈后,JavaScript引擎便开始执行全局代码了。首先会执行a=2的赋值操作,执行该语句会将全局上下文变量环境中a的值设置为2。设置后的全局上下文的状态如下图所示:

赋值操作改变执行上下文中的值

接下来,第二步是调用addAll函数。当调用该函数时,JavaScript引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中,如下图所示:

执行addAll函数时的调用栈

addAll函数的执行上下文创建好之后,便进入了函数代码的执行阶段了,这里先执行的是d=10的赋值操作,执行语句会将addAll函数执行上下文中的d由undefined变成了10。

然后接着往下执行,第三步,当执行到add函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈,如下图所示:

执行add函数时的调用栈

当add函数返回时,该函数的执行上下文就会从栈顶弹出,并将result的值设置为add函数的返回值,也就是9。如下图所示:

add函数执行结束时的调用栈

紧接着addAll执行最后一个相加操作后并返回,addAll的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。最终如下图所示:

addAll函数执行结束时的调用栈

至此,整个JavaScript流程执行结束了。

好了,现在你应该知道了调用栈是JavaScript引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。

在开发中,如何利用好调用栈

鉴于调用栈的重要性和实用性,那么接下来我们就一起来看看在实际工作中,应该如何查看和利用好调用栈。

1. 如何利用浏览器查看调用栈的信息

当你执行一段复杂的代码时,你可能很难从代码文件中分析其调用关系,这时候你可以在你想要查看的函数中加入断点,然后当执行到该函数时,就可以查看该函数的调用栈了。

这么说可能有点抽象,这里我们拿上面的那段代码做个演示,你可以打开“开发者工具”,点击“Source”标签,选择JavaScript代码的页面,然后在第3行加上断点,并刷新页面。你可以看到执行到add函数时,执行流程就暂停了,这时可以通过右边“call stack”来查看当前的调用栈的情况,如下图:

查看函数调用关系

从图中可以看出,右边的“call stack”下面显示出来了函数的调用关系:栈的最底部是anonymous,也就是全局的函数入口;中间是addAll函数;顶部是add函数。这就清晰地反映了函数的调用关系,所以在分析复杂结构代码,或者检查Bug时,调用栈都是非常有用的。

除了通过断点来查看调用栈,你还可以使用console.trace()来输出当前的函数调用关系,比如在示例代码中的add函数里面加上了console.trace(),你就可以看到控制台输出的结果,如下图:

使用trace函数输出当前调用栈信息

2. 栈溢出(Stack Overflow)

现在你知道了调用栈是一种用来管理执行上下文的数据结构,符合后进先出的规则。不过还有一点你要注意,调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript引擎就会报错,我们把这种错误叫做栈溢出。

特别是在你写递归代码的时候,就很容易出现栈溢出的情况。比如下面这段代码:

function division(a,b){
    return division(a,b)
}
console.log(division(1,2))

当执行时,就会抛出栈溢出错误,如下图:

栈溢出错误

从上图你可以看到,抛出的错误信息为:超过了最大栈调用大小(Maximum call stack size exceeded)。

那为什么会出现这个问题呢?这是因为当JavaScript引擎开始执行这段代码时,它首先调用函数division,并创建执行上下文,压入栈中;然而,这个函数是递归的,并且没有任何终止条件,所以它会一直创建新的函数执行上下文,并反复将其压入栈中,但栈是有容量限制的,超过最大数量后就会出现栈溢出的错误。

理解了栈溢出原因后,你就可以使用一些方法来避免或者解决栈溢出的问题,比如把递归调用的形式改造成其他形式,或者使用加入定时器的方法来把当前任务拆分为其他很多小任务。

总结

好了,今天的内容就讲到这里,下面来总结下今天的内容。

  • 每调用一个函数,JavaScript引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后JavaScript引擎开始执行函数代码。

  • 如果在一个函数A中调用了另外一个函数B,那么JavaScript引擎会为B函数创建执行上下文,并将B函数的执行上下文压入栈顶。

  • 当前函数执行完毕后,JavaScript引擎会将该函数的执行上下文弹出栈。

  • 当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。

栈是一种非常重要的数据结构,不光应用在JavaScript语言中,其他的编程语言,如C/C++、Java、Python等语言,在执行过程中也都使用了栈来管理函数之间的调用关系。所以栈是非常基础且重要的知识点,你必须得掌握。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1550887.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TheMoon 恶意软件短时间感染 6,000 台华硕路由器以获取代理服务

文章目录 针对华硕路由器Faceless代理服务预防措施 一种名为"TheMoon"的新变种恶意软件僵尸网络已经被发现正在侵入全球88个国家数千台过时的小型办公室与家庭办公室(SOHO)路由器以及物联网设备。 "TheMoon"与“Faceless”代理服务有关联,该服务…

46秒AI生成真人视频爆火,遭在线打假「换口型、声音」

ChatGPT狂飙160天,世界已经不是之前的样子。 新建了人工智能中文站 每天给大家更新可用的国内可用chatGPT资源 更多资源欢迎关注 是炒作还是真正的 AI 视频能力进化? AI 生成视频已经发展到这个程度了吗? 前段时间,英国王室凯特…

每天能提醒自己做事的app有哪个?

在忙碌的日常生活和工作中,我们时常面临各种任务和琐事。一旦处理不及时,很容易导致遗忘,进而给自己带来不必要的麻烦和损失。因此,拥有一款能够高效提醒我们做事的提醒app显得尤为重要。 敬业签就是这样一款实用的提醒软件。它不…

零基础10 天入门 Web3之第1天

10 天入门 Web3 Web3 是互联网的下一代,它将使人们拥有自己的数据并控制自己的在线体验。Web3 基于区块链技术,该技术为安全、透明和可信的交易提供支持。我准备做一个 10 天的学习计划,可帮助大家入门 Web3: 想要一起探讨学习的…

【氮化镓】位错对氮化镓(GaN)电子能量损失谱(EEL)的影响

本文献《Influence of dislocations on electron energy-loss spectra in gallium nitride》由C. J. Fall等人撰写,发表于2002年。研究团队通过第一性原理计算,探讨了位错对氮化镓(GaN)电子能量损失谱(EEL)…

python——修改注册表

如图:想要修改Public的值为2.2.1.1 import winreg# 定义要修改的键和新值 key_path rSOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList key_name Public new_name 2.2.1.1# 打开指定的键 # 注意此处是注册表路径拼接,此处是HKEY_LOCAL_MACH…

docker部署ubuntu

仓库: https://hub.docker.com/search?qUbuntu 拉一个Ubuntu镜像 docker pull ubuntu:18.04 查看本地镜像: docker images 运行容器 docker run -itd --name ubuntu-18-001 ubuntu:18.04 通过ps命令可以查看正在运行的容器信息 docker ps 进入容器 最…

Obsidian插件:增加目录栏 flating toc

一、插件介绍 增加目录栏 插件市场搜索 flating toc安装即可 二、使用 写文档时候可以看到左边默认出现目录 可以自己配置一些相关设置 最后也可以安装一下插件样式设置插件,自己按照自己喜好调整

leetcode —— 5.最长回文子串

题目: 给你一个字符串 s,找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。 示例 1: 输入:s "babad" 输出:"bab" 解释:"…

基础数据结构(蓝桥杯Python组)

链表 基础概念 链表可以快速插入、删除元素,用于存储数据。 每个节点维护两个部分:数据域和指针域数据域就是节点所存储的数据,指针其实就是下标(next,节点的下一个节点在哪)链表维护head和tail,每次从ta…

学浪视频提取

经过调查,学浪这个学习平台越来越多人使用了,但是学浪视频官方没有提供下载按钮,为了让这些人能够随时随地的观看视频,于是我钻研学浪视频的下载,终于研究出来了并且做成软件批量版 下面是学浪视频提取的软件,有需要的自己下载一下 链接:https://pan.baidu.com/s/…

由浅到深认识Java语言(36):I/O流

该文章Github地址:https://github.com/AntonyCheng/java-notes 在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址:https://blog.c…

C++中 eigen(一)建造向量

快速上手: https://eigen.tuxfamily.org/dox/group__QuickRefPage.html 文档: https://eigen.tuxfamily.org/dox/index.html Constructors 建造 向量 Vectors 向量 例如,方便的 typedef Vector3f 是一个包含 3 个浮点数的(列&a…

Spring用到了哪些设计模式?

目录 Spring 框架中⽤到了哪些设计模式?工厂模式单例模式1.饿汉式,线程安全2.懒汉式,线程不安全3.懒汉式,线程安全4.双重检查锁(DCL, 即 double-checked locking)5.静态内部类6.枚举单例 代理模…

15K star!一款功能强悍的手机电脑同屏工具,开源无需root!

在日常工作、生活场景中,经常会遇到需将手机与电脑屏幕进行共享。 今天就给大家推荐一款Android实时投屏神器:QtScrcpy。 它可以通过 USB / 网络连接Android设备,并进行显示和控制,且无需root权限。 1、简介 QtScrcpy是一款功…

nginx代理解决跨域问题

文章目录 一、什么是跨域、跨域问题产生的原因二、注意事项三、nginx代理解决总结 一、什么是跨域、跨域问题产生的原因 跨域(Cross-Origin)是指在 Web 开发中,一个网页的运行脚本试图访问另一个网页的资源时,这两个网页的域名、…

微信开发者工具接入短剧播放器插件

接入短剧播放插线 申请添加插件基础接入app.jsonapp.jsplayerManager.js数据加密跳转到播放器页面运行出错示例小程序页面页面使用的方法小程序输入框绑定申请添加插件 添加插件:登录微信开发者平台 ——> 设置 ——> 第三方设置 ——> 插件管理 ——> 搜索“短剧…

springboot多模块

这里springboot使用idea中的 Spring Initializr 来快速创建。 一、demo 1、创建父项目 首先使用 Spring Initializr 来快速创建好一个父Maven工程。然后删除无关的文件,只需保留pom.xml 文件。 (1)new Project -> spring initializr快…

FPGA时钟资源详解(1)——时钟Buffer的选择

FPGA时钟系列文章总览:FPGA原理与结构(14)——时钟资源https://ztzhang.blog.csdn.net/article/details/132307564 目录 一、概述 二、时钟Buffer的选择 2.1 BUFG 2.2 BUFR 和 BUFIO 2.2.1 源同步接口的支持 2.2.2 扩展时钟域…

StringRedisTemplate与RedisTemplate详解【序列化的方式不同】

spring 封装了 RedisTemplate 对象来进行对redis的各种操作&#xff0c;它支持所有的 redis 原生的 api。在RedisTemplate中提供了几个常用的接口方法的使用&#xff0c;分别是: private ValueOperations<K, V> valueOps; private HashOperations<K, V> hashOps; …