在咱们的前端平常工做中,无时无刻不在进行着变量的声明和赋值,你是否也曾碰到过变量声明报错或变量被污染的问题,若是你跟笔者同样碰到过,那么咱们应该暂时停下来好好思考问题发生的缘由以及如何采起相应的补救措施。固然排查问题最好的方式就是深刻其底层细节,了解在JavaScript中的内存分配方式。只有咱们对底层细节有必定的了解以后,才能垂手可得地化解在写代码过程当中遇到的各类问题。本文基于JavaScript的内存模型继续衍生出let
和const
的差别性对比,若文中有错误的地方,还请指出。javascript
在讲解JavaScript中的内存模型以前,咱们先从硬件层面来简单了解下内存是什么。前端
内存是计算机中重要的部件之一,它是外存与CPU进行沟通的桥梁。计算机中全部程序的运行都是在内存中进行的,所以内存的性能对计算机的影响很是大。内存(Memory)也被称为内存储器和主存储器,其做用是用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。只要计算机在运行中,CPU就会把须要运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来,内存的运行也决定了计算机的稳定运行。java
内存条是计算机组成结构中的关键部分,其自己是一个很是精密的部件,内部包含了上亿个电子元器件,它们很小,达到了纳米级别。这些元器件,实际上也就是电路,电路的电压会发生变化,但只有两种可能,要么0V(低电平),要么5V(高电平),0V是断电,用0来表示,5V是通电,用1来表示,所以一个元器件包含了两个状态0和1,即表示一位(bit)。可是做为人类,咱们并不擅长使用bit来思考和计算,所以咱们会将它们划分红更大的组,例如8位表示1个byte(字节),16位表示2个byte(字节),32位表示4个byte(字节)。有不少东西都是存储在内存中的,好比咱们的程序代码,程序中所声明的变量以及操做系统的代码等。git
了解了内存的基本概念后,咱们来简单聊聊内存的生命周期。JavaScript做为一门高级编程语言,不像其余语言(例如C语言)中须要开发人员手动地去管理内存,系统会自动为你分配内存。可是不管是哪一种编程语言,内存的生命周期都主要分为三个阶段:github
分配内存
:由操做系统来分配内存,供程序使用。在JavaScript中,这一步由操做系统来自动分配,无需开发人员手动操做。使用内存
:程序得到操做系统所分配的内存以后,在内存中发生读和写操做。释放内存
:程序使用完内存以后,会将这部份内存释放出来供其余程序使用。在JavaScript中,这一步一样不须要开发人员手动操做,由操做系统自动释放。咱们知道,在JavaScript中的数据类型分为基本数据类型和引用数据类型,其中基本数据类型包括String
、Number
、Boolean
、Null
、Undefined
,ES6中新增的Symbol
以及最新的BigInt
,除了这些之外,其余的均为引用数据类型,例如Array
、Date
、Function
、RegExp
、Error
,Object
等。那么这两种数据类型的其中一个区别就是,基本数据类型的内存大小都是固定的,而引用数据类型的内存大小都是动态不固定的,可能会随时发生变化。所以在内存分配阶段这两种数据类型会有必定的差别。编程
编译器在编译代码时,对于基本数据类型,因为其空间大小固定,编译器在检查时会提早计算它们须要的内存大小,并插入与操做系统交互的代码,向操做系统申请存储变量所需的堆栈字节数,而后将申请到的内存分配给调用堆栈中的程序,称为静态内存分配。例如在调用函数时,函数中的变量所需的内存会被添加到现有的内存之上,当函数执行完毕后,这部份内存又会之后进先出(LIFO)的顺序被移除。可是对于引用数据类型,其空间大小是动态的,在编译阶段没法直接肯定其须要多少内存,所以不能在堆栈上为其分配内存,相反,须要在运行时向操做系统申请适当的内存,而且这部份内存是在堆空间进行分配的,称为动态内存分配。静态内存分配和动态内存分配的区别以下表所示:数组
静态内存分配 | 动态内存分配 |
---|---|
编译阶段可肯定大小 | 编译阶段没法肯定大小 |
在编译时执行 | 在运行时执行 |
分配给堆栈 | 分配给堆 |
顺序分配,后进先出(LIFO) | 无序分配 |
在咱们的前端开发平常工做中,几乎天天都在作着变量的声明和赋值,这些变量最终都会被存放到内存中,因此咱们仍是有必要了解一下在JavaScript中的内存分配方式,这里使用基本数据类型和引用数据类型来分别讲述一下内存的分配过程,帮助咱们理解JavaScript的底层细节。
首先咱们从一个简单的基本数据类型的赋值开始,代码以下:编程语言
let num = 1;
当JavaScript引擎在执行到这行代码时,会执行以下操做:ide
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)。其中调用堆栈除了函数调用以外,主要用于存放基本数据类型的值,而引用数据类型的值通常都存放在内存堆中,堆中存放的数据都是无序的而且能够动态地增加,因此很是适合用于存储数组和对象。
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
声明的变量必须在声明时进行初始化,也方便了其余前端人员能一眼看出哪些变量是不可变的。
在本篇中主要总结了一下JavaScript中的内存模型,并针对基本数据类型和引用数据类型分别讲述了其在JavaScript中的内存分配方式,而后对let
和const
这两种在代码中的变量声明方式进行对比以了解其中的差别性,下篇基于内存模型继续讲解JavaScript引擎中的垃圾回收机制以及在写代码过程当中的几种有效避免内存泄漏的方式,和你们一块儿了解JavaScript的底层细节。
若以为笔者的文章对你有帮助的话,不妨关注下笔者的公众号,每周都会原创和整理一些前端技术干货,关注公众号后能够邀你入群,咱们一块儿交流前端,相互学习,共同进步。
文章已同步更新至Github博客,若觉文章尚可,欢迎前往star!
你的一个点赞,值得让我付出更多的努力!
逆境中成长,只有不断地学习,才能成为更好的本身,与君共勉!