JS栈和堆:数据是如何存储的

news2025/1/13 10:16:38

JS栈和堆:数据是如何存储的

  • 背景
  • JavaScript 是什么类型的语言
  • JavaScript 的数据类型
  • 内存空间
    • 栈空间和堆空间
    • 再谈闭包

背景

JS有多种数据类型:数字型,字符串型,数组型等,虽然 JavaScript 并不需要直接去管理内存,但是实际项目中为了能避开一些不必要的坑,还是需要了解数据在内存中的存储方式。例如下面两段示例代码:

function foo() {
  var a = 1
  var b = a
  a = 2
  console.log(a)
  console.log(b)
}
foo()
// 执行结果:
// 2
// 1
function foo() {
  var a = {name: 'yy'}
  var b = a
  a.name = 'qq'
  console.log(a)
  console.log(b)
}
foo()
// 执行结果:
// {name: 'qq'}
// {name: 'qq'}

第一段代码没什么难以理解的,但是如果你对第二段代码感到迷惑,想要彻底弄清楚这个问题,就需要先从 JavaScript 这种语言说起。

JavaScript 是什么类型的语言

每种编程语言都具有内建的数据类型,但不同语言它们的数据类型常有不同之处,使用方式也很不一样。比如 C 语言在定义变量之前,就需要确定变量的类型,我们将这种称为静态语言。相反地,像 JavaScript 这种在运行过程中需要检查数据类型的语言称为动态语言

虽然 C 语言是静态语言,但是在 C 语言中我们可以把其他类型数据赋予给一个声明好的变量,比如将 int 型变量赋值给 bool 型变量,因为在赋值过程中,C 编译器会把 int 型的变量悄悄转换为 bool 型的变量,通常将这种偷偷转换的操作称为隐式类型转换,而支持隐式类型转换的语言称为弱类型语言,不支持隐式类型转换的语言称为强类型语言。这这点上,C 和 JavaScript 都是弱类型语言。

在这里插入图片描述

JavaScript 的数据类型

现在我们知道,JavaScript 是一种弱类型、动态语言。意味着:

  • 弱类型:你不需要告诉 JavaScript 引擎这个变量是什么数据类型,JavaScript 引擎在运行代码时会自己计算出来
  • 动态:你可以使用同一个变量保存不同类型的数据(变量没有数据类型,值才有数据类型)

JavaScript 中的数据类型一共有 8 种,它们分别是:

类型描述
Booleantruefalse 两个值
Nullnull
Undefined一个没有被赋值的变量会有个默认值 undefined,变量提升时的默认值也是 undefined
Number数字型
BigIntJavaScript 中的任意精度整数,可以安全存储和操作大整数,即使超出 Number 能够表示的安全整数范围。(Number 数据类型大于或等于 2 的 1024 次方的数值 JavaScript 无法表示,会返回 infinity,ES2020 引入一种新的数据类型 BigInt 来解决这个问题)
String字符串
Symbol符号类型是唯一的并且是不可修改的,通常用来作为 Object 的 key
Object在 JavaScript 中,对象可以看作是一组属性的集合

需要注意的是:

  • 使用 typeof 检测 null 时,返回的是 object,这是当初 JavaScript 语言的一个 Bug,为了兼容老的代码所以一直保留至今
  • Object 类型比较特殊,它是由上述 7 种类型组成的一个包含了 key,value键值对的数据类型
  • 我们把前面 7 中数据类型称为原始类型,最后一个对象类型称为引用类型,因为它们在内存中存放的位置不一样。

内存空间

在 JavaScript 执行过程中,只要有三种类型内存空间,分别是代码空间栈空间堆空间。其中的代码空间主要是存储可执行代码。今天主要来介绍栈空间和堆空间。

栈空间和堆空间

这里的栈空间就是在 JS 调用栈 文中反复提及的调用栈,先来看下面这段示例代码:

function foo() {
  var a = 'yy'
  var b = a
  var c = {name: 'qq'}
  var d = c
}
foo()

在 JS 调用栈 这篇文章中讲解过,当执行一段代码时,需要先编译并创建执行上下文,然后再按照顺序执行代码。下图是执行到 b = a 这行代码时其调用栈的状态图,可以参考:

在这里插入图片描述

此时,变量 a 和 变量 b 的值都被保存在执行上下文中,而执行上下文又被压入栈中,所以也可以认为变量 a 和变量 b 的值都是存放在栈中的。

接下来继续执行 c = {name: 'qq'} 这行代码,由于 JavaScript 引擎判断右边的值是一个引用类型,此时 JavaScript 引擎不是直接将该对象存放在变量环境中,而是将它分配到堆空间里,分配后该对象会有一个在“堆”中的地址,然后再将该数据的地址写进 c 的变量值,最终分配好内存的示例图如下:

在这里插入图片描述

从上图可以清晰观察到,对象类型时存放在堆空间的,栈空间只保留了对象的引用地址,当 JavaScript 需要访问该数据时,是通过栈中的引用地址来访问的。

为何一定要分“堆”和“栈”两个存储空间呢?所有数据直接都存放在“栈”中可以么?
不可以。因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了所有数据都存放在栈空间中,会影响到上下文切换的效率,进而影响到整个程序的执行效率。
例如文中的 foo 函数执行结束了,JavaScript 引擎需要离开当前的执行上下文,只需要将指针下移到全局上下文的地址就行了,foo 函数执行上下文栈区空间全部回收。所以,通常情况下,栈空间都不会设置太大。

在 JavaScript 中,赋值操作和其他语言有很大的不同,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。所以 d = c 这行代码的操作就是把 c 的引用地址赋值给了 d,具体可参考下图:

在这里插入图片描述

从图中看出,变量 c 和变量 d 都指向了同一个堆中的对象。这就很好解释了在文章开头的示例代码 2 中,通过 a 修改 name 的值,变量 b 的值也会跟着改变,因为归根到底它们是同一个对象。

再谈闭包

在知道了作用域内的原始数据类型会被存储到栈空间,引用类型会被存储到堆空间,基于这两点的认知,进一步探讨下闭包的内存模型。

关于什么是闭包,可以参考文章 JS作用域链和闭包 这篇文章。

还是以这段代码为例:

function foo() {
  var myname = 'yy'
  let test1 = 1
  const test2 = 2
  var innerbar = {
    getName: function() {
      console.log(test1)
      return myname
    },
    setName: function(newName) {
      myname = newName
    }
  }
  return innerbar
}
var bar = foo()
bar.setName('qq')
bar.getName()
console.log(bar.getName())

由于变量 mynametest1test2 都是原始类型数据,所以在执行 foo 函数时,它们会被压入到调用栈中;当 foo 函数执行结束后,调用栈中的 foo 函数的执行上下文会被销毁,其内部变量 mynametest1test2 也应该一同被销毁。但是根据 JS作用域链和闭包 文中的分析,由于 foo 函数产生了闭包,所以变量 mynametest1 并没有被销毁,而是保存在内存中,现在我们站在内存模型的角度来分析这段代码的执行流程:

  1. 当 JavaScript 引擎执行到 foo 函数时,首先会编译并创建一个空的执行上下文
  2. 编译过程中遇到内部函数 setName,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了 foo 函数中的 myname 变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包,于是在堆空间创建一个"closure(foo)" 对象(这是一个内部对象,JavaScript 无法访问),用来保存 myname 变量
  3. 接着继续扫描到 getName 方法,发现函数内部引用了变量 test1,于是 JavaScript 引擎又将 test1 添加到 "closure(foo)" 对象中
  4. 此时,堆中的 "closure(foo)" 对象中就包含了 mynametest1 两个变量了
  5. 由于 test2 并没有被内部函数引用,所以它依旧保存在调用栈中

通过以上分析,可以画出执行到 foo 函数中 return innerbar 语句时的调用栈状态图如下:

在这里插入图片描述

当执行到 foo 函数时,闭包就产生了;当 foo 函数执行结束之后返回的 getNamesetName 方法都引用了 "closure(foo)" 对象,所以即使 foo 函数退出了,"closure(foo)" 依然被其内部的 setNamegetName 方法引用,所以在下次调用 bar.setNamebar.getName 时,创建的执行上下文中就包含了 "closure(foo)"

总的来说,产生闭包的两个核心步骤:第一步是需要预扫描内部函数,第二步是把内部函数引用的外部变量保存到堆中。

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

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

相关文章

都2024年了,FP卖家还不知道AB站怎么玩?

自从开始写FP独立站各种运营技巧和黑科技的文章,经常都会有朋友来私V,询问怎么进行AB站跳转。 可能是现在平台对于FP商家的限制越来越多,再加上如今到处都是“内卷”的电商环境,让FP商家生存越来越艰难,今天就着重讲一…

美当局批准现货比特币ETF,BTC不涨反跌?解读22页官方文件,SEC的担忧被完全解决了吗?

美东时间2024年1月10日下午,美SEC官宣批准现货比特币ETF的上市和交易,这是一个里程碑时刻,代表着加密资产类别获得主流采用的最重要一步。 11只获得批准的现货比特币ETF分别来自:BlackRock、Bitwise、Grayscale、Hashdex、Valkyri…

CAN201 计网大题收集

网络性能计算 e.g1 e.g2 木桶效应 e.g3 吞吐量 e.g4 时延 e.g5 时延 e.g5 e.g e.g6 拓展 e.g7 传输层 TCP D 拥塞控制算法 拥塞控制算法_哔哩哔哩_bilibili 慢开始,拥塞避免,快重传,快恢复 物理层 根据我印象好像不太需要学物理层这块…

科技成果鉴定测试的意义在哪?鉴定测试报告重要吗?

科技成果鉴定测试是指通过一系列的评估和验证过程,对科技成果所包含的技术特征、技术优势以及市场应用前景进行客观、准确的评估和证明的过程。科技成果鉴定测试可以对科技项目进行全面、系统的评估,从而找出项目的优势和不足之处,并为项目的…

usb静电防护芯片选择

方案1 USBLC6-2SC6 优缺点 优点:进出使用不同的焊盘,如果没有焊接好信号必定不能通过。有效的避免了虚焊导致故障。 缺点:不能省略,调试时也不能省略。 原理图 参考价格 参考来源 USB切换方案,多电脑共用USB方案…

零基础小白如何自学sql?

学习SQL对于数据分析和处理来说非常重要。SQL是一种强大的工具,可以帮助你与数据库沟通,提取,整理和理解数据。 以下是一些学习SQL的建议: 01 前期:SQL数据库学习 了解SQL的基本概念:首先,你…

求幸存数之和 - 华为OD统一考试

OD统一考试(C卷) 分值: 100分 题解: Java / Python / C++ 题目描述 给一个正整数列nums,一个跳数jump,及幸存数量left。运算过程为:从索引为0的位置开始向后跳,中间跳过 J 个数字,命中索引为 J+1 的数字,该数被敲出,并从该点起跳,以此类推,直到幸存left个数为止。…

Java高级工程师20道面试题、答案及案例

文章目录 Java高级工程师面试题、答案及案例: 问题: 在Java中,如何实现线程安全的单例模式?请写出双重检查锁定(Double-Checked Locking)的实现方式。 答案与案例: public class Singleton {pri…

电机控制----------龙伯格观测器引入

一、建立龙波格观测器 通过求解A矩阵的特征值来判断,整个系统是否稳定。 二、状态空间方程

ChatGPT可以帮你做什么?

学习 利用ChatGPT学习有很多,比如:语言学习、编程学习、论文学习拆解、推荐学习资源等,使用方法大同小异,这里以语言学习为例。 在开始前先给GPT充分的信息:(举例) 【角色】充当一名有丰富经验…

003-10-02【Spark官网思维笔记】香积寺旁老松树边马大爷家女儿大红用GPT学习Spark入门知识

003-10-02【Spark官网思维笔记】香积寺旁老松树边马大爷家女儿大红用GPT学习Spark入门知识. Spark 快速入门快速开始使用 Spark Shell 进行交互式分析:独立的应用程序其他 1, 使用 Spark Shell 进行交互式分析1.1 基本1.2 有关Dataset操作的更多信息1.3 缓存 2&…

STL-list的使用简介

目录 ​编辑 一、list的底层实现是带头双向循环链表 二、list的使用 1、4种构造函数(与vector类似)​编辑 2、迭代器iterator 3、容量(capicity)操作 4、element access 元素获取 5、增删查改 list modifiers 6、list的迭…

YOLOv8优化策略:轻量化改进 | 华为Ghostnet,超越谷歌MobileNet | CVPR2020

🚀🚀🚀本文改进:Ghost bottleneck为堆叠Ghost模块 ,与YOLOV8建立轻量C2f_GhostBottleneck 🚀🚀🚀YOLOv8改进专栏:http://t.csdnimg.cn/hGhVK 学姐带你学习YOLOv8,从入门到创新,轻轻松松搞定科研; 1.Ghostnet介绍 论文: https://arxiv.org/pdf/1911.11907.…

Js--数组(三)

1.什么是数组? 数组:(Array)是一种可以按顺序保存数据的数据类型 2.为什么要数组? 思考:如果我想保存一个班里所有同学的姓名怎么办? 场景:如果有多个数据可以用数组保存起来,然后放到一个变量…

合并 PDF 文件的7个免费软件

PDF 合并是将所需信息转移到一个文件夹中的最佳选择,因此,您需要最好的 pdf 合并软件。 在 PDF 中复制粘贴不同的格式然后进行编辑非常耗时。因此,请查看我们的首选免费软件以合并 PDF 文件。 此列表中的工具可让您一次处理大量文件&#x…

STM32 ADC采样调试笔记

最近在搞STM32L051系列一个小MCU,要用这个去采集两路ADC作为输入。期间也碰到过一些问题,顺便记录下。 ADC采集原理不说了,主要采集电压,用数字进行细分,这样就可以知道输入电压多少了,网上也有很多相关文…

【附源码】基于SSM+Java的题库管理系统的设计与实现

基于SSMJava的题库管理系统的设计与实现 🍅 作者主页 央顺技术团队 🍅 欢迎点赞 👍 收藏 ⭐留言 📝 🍅 文末获取源码联系方式 📝 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(We…

一小时掌握:使用ScrapySharp和C#打造新闻下载器

引言 爬虫技术是指通过编程的方式,自动从互联网上获取和处理数据的技术。爬虫技术有很多应用场景,比如搜索引擎、数据分析、舆情监测、电商比价等。爬虫技术也是一门有趣的技术,可以让你发现网络上的各种有价值的信息。 本文将介绍如何使用…

Linux -- Nginx服务基础

4.1Nginx服务基础 Nginx(发音为[engine x])专为性能优化而开发,其最知名的优点是它的稳定性和低系统资源消 耗,以及对HTTP并发连接的高处理能力(单台物理服务器可支持30000~50000个并发请求),正因 为如此,…

ffmpeg 视频分辨率修改 质量压缩

随着手机像素的提高,拍摄视频也越来越大,10秒的视频动辄 二三十兆,这给视频传输和播放都带来了 诸多不变。一般都需要 前端或或者后端 对视频进行压缩。由于我这边前端是 H5,所以只能后端进行压缩, 采用主流压缩库采用…