JavaScript的内存模型
引言
在我们的前端日常工作中,无时无刻不在进行着变量的声明和赋值,你是否也曾碰到过变量声明报错或变量被污染的问题,如果你跟笔者一样碰到过,那么我们应该暂时停下来好好思考问题发生的原因以及如何采取相应的补救措施。当然排查问题最好的方式就是深入其底层细节,了解在JavaScript中的内存分配方式。只有我们对底层细节有一定的了解之后,才能轻而易举地化解在写代码过程中遇到的各种问题。本文基于JavaScript的内存模型继续衍生出let
和const
的差异性对比,若文中有错误的地方,还请指出。
1、内存是什么
在讲解JavaScript中的内存模型之前,我们先从硬件层面来简单了解下内存是什么。
内存是计算机中重要的部件之一,它是外存与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。内存(Memory)也被称为内存储器和主存储器,其作用是用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。只要计算机在运行中,CPU就会把需要运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来,内存的运行也决定了计算机的稳定运行。
内存条是计算机组成结构中的关键部分,其本身是一个非常精密的部件,内部包含了上亿个电子元器件,它们很小,达到了纳米级别。这些元器件,实际上也就是电路,电路的电压会发生变化,但只有两种可能,要么0V(低电平),要么5V(高电平),0V是断电,用0来表示,5V是通电,用1来表示,因此一个元器件包含了两个状态0和1,即表示一位(bit)。但是作为人类,我们并不擅长使用bit来思考和计算,因此我们会将它们划分成更大的组,例如8位表示1个byte(字节),16位表示2个byte(字节),32位表示4个byte(字节)。有很多东西都是存储在内存中的,比如我们的程序代码,程序中所声明的变量以及操作系统的代码等。
2、内存的生命周期
了解了内存的基本概念后,我们来简单聊聊内存的生命周期。JavaScript作为一门高级编程语言,不像其他语言(例如C语言)中需要开发人员手动地去管理内存,系统会自动为你分配内存。但是无论是哪种编程语言,内存的生命周期都主要分为三个阶段:
分配内存
:由操作系统来分配内存,供程序使用。在JavaScript中,这一步由操作系统来自动分配,无需开发人员手动操作。使用内存
:程序获得操作系统所分配的内存之后,在内存中发生读和写操作。释放内存
:程序使用完内存之后,会将这部分内存释放出来供其他程序使用。在JavaScript中,这一步同样不需要开发人员手动操作,由操作系统自动释放。
我们知道,在JavaScript中的数据类型分为基本数据类型和引用数据类型,其中基本数据类型包括String
、Number
、Boolean
、Null
、Undefined
,ES6中新增的Symbol
以及最新的BigInt
,除了这些以外,其他的均为引用数据类型,例如Array
、Date
、Function
、RegExp
、Error
,Object
等。那么这两种数据类型的其中一个区别就是,基本数据类型的内存大小都是固定的,而引用数据类型的内存大小都是动态不固定的,可能会随时发生变化。因此在内存分配阶段这两种数据类型会有一定的差异。
编译器在编译代码时,对于基本数据类型,由于其空间大小固定,编译器在检查时会提前计算它们需要的内存大小,并插入与操作系统交互的代码,向操作系统申请存储变量所需的堆栈字节数,然后将申请到的内存分配给调用堆栈中的程序,称为静态内存分配。例如在调用函数时,函数中的变量所需的内存会被添加到现有的内存之上,当函数执行完毕后,这部分内存又会以后进先出(LIFO)的顺序被移除。但是对于引用数据类型,其空间大小是动态的,在编译阶段无法直接确定其需要多少内存,因此不能在堆栈上为其分配内存,相反,需要在运行时向操作系统申请适当的内存,并且这部分内存是在堆空间进行分配的,称为动态内存分配。静态内存分配和动态内存分配的区别如下表所示:
静态内存分配 | 动态内存分配 |
---|---|
编译阶段可确定大小 | 编译阶段无法确定大小 |
在编译时执行 | 在运行时执行 |
分配给堆栈 | 分配给堆 |
顺序分配,后进先出(LIFO) | 无序分配 |
3、JavaScript中的内存分配
在我们的前端开发日常工作中,几乎每天都在做着变量的声明和赋值,这些变量最终都会被存放到内存中,所以我们还是有必要了解一下在JavaScript中的内存分配方式,这里使用基本数据类型和引用数据类型来分别讲述一下内存的分配过程,帮助我们理解JavaScript的底层细节。
首先我们从一个简单的基本数据类型的赋值开始,代码如下:
let num = 1;
当JavaScript引擎在执行到这行代码时,会执行如下操作:
- 为变量
num
创建一个唯一标识符(identifier),该标识符用于与栈内存中的地址A1
形成映射关系。 - 在栈内存中为其分配一个地址
A1
。 - 将值
1
存储到分配的地址。
示例图如下:
通常我们说num
变量的值等于1
,但其实严格意义上来讲,num
变量的值等于栈内存中存放对应值的内存地址(如图中的A1
)。接下来我们创建一个新的变量newNum
并将num
赋值给它:
let newNum = num;
经过以上赋值之后,通常说newNum
的值为1
,同样从严格意义上来讲的话是指newNum
和num
指向同一个内存地址A1
,如下图所示:
如果接下来我们执行以下操作,看会发生什么:
num = num + 1;
我们对num
变量进行自增长,很显然num
变量的值为2
。由于newNum
和num
指向同一个内存地址A1
,那么此时newNum
的值是否也为2
呢,在回答这个问题之前,我们先来看一下当前内存地址发生的变化:
在上图中我们可以发现,num
变量的内存地址发生了改变,由原来的A1
变为A2
,这是因为在JS中的基本数据类型都是不可变的,一旦修改,只会为其分配新的内存地址并将修改后的新值存入到新的地址中,因此回答上面的那个问题,newNum
的值保持不变,依旧为1
,因为它的内存地址没有发生改变。再看如下示例:
let str = 'ab';
str = str + 'c';
因为字符串也是属于基本数据类型,基本数据类型都是不可变的,所以即使上述代码中只是简单的将c
拼接到了原来的字符串ab
后面,但是依旧会为其分配新的内存地址,变量str
最终会指向这个新的内存地址,如下图所示:
了解了基本数据类型的内存分配方式之后,接下来我们来了解下引用数据类型的内存分配方式。同样我们从一个简单的引用数据类型的赋值开始:
let arr = [];
当JavaScript引擎在执行到这行代码时,会执行如下操作:
- 为变量
arr
创建一个唯一标识符(identifier),该标识符用于与栈内存中的地址A3
形成映射关系。 - 在栈内存中为其分配一个地址
A3
。 - 栈内存中存储在堆中分配的内存地址的值
H1
。 - 在堆中存储分配的值
空数组[]
。
示例图如下:
在JavaScript引擎(例如Chrome和Node的V8引擎)中主要是由两个部件组成,一个叫内存堆(Memory Heap),一个叫调用堆栈(Call Stack)。其中调用堆栈除了函数调用之外,主要用于存放基本数据类型的值,而引用数据类型的值一般都存放在内存堆中,堆中存放的数据都是无序的并且可以动态地增长,所以非常适合用于存储数组和对象。
4、let
和const
的差异性对比
在了解完以上两种数据类型的内存分配方式后,我们这里对let
和const
的使用方式进行一下对比,通常来说,我们建议在写代码的过程中能使用const
的地方尽量减少使用let
,这样可以在某种程度上避免变量被无端修改而引发的一系列问题。如下代码:
let num = 1;
num = num + 1;
let arr = [];
arr.push(1);
arr.push(2);
arr.push(3);
在上述代码中,变量num
因为使用let
的方式声明,所以允许其被修改,因为基本类型的值是不可变的,所以会为num
变量分配新的内存地址。对于arr
变量,这里同样使用let
方式进行声明,表示允许其修改,但是对于push
操作其实并没有修改arr
变量的内存地址,只是将新的值推入了堆内存的数组中,所以此处建议修改为使用const
进行声明。
笔者的观点是:将修改理解为修改内存地址,若允许修改内存地址,则使用
let
进行声明,否则使用const
进行声明。
如下示例:
const num = 1;
num = num + 1;
由在上一小节中了解到的基本数据类型的内存分配方式,我们知道为变量num
在栈内存中分配了一个地址来保存对应的值。
但是这里我们是使用const
的方式来进行声明的,当我们重新为变量num
进行赋值时,JS尝试为其分配新的内存地址,那么这里也就是抛出错误的地方,因为我们明确不允许对其进行修改。
因此在控制台中我们会看到对应的报错信息。
再看如下示例:
const arr = [];
对于引用数据类型,我们知道会在栈内存上为其分配内存地址,存储的是堆中的内存地址的值。
我们做如下操作:
arr.push(1);
arr.push(2);
arr.push(3);
执行push
操作实际上是将新值推入堆中的数组,内存地址并没有发生改变。这也就是为什么虽然使用const
声明变量,但是依旧没有报错的原因。但是如果我们使用如下方式:
arr = 1;
arr = undefined;
arr = null;
arr = [];
arr = {};
这些方式都会修改原数组的内存地址,const
声明是不允许修改内存地址的,所以很明显会抛出错误。因此这里也是建议默认情况下使用const
声明变量,除非需要修改内存地址,const
声明的变量必须在声明时进行初始化,也方便了其他前端人员能一眼看出哪些变量是不可变的。
5、总结
在本篇中主要总结了一下JavaScript中的内存模型,并针对基本数据类型和引用数据类型分别讲述了其在JavaScript中的内存分配方式,然后对let
和const
这两种在代码中的变量声明方式进行对比以了解其中的差异性,下篇基于内存模型继续讲解JavaScript引擎中的垃圾回收机制以及在写代码过程中的几种有效避免内存泄漏的方式,和大家一起了解JavaScript的底层细节。
6、交流
若觉得笔者的文章对你有帮助的话,不妨关注下笔者的公众号,每周都会原创和整理一些前端技术干货,关注公众号后可以邀你入群,我们一起交流前端,相互学习,共同进步。
文章已同步更新至Github博客,若觉文章尚可,欢迎前往star!
你的一个点赞,值得让我付出更多的努力!
逆境中成长,只有不断地学习,才能成为更好的自己,与君共勉!
JavaScript的内存模型的更多相关文章
- JavaScript 是如何工作的:JavaScript 的内存模型
摘要: 从内存角度理解 let 和 const 的意义. 原文:JavaScript 是如何工作的:JavaScript 的内存模型 作者:前端小智 Fundebug经授权转载,版权归原作者所有. 这 ...
- javascript 内存模型
我对于 JavaScript 的内存模型一直都比较困惑,很想了解在操作变量的时候,JS 是如何工作的.如果你和我有同样的困惑,希望这篇文章能给你一些启发. 译文,喜欢原文的可以直接拉到底部 当我们声明 ...
- (转)JavaScript内存模型
JavaScript对象内存模型 转自:http://blog.csdn.net/u010425776/article/details/53617292 推荐-JavaScript作用域链内存模型: ...
- JavaScript学习系列之内存模型篇
一个热爱技术的菜鸟...用点滴的积累铸就明日的达人 正文 如果真的想学好一门语言,那么一定要了解它内存模型,本篇文章就带你走进JavaScript的内存模型,由于本人才疏学浅,若有什么表述有误的地方, ...
- 浅谈JavaScript原型图与内存模型
js原型详解 1.内存模型: 1.原型是js中非常特殊一个对象,当一个函数(Person)创建之后,会随之就产生一个原型对象 2. 当通过这个函数的构造函数创建了一个具体的对象(p1)之后,在这个具体 ...
- 栈 堆 stack heap 堆内存 栈内存 内存分配中的堆和栈 掌握堆内存的权柄就是返回的指针 栈是面向线程的而堆是面向进程的。 new/delete and malloc/ free 指针与内存模型
小结: 1.栈内存 为什么快? Due to this nature, the process of storing and retrieving data from the stack is ver ...
- Java内存模型深度解析:总结--转
原文地址:http://www.codeceo.com/article/java-memory-7.html 处理器内存模型 顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会 ...
- JVM学习(3)——总结Java内存模型
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 为什么学习Java的内存模式 缓存一致性问题 什么是内存模型 JMM(Java Memory Model)简 ...
- 浅析java内存模型--JMM(Java Memory Model)
在并发编程中,多个线程之间采取什么机制进行通信(信息交换),什么机制进行数据的同步? 在Java语言中,采用的是共享内存模型来实现多线程之间的信息交换和数据同步的. 线程之间通过共享程序公共的状态,通 ...
随机推荐
- Python基本数据结构之二进制
二进制---->ASCII :只能存英文和拉丁字符.一个字符占一个字节,8位----->gb2312:只能6700多个中文,1980------->gbk1.0:村落2万多字符,19 ...
- m102 SE赛
这次考试考完试正在刷提交记录的时候,到我这突然oj卡了一下,然后卡了大约10s,再刷出来就发现:
- Mac中的Python安装selenium,结合chrom及chromdriver使用
一.安装selenium 1.在终端通过命令安装 pip3 install -U selenium 二.准备环境 1.在电脑中安装谷歌浏览器chrom,和下载估计浏览器驱动chromdriver,以下 ...
- yum 配置文件 以及 语法
yum的配置文件 #vi /etc/yum.conf [main] cachedir=/var/cache/yum/$basearch/$releasever keepcache= debugleve ...
- lqb 基础练习 查找整数 (遍历)
基础练习 查找整数 时间限制:1.0s 内存限制:256.0MB 问题描述 给出一个包含n个整数的数列,问整数a在数列中的第一次出现是第几个. 输入格式 第一行包含一个整数n. 第二行包含 ...
- nyoj 168-房间安排 (贪心)
168-房间安排 内存限制:64MB 时间限制:3000ms 特判: No 通过数:33 提交数:71 难度:2 题目描述: 2010年上海世界博览会(Expo2010),是第41届世界博览会.于20 ...
- 使用Android Studio进行ndk开发的准备
1. gradle-ex2. ndk开发包3. 项目目录结构4. lldb调试器 1. 一般来说gradle不是必需的,gradle也是可以进行ndk编译的,然而你需要在编译时使用更多(多于一个)c+ ...
- synchronized:内部锁
synchronized:内部锁 起源: 并行程序开发涉及多线程.多任务间的协作和数据共享 一).内部锁:synchronized 1).定义在方法上 public synchronized void ...
- 正则表达式 第六篇:调用CLR函数执行正则查询
在SQL Server数据库中可以执行模糊查询,像like子句,和全文查询(Fulltext search),但是无法直接执行正则查找,SQL Server没有执行正则表达式的内置函数,但是我们可以创 ...
- 2019-10-8:渗透测试,基础学习,php基础,会话,文件包含,笔记
php面向对象基础->调用符号构造函数construct,主要用来创建对象时初始化对象,为成员变量赋初始值,总与new运算符一起使用在创建对象的语句中 析构函数destructor,与构造函数相 ...