荥阳市论坛

注册

 

发新话题 回复该主题

深入理解js数据类型与堆栈内存 [复制链接]

1#
北京中科医院爆光 http://baidianfeng.39.net/a_bdfys/161223/5153139.html
前言

在JavaScript中,它的内存分为三种类型:代码空间、栈空间、堆空间,其中代码空间用于存放可执行代码。

本文带大家来深入理解下栈空间与堆空间(堆内存与栈内存),欢迎各位感兴趣的开发者阅读本文。

理解数据类型

最新的ECMAScript标准定义了9种数据类型/p>6种原始类型,使用typeof运算符检查undefined:typeofinstance==="undefined"Boolean:typeofinstance==="boolean"Number:typeofinstance==="number"String:typeofinstance==="stringBigInt:typeofinstance==="bigint"Symbol:typeofinstance==="symbol"null:typeofinstance==="object"Object:typeofinstance==="object",任何构造函数对象实例的特殊非数据结构类型,也用做数据结构:newObject,newArray,newMap,newSet,newWeakMap,newWeakSet,newDate,和几乎所有通过new关键词创建的东西。Function:非数据结构,尽管typeof操作的结果是:typeofinstance==="function"。这个结果是为Function的一个特殊缩写,尽管每个Function构造器都由Object构造器派生。

typeof操作符的唯一目的就是检查数据类型,如果我们希望检查任何从Object派生出来的结构类型,使用typeof是不起作用的,因为总是会得到"object"。检查Object种类的合适方式是使用instanceof关键字。但即使这样也存在误差。

动态类型

JavaScript是一种弱类型或者说动态语言。我们不需要提前声明变量的类型,在程序运行过程中,类型会被自动确定。这也意味着我们可以使用同一个变量保存不同类型的数据:

varinfo="字符串类型";//string类型info=20;//number类型info=true;//boolean类型隐式转换+和-运算符转换

console.log("20"+6)//""字符串拼接string+number=stringconsole.log("16"-6)//10减法运算string-number=number比较运算符

//==(等于),会自动转换数据类型再比较//===(严格等于),不会自动转换数据类型,如果数据类型不一致,返回false;如果一致,再比较。false==0;//truefalse===0;//falseundefined==null;//true,(undefined是null的子集)NaN(NotaNumber)这个特殊的Number与所有其他值都不相等,包括它自己:

NaN===NaN;//falseisNaN(NaN);//true(isNaN()函数用于判断NaN)浮点数相等比较

1/3===(1-2/3);//false//浮点数在运算过程中会产生误差,因为计算机无法精确表示无限循环小数。要比较两个浮点数是否相等,只能计算它们之差的绝对值,看是否小于某个阈值Math.abs(1/3-(1-2/3))0.;//true包装对象

在JavaScript中,一切皆对象。Array(数组)和Function(函数)本质上都是对象,就连三种原始类型的值——Number(数值)、String(字符串)、Boolean(布尔值)——在一定条件下,也会自动转为对象,也就是原始类型的包装对象。

一般来说,只有对象是可以对属性进行读写操作的,但是我们平常用的很多的字符串方法和属性,都是通过.操作符访问的,例如:

console.log("神奇的程序员".length);console.log("我是大白".indexOf("白"));

如上述代码所示,在我们调用这些方法和属性时,JS内部已经隐式地帮我们帮创建了一个包装对象了,上述代码JS在运行时会处理成这样:

console.log(newString("神奇的程序员").length);console.log(newString("我是大白").indexOf("白"));

浏览器自己隐式创建的包装对象和我们显式创建的包装对象不严格相等,我们举个例子说明下:

varname="神奇的程序员";varinfo=newString("神奇的程序员");console.log(name==info);//trueconsole.log(name===info);//false

运行结果如下:

image-类型检测

接下来我们来学习下js中几个常用的类型检测方法。

typeof运算符

typeof可以检测变量的数据类型,返回如下6种字符串number、string、boolean、object、undefined、function

我们举个例子说明下:

varage=1;console.log(typeofage);//numbervarinfo=undefined;console.log(typeofinfo);//undefinedvartitle=null;console.log(typeoftitle);//object,(null是空对象引用/或者说指针)。varobj=newObject();console.log(typeofobj);//objectvararr=[1,2,3];console.log(typeofarr);//objectvarfn=function(){}console.log(typeoffn);//function

运行结果如下:

image-instanceof运算符

instanceof,用于检测某个对象的原型链是否包含某个构造函数的prototype属性。

instanceof适用于检测对象,它是基于原型链运作的。

instanceof除了适用于任何object的类型检查之外,也可以用来检测内置对象,比如:Array、RegExp、Object、Functioninstanceof对基本数据类型检测不起作用,主要是因为基本数据类型没有原型链。

我们举个例子来说明下:

console.log([1,2,3]instanceofArray);//trueconsole.log(/abc/instanceofRegExp);//trueconsole.log({}instanceofObject);//trueconsole.log(function(){}instanceofFunction);//true

运行结果如下:

image-constructor属性

构造函数属性,可确定当前对象的构造函数,我们举个例子说明下:

varo=newObject();console.log(o.constructor==Object);//truevararr=newArray();console.log(arr.constructor==Array);//true

运行结果如下:

image-hasOwnProperty属性

判断属性是否存在于当前对象实例中(而不是原型对象中),我们举个例子来说明下:

constinfo={title:"书",name:"大白"};console.log(info.hasOwnProperty("title"));//true

运行结果如下:

image-堆栈内存空间

接下来,我们看下什么是堆、栈内存空间。

栈内存空间

见名知意,栈内存空间就是用栈作为数据结构在内存中所申请的空间。

对栈这种数据结构不了解的开发者,请移步我的另一篇文章:数据结构:栈与队列。

我们来回顾下栈的特点:

后进先出,最后添加进栈的元素最先出。访问栈底元素,必须拿掉它上面的元素。

我们画个图来描述下栈,如下所示:

image-堆内存空间

同样的,见名知意,堆内存空间就是用堆作为数据结构在内存中所申请的空间。

对堆这种数据结构不了解的开发者,请移步我的另外两篇文章:数据结构:堆、实现二叉堆

通常情况下,我们所说的堆数据结构指的是二叉堆,我们来回顾下二叉堆的特点:

它是一颗完全二叉树二叉堆不是最小堆就是最大堆

我们画个图来描述下最大堆与最小堆,如下所示:

image-变量类型与堆栈内存的关系基本数据类型

我们知道JS的基本数据类型有7种:

stringnumberbooleannullundefinedsymbolbigInt

基本数据类型变量保存在栈内存中,因为基本数据类型占用空间小、大小固定,通过值来访问,属于被频繁使用的数据。

接下来,我们通过一个例子来讲解下,基本数据类型在栈内存中的存储:

letname="大白";letage=20;

上述代码中,我们定义了2个变量:

name为string类型age为number类型

我们画个图来描述下它在栈内存的存储:

image-

注意??:闭包中的基本数据类型变量是保存在堆内存里的,当函数执行完弹出调用栈后,返回一个内部函数的一个引用,这时候函数的变量就会转移到堆上,因此内部函数依然能访问到上一层函数的变量。

引用数据类型

除了上个章节提到的基本数据类型外,其他的都属于引用数据类型,例如:Array、Function、Object等。

引用数据类型存储在堆内存中,引用数据类型占据空间大、大小不固定,如果存储在栈中,将影响程序的运行性能。

引用数据类型会在栈中存储一个指针,这个指针指向堆内存空间中该实体的起始地址。

当解释器寻找引用值时,会先检索其在栈中的地址,取得地址后,从堆中获得实体。

我们举个例子来描述下上述话语:

//基本数据类型-栈内存letname="大白";//基本数据类型-栈内存letage=20;//基本数据类型-栈内存letinfo=null;//对象指针存放在栈内存中,指针指向的对象放在堆内存中letmsgObj={msg:"测试",id:5};//数组的指针存放在栈内存中,指针指向的数组存放在堆内存中letages=[19,22,57]

上述代码中:

我们创建了两个变量msgObj、ages,他们的值都是引用类型(object、array)堆内存空间采用二叉堆作为数据结构,msgObj与ages的具体值会存在堆内存空间中存储完成后,堆内存空间会返回这两个值的引用地址(指针)拿到引用地址后,这个引用地址会和它的变量名对应起来,存放在栈内存空间中在查找变量msgObj与ages的具体值时,会先从栈内存空间中获取它的引用地址获取到引用地址后,通过引用地址在堆内存空间的二叉堆中查找到对应的值。

我们画个图来描述下上述话语,如下所示:

堆内存空间中的Object,表示的是存储在空间中的其他对象的引用值。

image-

我们来理解下堆内存空间与堆内存的区别:

堆内存空间:相当于一个采用二叉堆作为数据结构的容器。

堆内存:指的是一个引用类型的具体值。

堆内存存在于堆内存空间中。

变量复制

接下来,我们从内存角度来看下变量复制。

基本数据类型的复制

我们通过一个例子来看下基本类型的复制,代码如下所示:

letname="神奇的程序员";letalias=name;alias="大白";

上述代码中:

name、alias都是基本类型,它们的值存储在栈内存。它们分别有各自独立的栈空间因此,修改alias的值,name不受影响

我们画个图来描述下:

image-37引用数据类型的复制

接下来,我们通过一个例子来看下引用类型的复制,代码如下所示:

letbook={title:"书",id:12}letinfo=book;info.title="故事书";console.log(book.title);//故事书

上述代码中:

info、book都是引用类型,它们的引用存在栈内存,值存在堆内存它们的值指向同一块堆内存,栈内存中会复制一份相同的引用

我们画个图来描述下:

image-深拷贝与浅拷贝

通过上述章节的学习,我们了解到引用数据类型在复制时,改了其中一个数据的值,另一个数据的值也会跟着改变,这种拷贝方式我们称为浅拷贝。

在实际开发中,我们希望引用类型复制到新的变量后,二者是独立的,不会因为一个的改变而影响到另一个。这种拷贝方式就称为深拷贝。

深拷贝,实际上就是重新在堆内存中开辟一块新的空间,把原对象的数据拷贝到这个新地址空间里来,通常来说,我们有两种方法:

转一遍JSON再转回来,但是这个办法有一个问题,这只能转化一般常见数据,function,undefined等类型都无法通过这种变回来手动去写循环遍历

我们来看下第一种方法,代码如下所示:

constdata={name:"大白"};constobj=JSON.parse(JSON.stringify(data));obj.age=20;console.log("data=",data);console.log("obj=",obj);

运行结果如下:

image-

最后,我们来看下第二种写法,代码如下所示:

constdata=[{name:"大白"}];letobj=data.map(item=item);obj.push({name:"神奇的程序员"});console.log("data=",data);console.log("obj=",obj);

运行结果如下:

image-代码地址

本文为《JS原理学习》系列的第4篇文章,本系列的完整路线请移步:JS原理学习(1)》学习路线规划

本系列文章的所有示例代码,请移步:js-learning

写在最后

至此,文章就分享完毕了。

我是神奇的程序员,一位前端开发工程师。

如果你对我感兴趣,请移步我的个人网站,进一步了解。

分享 转发
TOP
发新话题 回复该主题