资讯专栏INFORMATION COLUMN

JS数据类型 与 内存堆栈

nodejh / 260人阅读

摘要:所以相对于简单数据类型而言,他们占用内存比较小,如果放在堆中,查找会浪费很多时间,而把堆中的数据放入栈中也会影响栈的效率。六总结本文总结了数据类型及其声明赋值更新时在内存堆栈中的表现,可以更深入的理解这些数据类型。

JS数据类型 与 内存堆栈 一、前言

JS的数据类型已经是大家都很熟悉的东西了,但是大家是否对这些数据类型在内存中的分配了解,甚至在操作这些变量时,内存中是如何表现的,本文将对这些做一个总结。

二、JS数据类型

ECMAScript 变量可能包含两种不同数据类型的值:基本类型值和引用类型值。在将一个值赋给变量时,解析器必须确定这个值是基本类型值还是引用类型值。

基本类型值 指的是简单的数据段,如: Undefined、Null、Boolean、Number 和 String。这 5 种基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值。

引用类型值指 那些可能由多个值构成的对象,并且引用类型的值是保存在内存中的对象,如: Object、Array、Function、Date对象等。与其他语言不同,JavaScript 不允许直接访问内存中的位置, 也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。 为此,引用类型的值是按引用访问的。

三、如何理解按值访问 按引用访问

要说明这个问题需要先来了解一下栈内存和堆内存。
基本类型值是存储在栈中的简单数据段,也就是说,他们的值直接存储在变量访问的位置。
堆是存放数据的基于散列算法的数据结构,在javascript中,引用值是存放在堆中的。

如以下代码:

const a = 1;
const obj1 = {
    name: "小明",
    age: 18
}
const obj2 = obj1

在内存中的表现为:

在变量声明时,基本类型变量会直接在栈内存中为它分配空间,变量值也是直接存储在栈内存中;
而基本类型变量的真实值是存储在堆内存中的,同时栈内存中会保存一个指针,这个指针指向真实值在对内存中的位置,也以理解为栈内存中存放了引用类型值存储的地址。
所以访问基本类型的变量时,是直接访问到栈内存中其真正的值;而访问引用类型的变量时,是通过栈内存中保存的引用地址去访问。所以在上例中,重新声明了一个obj2,并将obj1赋给obj2 其实是将obj1存放的引用赋给了obj2,此时obj1和obj2的指针指向了堆内存中的同一个对象,那么我们更改obj1的name属性时obj2会变化,同样更改obj2的name属性时obj1也会变化。那么此时如果新声明一个变量b = a会怎样呢,因为是基本类型各不影响。

四、栈内存堆内存的区别

栈的优势就是存取速度比堆要快,仅次于直接位于CPU中的寄存器,但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,垃圾收集器会自动地收走这些不再使用的数据,但是缺点是由于在运行时动态分配内存,所以存取速度较慢。

所以相对于简单数据类型而言,他们占用内存比较小,如果放在堆中,查找会浪费很多时间,而把堆中的数据放入栈中也会影响栈的效率。比如对象和数组是可以无限拓展的,正好放在可以动态分配大小的堆中。

*注 : 以下为c++中,对内存与栈内存的区别,很多地方相通,可辅助理解

主要的区别由以下几点:
1、管理方式不同;
2、空间大小不同;
3、能否产生碎片不同;
4、生长方向不同;
5、分配方式不同;
6、分配效率不同;
管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。(js有自己的垃圾回收机制,此条不适用)
空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改。
碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
五、其他变量操作在内存中的表现 及 开发中的注意点

如下例:

var lang = "Java";
lang = lang + "Script";

以上示例中的变量 lang 开始时包含字符串"Java"。而第二行代码把 lang 的值重新定义为"Java" 与"Script"的组合,即"JavaScript"。实现这个操作的过程如下:首先创建一个能容纳 10 个字符的 新字符串,然后在这个字符串中填充"Java"和"Script",最后一步是销毁原来的字符串"Java"和字 符串"Script",因为这两个字符串已经没用了。这个过程是在后台发生的,而这也是在某些旧版本的浏览器(例如版本低于 1.0 的 Firefox、IE6 等)中拼接字符串时速度很慢的原因所在。但这些浏览器后来的版本已经解决了这个低效率问题。
浏览器内核的底层优化尚不清楚,但是有人采用下面的方法进行优化,只看一下方法了解即可,因为浏览器内核优化之后效率已提高不必如此大费周章。提高效率的办法是用数组的join函数:
如:

var str ;
str = "this is a string";
str = str + ",another string.";
var tempArr = [] ,src,res;
src = "this is a string";
tempArr.push(src);
tempArr.push(",another string.");
res = tempArr.join("");

关于开发中需要注意的事项,主要是对于引用类型需要特别注意。
一、需要将原数据 存储副本,并操作数据(如对请求回来的列表数据进行过滤显示)。这是个很常见的问题,基本都遇到过这种情况,应该已经有处理经验。主要就是如果直接将原数据data赋值给新变量a,那么自己操作a时data的数据也会受到影响。解决办法就是避免简单表面的进行存副本操作,应该存一个独立的副本。如果数据源是数组结构就Array.prototype.slice.call(arr), 如果数据源是对象结构进行拷贝,至于深拷贝还是浅拷贝看自己的需要。个人感觉可以形成一种编码习惯,类似场景要操作这样的数据时先存一个独立的副本(当然也要看需求是否需要,自行权衡)。

二、在MVVM框架中会经常出现 如model的某项数据是层数较深的复杂结构时,更改了数据项下的某个值时,view并不更新,因为view model觉得自己没有变化,没有通知view。比如react-redux中,action触发的reducer更新了store下某项深结构数据下的某个值,又比如vue中简单更改data中数组的某一项,都是不会触发view更新的。解决办法react中可少用深结构数据尽量浅,也可以采用深拷贝更新store。vue是框架中做了hack处理,可使用$apply解决,也可以用官方点名的八大数组自带方法处理。

六、总结

本文总结了JS数据类型及其声明赋值更新时在内存堆栈中的表现,可以更深入的理解这些数据类型。并在有更深理解之后来回顾开发中的常见问题,做出总结,欢迎补充。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96464.html

相关文章

  • JavaScript 是如何工作的:JavaScript 的共享传递和按值传递

    摘要:它对数组和对象使用按值传递,但这是在的共享传参或拷贝的引用中使用的按值传参。例如在这里,变量和值在执行期间存储在堆栈中。返回值这是可选的,函数可以返回值,也可以不返回值。变量被推入堆栈,从而在执行时成为的副本。 这是专门探索 JavaScript 及其所构建的组件的系列文章的第 22 篇。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 如果你错过了前面的章节,可...

    keithyau 评论0 收藏0
  • JavaScript 是如何工作的:JavaScript 的共享传递和按值传递

    摘要:它对数组和对象使用按值传递,但这是在的共享传参或拷贝的引用中使用的按值传参。例如在这里,变量和值在执行期间存储在堆栈中。返回值这是可选的,函数可以返回值,也可以不返回值。变量被推入堆栈,从而在执行时成为的副本。 这是专门探索 JavaScript 及其所构建的组件的系列文章的第 22 篇。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 如果你错过了前面的章节,可...

    陈伟 评论0 收藏0
  • JavaScript 是如何工作的:JavaScript 的内存模型

    摘要:调用堆栈是存放原始数据类型的地方除了函数调用之外。上一节中声明变量后调用堆栈的粗略表示如下。解释改变的正确方法是更改内存地址。在声明时,将在调用堆栈上分配内存地址,该值是在堆上分配的内存地址。 这是专门探索 JavaScript 及其所构建的组件的系列文章的第 21 篇。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 如果你错过了前面的章节,可以在这里找到它们:...

    baoxl 评论0 收藏0
  • 【进阶1-3期】JavaScript深入之内存空间详细图解

    摘要:进阶期理解中的执行上下文和执行栈进阶期深入之执行上下文栈和变量对象但是今天补充一个知识点某些情况下,调用堆栈中函数调用的数量超出了调用堆栈的实际大小,浏览器会抛出一个错误终止运行。 (关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导) 本周正式开始前端进阶的第一期,本周的主题是调用堆栈,今天是第3天。 本计划一共28期,每期重点攻...

    coordinate35 评论0 收藏0
  • 翻译连载 | 第 9 章:递归(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS

    摘要:每个函数调用都将开辟出一小块称为堆栈帧的内存。当第二个函数开始执行,堆栈帧增加到个。当这个函数调用结束后,它的帧会从堆栈中退出。保持堆栈帧跟踪函数调用的状态,并将其分派给下一个递归调用迭。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM...

    LeviDing 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<