js中数组是如何在内存中存储的?

news2024/11/23 7:35:17

数组不是以一组连续的区域存储在内存中,而是一种哈希映射的形式。它可以通过多种数据结构来实现,其中一种是链表。

js分为基本类型和引用类型:

  • 基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问;
  • 引用类型是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用

js的数据类型

js的数据分为两种, 一种是原始类型(Boolean,Null,Undefined,Number,BigInt,String,Symbol), 一种是对象(Object)。
原始类型的数据放在栈中,对象的数据放在堆中。

堆栈的区别

堆(heap)是不连续的内存区域,即数据可以任意存放, 主要存放的是对象等。

栈(stack)是一块连续的内存区域,每个区块按照一定次序存放(后进先出),栈中主要存放的是基本类型的变量的值以及指向堆中的数组或者对象的地址。

为什么要区分堆栈

变量主要是两种形式,一种内容短小(比如一个int整数),需要频繁访问,但是生命周期很短,通常只在一个方法内存活,而另一种内容可能很多(比如很长一个字符串),可能不需要太频繁的访问,但生命周期较长,通常很多个方法中可能都要用到,那么自然将这两类变量分开就显得比较理性,一类存储在栈区,通常是局部变量、操作符栈、函数参数传递和返回值,另一类存储在堆区,通常是较大的结构体(或者OOP中的对象)、需要反复访问的全局变量。 堆区就是各种慢,申请内存慢,访问慢,修改慢,释放慢,整理慢(或者说GC垃圾回收),但优点也不言而喻,访问随机灵活,空间超大,在不超可用内存的情况下你要多大就给多大。 栈区就像临时工,干完就跑,所以超快,但是缺点也很多,比如生命周期短,一般只能在一个方法内存活,又比如你需要事先知道需要多大的栈(事实上绝大多数语言栈区要分配的大小编译期就确定了,Java就是这样),而且通常最大栈区可用内存都很小,你不可能往栈区里堆很多数据。

原始类型

原始类型有一个特点就是不可变。示例代码如下

// 例子1
var str = "abc";
str[0] = "d";
console.log(str) // abc  

// 例子2
var str2 = "abc";
str2 = "dbc";
console.log(str) // dbc

例子1的数据没有改变, 例子2的数据却改变了, 实际上例子2是创建了一个新的字符串, 也就是内存开辟了一个新的区域给"dbc"使用。

简单点来讲, 就是假设栈中存放了一个数据如"abc", 那么这个数据就永远不会改变, 而如果是如例子2中赋值了一个其他的字符串或者任何其他改变值的情况下, 栈中都会保留原来的"abc", 然后新开一个地方存放"dbc"。 类似下图:

为什么要把基础类型的值设成不可变

  1. 为了安全
    假设基础类型的值是可变的, 那么下面的代码会变得很奇怪
var strTest = "varaiable";
var fun = (str) => { str + "---ok" };
fun(strTest);
console.log(strTest) // varaiable---ok

// 可以看到strTest的值被改变了, 特别是在map之类的对象中更为显著  
var map = new Map()
var strTest = "t1";
map.set(strTest, 10);
strTest = "notT1";
map.get("t1"); // undefined;
map.get("notT1"); // 10

这样的代码容易造成更多的bug,特别是像java之类的多线程语言, 更有可能造成线程不安全的问题。

  1. 为了共享
    实际上, 基础类型中, 值一样的变量是共享一个内存区域的。

    这样做的好处是避免额外的内存开销,提升效能。

        当然, 这个前提是基础类型不可变, 不然如果str1的值变化了, str2的值也会跟着变化(实 际上并没有对其操作)。

对象类型

V8中的对象(数组也是对象)存储相对来说比较复杂,他们是存放在堆里面的数据。并且格式大致如下:

这和很多资料说的是用Map实现不同, 很明显, 根据上图(来自v8的博客),起码可以说明不是使用Map来处理的。

V8是把对象中的属性分成两类, 一类是字符常量, 一类是数字or数字字符串(如"1"这种),并分别放在了两个数组,Properties和Elements。

普通的字符常量
先从普通的字符常量说起, 字符常量的存放方式又细分为三类。

第一类: In-object
实际上, 在生成一个对象的时候, v8会给该对象留下一些空间以分配属性(数量由对象的初始大小预先确定),这些属性直接存储在对象本身上。这些是V8中最快的属性,因为无需任何间接访问即可访问它们,如下图:

第二类: Fast properties
v8的In-object空间并不多,通过对象字面量创建的无属性对象分配 4 个对象内属性存储(inobject_properties)空间。当这些空间被使用完之后, 即会通过HideClass(隐藏类,有些也叫Map,这里统一叫隐藏类)来协助完成属性的快速访问。

HiddenClasses and DescriptorArrays
HiddenClass存储有关对象的元信息,包括该对象上的属性数量以及对该对象原型的引用。除此之外,HiddenClasses里面还有一个DescriptorArrays数组, 该数组存储了对象属性的信息。
即如下图:

这里一般会有一个疑惑, 为什么需要一个隐藏类, 我直接搞一个hashTable不是更快吗?
关于隐藏类及ICs的概率, 推荐阅读这一篇文章JavaScript 引擎基础:Shapes 和 Inline Caches, 概念清晰易懂,图文并茂。
这里简单说一下概念:
首先看下, 隐藏类是怎么来的

从图中可以看出, 隐藏类是通过一颗树来不断生成的,每添加一个属性都会新生成一个隐藏类节点(添加数组索引属性不会创建新的), 然后呢, 具有相同结构(相同属性,顺序相同)的对象具有相同的隐藏类。也就是说, 如果在上面的代码中加一个代码如下:

var a = {};
a.a = "ddd";

var b = {};
b.a = "3";
b.b = "test";

那么a的隐藏类是右边的第一个nofOwnDescriptors, b是第二个。对于程序代码来说, 实际上很多对象都是拥有相同的隐藏类。而隐藏类背后的主要动机是 Inline Caches 或 ICs 的概念。ICs 是促使 JavaScript 快速运行的关键因素!JavaScript 引擎利用 ICs 来记忆去哪里寻找对象属性的信息,以减少昂贵的查找次数。
大致就是每次将代码编译成字节码并读取属性时,都会根据隐藏类把该属性的位置保存起来,在下一次读取或者遇到拥有相同隐藏类的对象读取时,可以根据隐藏类提供的属性位置直接读取,而避免查找过程。

第三类: Slow properties
最后一种方式即是字典存储方式。字典存储模式相对来说比较简单, 先看下官方提供的图:

简单点说, 就是隐藏类里面的DescriptorArrays会直接置为空, 然后把属性的值和元信息直接存储在properties数组中,并通过hash的方式进行get和set。
既然上面说了拥有隐藏类可以带来效能的提升, 为什么还要提供字典方式?
v8的原文如下:

However, if many properties get added and deleted from an object, it can generate a lot of time and memory overhead to maintain the descriptor array and HiddenClasses

大致意思是说,增加删除属性的操作过多会使用大量的时间和内存开销来维护descriptorArray 和 HiddenClasses。

最后, 什么时候是Fast properties(隐藏类), 什么时候是slow properties(字典模式)?
关于这一方面,推荐该系列文章奇技淫巧学 V8 之一,对象访问模式优化, 以下部分为引用 新创建的小对象为Fast properties。执行如下操作的时候会变成slow properties

  1. 动态添加过多的属性
  2. 删除属性(delete)
  3. 删除非最后添加的属性(V8 >= 6.0)

数组类型
数组的话种类比较多, 按官方的话说多达20种类型。
实际上, 数组一般是放到了一开始提的elements数组里面, 然后按索引读值, 这个比较简单, 说下其中比较典型的两种。

  1. 存在缺失的元素,会按原型链串上去拿值,实际上就是对象原型链..
const o = ['a', 'b', 'c'];
console.log(o[1]);          // Prints 'b'.

delete o[1];                // Introduces a hole in the elements store.
console.log(o[1]);          // Prints 'undefined'; property 1 does not exist.
o.__proto__ = {1: 'B'};     // Define property 1 on the prototype.

console.log(o[0]);          // Prints 'a'.
console.log(o[1]);          // Prints 'B'.
console.log(o[2]);          // Prints 'c'.
console.log(o[3]);          // Prints undefined

  1. 稀疏数组, 如果存在这种情况, 那么elements会存在大量的内存没有使用, 所以v8优化成字典模式,也就是和上面的字符串一样。
const sparseArray = [];
sparseArray[9999] = 'foo'; // Creates an array with dictionary elements.

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

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

相关文章

【FFmpeg+Qt】视频进度条控制——点击跳转和拖动跳转

首先进度条采用Qslider,设置进度条主要有两点,一是当前视频总时长,二是当前播放时长,需要通过FFmpeg转码成mp4文件才能获取相应的时长数据; 往期回顾: 【QtFFmpeg】视频转码详细流程_logani的博客-CSDN博…

从用户测试中学到的知识

从客户那里获得良好的反馈是个挑战。用户测试有的时候看起来是一个艰巨而且昂贵的任务。但是用户测试可以带来良好的经验,从而帮助设计更好的产品。 那么,从哪里开始呢?我测试了几种方法,有些失败,有些成功。下面我将讲述我所学到…

基于JAVA的教学进度在线管理系统/教学大纲在线管理系统源代码+数据库,含详细项目需求分析、概要设计、详细设计及项目总结文档

项目启动步骤 使用 SQL_Scripts/tms.sql 中的 sql 语句创建数据库与数据库表(数据库建立中,暂无) 修改 src/a_little_config.txt 文件,填入正确的数据库连接用户名、密码 将项目导入 IntelliJ IDEA 或 eclipse。 打开 cn.findix.tms.bin 包下的 WWW 文…

C#使用随机数模拟器来模拟世界杯排名(三)

接上篇 C#使用随机数模拟器来模拟世界杯排名(二)_斯内科的博客-CSDN博客 上一篇我们使用随机数匹配比赛的世界杯国家, 这一篇我们使用随机数以及胜率模拟器 决赛出 世界杯冠军、亚军。 我们在主界面 新增按钮【开始比赛 直到 决出冠军】和【刷新重新随机分配】 …

Python语言程序设计实验报告

第二章:Python变量与数据类型 一、实验目的: 1.了解Python变量的概念与相关含义; 2.学习Python中的数据类型; 二、实验环境: 1.笔记本电脑 2.PyCharm Community Edition 2022.2.3工具 三、实验内容: 1.将字…

ZABBIX6.0LTS安装笔记

一、准备好干净的操作系统 推荐使用:Rocky Linux 8.6 二、安装ZABBIX 官网:https://www.zabbix.com/cn/download 【1】选择您Zabbix服务器的平台 【2】 安装Zabbix包 下载安装包源 # rpm -Uvh https://repo.zabbix.com/zabbix/6.0/rhel/8/x86_64/zabb…

Spring的动态AOP源码解析

一… 引入 1.1 概念 1.2 注解方式使用AOP @Aspect public class LogAspects {/*** 1. 本类引用,只需要写方法名* 2. 其他类引用,需要写路径*/@Pointcut("execution(public int com.floweryu.aop.MathCalculator.*(..))")public void pointCut

Linux--进程间通信

目录1. 进程间通信目的2. 管道2.1 管道特性(匿名管道)2.1.1 单向通信2.1.2 面向字节流2.2 管道的大小2.3 命名管道3. system V进程间通信3.1 shmget函数3.1.1 key VS shmid3.2 shmctl函数3.3 shmat函数 VS shmdt函数:3.4 测试4. 感性认识4.1 …

R语言中的多类别问题的绩效衡量:F1-score 和广义AUC

最近我们被客户要求撰写关于分类的研究报告,包括一些图形和统计输出。对于分类问题,通常根据与分类器关联的混淆矩阵来定义分类器性能。根据混淆矩阵 ,可以计算灵敏度(召回率),特异性和精度。 对于二进制…

基于javaweb物业管理系统的设计与实现/小区物业管理系统

摘 要 随着世界经济快速的发展,全国各地的城市规模不断扩大,住进城市的人口日益增多,房地产行业在现代社会的发展中有着重要的作用,有越来越多的人居住在小区里。 因此,一套高效并且无差错的物业管理系统软件在现代社会…

基于Android的校园一卡通App平台

演示视频信息: A6604基于Android的校园一卡通一、研究背景、目的及意义 (一)研究背景 二十一世纪是信息化的时代,信息化建设成为我们的首要任务。当前我国大力发展信息产业,在全国范围内各行各业开始实施信息化…

为什么要上机械制造业ERP系统?对企业有什么帮助?

在日益竞争激烈的市场背景下,机械制造企业提供的产品需要具有更短的交货期、更高的质量、更好的服务。而机械行业由于其工艺复杂的生产特点,工艺及在制品管理困难,单纯的靠手工记账处理,已经难以满足现代企业科学管理的需要。只有…

艾美捷IFN-gamma体内抗体参数及应用

艾美捷IFN-gamma体内抗体背景: 干扰素γ(IFN-γ)或II型干扰素是一种二聚可溶性细胞因子,是II型干扰素的唯1成员。它是一种细胞因子,对抵抗病毒和细胞内细菌感染的先天性和适应性免疫以及肿瘤控制至关重要。IFNG主要由…

TensorFlow平台应用

目录 一:TensorFlow简介 二:TensorFlow工作形式 三:图/Session 四:安装tensorflow 五:张量 六:变量/常量 七:创建数据流图、会话 八:张量经典创建方法 九:变量赋…

[Java EE初阶]Thread 类的基本用法

本就无大事,庸人觅闲愁. 文章目录1. 线程创建2. 线程中断2.1 通知终止后立即终止2.2 通知终止,通知之后线程继续执行2.3 通知终止后,添加操作后终止3. 线程等待4. 线程休眠5. 获取线程实例1. 线程创建 创建线程有五个方法 详情见我的另一个文章 https://editor.csdn.net/md/?…

【K8S系列】第十二讲:Service进阶

目录 ​编辑 序言 1.Service介绍 1.1 什么是Service 1.2 Service 类型 1.2.1 NodePort 1.2.2 LoadBalancer 1.2.3 ExternalName 1.2.4 ClusterIP 2.yaml名词解释 3.投票 序言 当发现自己的才华撑不起野心时,就安静下来学习吧 三言两语,不如细…

Unity 灯光

初始化时,系统默认会给一个灯光,类型为定向光。 定向光意为,从无穷远处照射过来的平行光,因此每个图形的阴影的方向一致 灯光的系统参数 阴影类型:①无阴影 ②硬阴影 ③软阴影 (注意)阴影类型最…

力扣(LeetCode)164. 最大间距(C++)

桶排序(划分区间) 一次遍历找到区间内最大值 MaxMaxMax ,最小值 MinMinMin 。区间 (Min,Max](Min,Max](Min,Max] 左开右闭,划分为 n−1n-1n−1 个长度为 lenlenlen 的区间 ,划分的区间左开右闭,所以每个子区间有 len−1len-1len−…

SpringCloud学习笔记

SpringCloud学习笔记成熟分布式微服务架构包含技术模块SpringCloud与SpringBoot版本选择SpringCloud各技术模块的技术选择SpringCloud实现订单-支付微服务创建父工程(管理子工程即各个微服务)父工程的build.gradle配置父工程的settings.gradle配置创建支付子工程(payment_nativ…

物联网开发笔记(64)- 使用Micropython开发ESP32开发板之控制ILI9341 3.2寸TFT-LCD触摸屏进行LVGL图形化编程:控件显示

一、目的 这一节我们学习如何使用我们的ESP32开发板来控制ILI9341 3.2寸TFT-LCD触摸屏进行LVGL图形化编程:控件显示。 二、环境 ESP32 ILI9341 3.2寸TFT-LCD触摸屏 Thonny IDE 几根杜邦线 接线方法:见前面文章。 三、滑杆代码 import lvgl as lv i…