JS内存空间

对于很多没经验的前端开发来说,觉得JS反正有垃圾回收机制,很容易忽视内存空间的管理,这其实是一个大错误。

直到最近,看了阮一峰老师关于JS内存泄漏的文章,才发现自己以前写的代码,存在许多内存泄漏的问题,再者,因为忽略对内存空间的学习,导致后面很多进阶概念很模糊,比如闭包、作用域链,比如深拷贝与浅拷贝的区别等等。

这里先介绍内存空间,后续还会通过别的文章来介绍深浅拷贝和内存泄漏。

一、内存空间管理

JavaScript的内存生命周期:

  • 分配你所需要的内存
  • 使用分配到的内存(读、写)
  • 不需要时将其释放、归还

为了便于理解,我们使用一个简单的例子来解释这个周期。

var a = 10; // 在内存中给数值变量分配空间
alert(a + 90); // 使用分配到的内存
a = null; // 使用完毕之后,释放内存空间

在JS中,每一个数据都需要一个内存空间。
内存空间又被分为两种,栈内存(stack)与堆内存(heap)。

二、栈与堆

(stack)是有序的,主要存放一些基本类型的变量和对象的地址,每个区块按照一定次序存放(后进先出),它们都是直接按值存储在栈中的,每种类型的数据占用的内存空间的大小也是确定的,并由系统自动分配和自动释放。

因此,这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间,且寻址速度也更快。

image.png

堆(heap)是没有特别的顺序的,数据可以任意存放,多用于复杂数据类型(引用类型)分配空间,例如数组对象、object对象。

其实这样说也不太准确,因为,引用类型数据的地址是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得想要访问对象的地址,然后,再通过地址指向找出堆中的所需数据。就好比书架上的书,虽然已经按顺序放好了,但我们只要知道书的名字,就可以对应的取下来。

image.png

1.2、变量的存放

首先,我们来看一下代码:

//原始类型都放在栈(stack)里
//引用类型都放在堆(heap)里
var a = 10;
var b = "lzm";
var c = true;
var d = { n: 22 }; //地址假设为0x0012ff7f,不代表实际地址
var e = { n: 22 }; //重新开辟一段内存空间,地址假设为0x0012ff8c
console.log(e == d); //false
var obj = new Object(); //地址假设为0x0012ff9d
var arr = ["a", "b", "c"]; //地址假设为0x0012ff6e

为什么console.log(e==d)的结果为false?可以用下面的内存图解释:

image.png

变量a,b,c为基本数据类型,它们的值,直接存放在栈中d,e,obj,arr为复合数据类型,他们的引用变量及地址存储在栈中,指向于存储在堆中的实际对象

我们是无法直接操纵堆中的数据的,也就是说我们无法直接操纵对象,我们只能通过栈中对对象的内存地址来操作对象,就像我们通过遥控机操作电视一样,区别在于这台电视本身并没有控制按钮。

变量d,e虽然指向存在堆内存中对象内容的值是相等的,但是它们来自栈内存中变量地址不相同,导致console.log(e==d)的结果为false

这里就回到了最初的疑问,为什么原始类型值要放在栈中,而引用类型值要放在堆中,为什么要分开放置呢?单列一种内存岂不是更省事吗?那接下来,援引这篇文章里边的解释:

堆比栈大,栈比堆的运算速度快,对象是一个复杂的结构,并且可以自由扩展,如:数组可以无限扩充,对象可以自由添加属性。将他们放在堆中是为了不影响栈的效率,并且可以自由扩展,而是通过引用的方式查找到堆中的实际对象再进行操作。所以简单数据类型的值直接存放在栈中。

1.3、比较抠细节的面试题

下面的几道是关于内存空间的面试题,虽然不是特别的难,但比较扣细节你稍不注意就错了,我的建议还是老老实实画个内存图再自信的给出正确答案吧。

第一题:

var a = 1;
var b = a;
b = 2;
请问 a 显示是几? 

image.png

上图中可以看出,答案为:1。在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a执行之后,a与b虽然值都等于1,但是他们其实已经是相互独立互不影响的值了。

第二题:

var a = { name: "a" };
var b = a;
b = { name: "b" };
请问现在 a.name 是多少? 

image.png

上图中可以看出,答案为:"a"。因为b = { name: "b" };后相当于重新在堆内存中分配内存给对象{ name:'b' },同时栈内存中变量b的指向地址也随之变化,变量a不受影响。

第三题:

var a = { name: "a" };
var b = a;
b.name = "b";
请问现在 a.name 是多少?

image.png

上图中可以看出,答案为:"b"。我们通过var b = a执行一次复制引用类型的操作。引用类型的赋值,仅仅只是复制引用类型的一个地址指针。当地址指针相同时,他们在内存中访问到的具体对象实际上是同一个,因此b.name ='b'使堆内存中对象的value值变化,a.name的值也随之变化。

第四题:

var a = { name: "a" };
var b = a;
b = null;
请问现在 a 是什么?

image.png

上图中可以看出,答案为:{ name: "a" }。因为null为基本类型,存在栈内存当中。因此栈内存中的变量b由之前指向对象的一个地址转变为null,变量a的地址还是指向原先的对象。

第五题:

var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };

a.x 	// 这时 a.x 的值是多少
b.x 	// 这时 b.x 的值是多少

答案是:a.x --> undefined;b.x --> {n: 2}。这道题的关键在于:

  • 1、优先级.的优先级高于=,所以先执行a.x,堆内存中的{n: 1}就会变成{n: 1, x: undefined},改变之后相应的b.x也变化了,因为指向的是同一个对象。
  • 2、赋值操作是从右到左,所以先执行a = {n: 2}a的引用就被改变了,然后这个返回值又赋值给了a.x,需要注意的是这时候a.x是第一步中的{n: 1, x: undefined}那个对象,其实就是b.x,相当于b.x = {n: 2}

image.png

二、总结一下栈内存和堆内存的区别

image.png

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值