7

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


baozw
411 声望5 粉丝