Javascript变量类型之值类型与引用类型

311 阅读8分钟

前言

变量类型在我们日常开发中经常接触到,但是js中的变量类型与其他强类型语言不同,由于js是弱类型语言,因此他的变量拷贝在我们实际的日常开发中有很多需要注意的项。而半斤在最近的开发中遇到了很多匪夷所思的问题,很多都与变量类型中的引用类型有关,所以想写一篇文章来总结概括一下。作为笔记,也分享给小伙伴们。

值类型与引用类型

按照惯例,先上代码。

let a = 2;
const b = a;
a = 3;
console.log("b:", b)  // b:2

小伙伴想必一定能一眼看出来这个打印的结果。这是非常符合我们的直观逻辑的。那么我们来看下下面这段代码。

const dabai = {
    age: 23,
    hobby: "sing",
    home: {
        city: "shanghai"
    }
}

const dabaiCopy = dabai;
dabaiCopy.home.city = "hangzhou";
console.log("dabai:", dabai);

大家知道打印的结果吗?

在这里插入图片描述

对的,一眼看上去是不是很违反常理,这里的dabai的home属性下的city属性竟然是hangzhou。我们上一步的操作明明是修改的dabaiCopy的home.city属性修改为hangzhou。而不是dabai这个对象。别急,我们继续往下看。

首先我们需要先了解一下两种变量类型,值类型与引用类型。小伙伴们可能对这两种类型感觉有点陌生?但有没有想过为什么会有这两种类型会存在呢?来来来,跟随skr~的脚步,走~你。

javascript中的数据类型

大家都知道,js目前是有8种数据类型,其中有7种基本类型是Boolean,Null,Undefined,Number,Bigint, String和Symbol.还有一种复杂类型Object。而其中Boolean,undefined,string,Symbol,number,Bigint类型是值类型,而Object,Array(可以看成一种特殊的对象类型),特殊的引用类型null,和function(不用于存储数据)。这些都可以看成为对象类型。他们的变量类型其实都是引用类型。那为什么我们要区分开来这两种类型呢?来,我们不妨先思考一个小问题。

假设我们需要拷贝一个硬盘里的文件到另一个硬盘中,如果拷贝的文件中有一个5kb小文件,还有一个5tb的大文件,那么我们如何去备份呢?那么我们又如何可以尽量的减少拷贝时的空间占用呢?不急,我们下面会揭晓答案。不过在这之前,我们先来聊聊值类型和引用类型。

值类型

我们要了解值类型,首先我们知道值类型的一些特点。

值类型的特点:占用空间固定,并保存在栈中。当执行一个函数时,js引擎会开辟一个内存栈来存储这个函数中所有的变量。所以当我们进行拷贝等操作的时候,js引擎会在栈中对相应的变量进行复制,并创建一个新的变量来存储,这样两个变量之间就没有任何关联。就好像我们把文件从一个硬盘拷贝到另外一个硬盘中,这两个硬盘中的同名文件没有任何关联。是两个独立的文件。

下图演示了怎么拷贝值类型的变量时,我们的内存栈中的变化。

从上面两图中,我们可以看到,当我们执行const b = a的时候,栈内存中开辟了新的内存空间来存储我们的变量b,并将a的值2赋给了b的value。所以我们可以看到a和b已经是两个不同的变量了。他们存储在不同的内存空间中,所以他们之间互不关联,更改其中一个变量的值也不会影响另外一个变量的值。

引用类型

在聊引用类型之前,让我们先回到之前提出的那个问题?如果我们要拷贝一个大文件从一个硬盘到另一个硬盘,我们应该怎么去做?你能想到什么高效的方法可以节省硬盘的存储空间吗?

聪明的你一定想到了百度网盘这一类云盘。对的,云盘可以帮助我们解决硬盘中拷贝文件太大,占用硬盘空间太大的问题。把大文件备份在云端的服务器中,当我们需要的时候,只需要一个云盘的分享链接就可以从服务器端进行拉取,得到大文件中的内容。这样就可以解决大文件的占用硬盘空间问题。

好的,理解了上面的小问题后,让我们来看看什么是引用类型?

引用类型的特点与它的存储方式有关,js引擎在存储这类变量的时候,会开辟一个足够大的内存空间,顶部是栈,底部是堆。栈会把新变量逐渐向下存储,而堆会向上存储。但不用担心会在中间重复,js引擎会预留并处理好这个空间,如下左图。

当我们执行const a = { name: "dabai"}的时候,分配的内存空间中的顶部的栈会存储一个key为a, value为keya的变量。其中keya表示堆中变量的地址,从上面左边的图中可以看到,在底部的堆中keya对应的value就是我们要存储的值{name: "dabai"}。而当我们进行赋值操作的时候,就是右边的图,执行const b = a的时候,js引擎会在栈中新开辟一个用来存储变量b的空间,然后将a的value拷贝到b的value属性。所以a和b变量存储的是同一个内存地址keya,指向同一个堆地址keya。而keya的value存储的是{name: "dabai"}。

当我们去修改对象a 的值的时候,比如执行a.name = "banjin",那么更改的其实是存储在堆内存中的keya对应的value:{name: "dabai"} --> {name: "banjin"},而b对象的value(keya)其实是没有更改的,也对应着堆里的keya。所以这时候打印b的值会打印出{name: "banjin"}。如下图所示过程。

在这里插入图片描述

值类型与引用类型的区别

看到这里,相信小伙伴已经大概理解了值类型与引用类型的区别了,我们来概括一下,值类型是保存在内存栈中的一对键值对,对其进行拷贝的时候,js引擎会新建一个变量,然后将值完全拷贝过去,新变量和旧变量之间没有关联。而引用类型则是在栈中保存着key(变量名)和value(堆中变量存储的地址)。而栈中的value对应着堆中的key(地址)两者是相等的,而堆中保存的value是实际存储的值。所以当我们更改被拷贝(a)的变量的时候,拷贝的变量(b)也会一起被更改。因为两者指向同一个堆里的值。

如果上面的引用类型难以理解,那么我们可以想象一下云盘。当我们使用云盘进行分享的时候,可以将链接发送给别人,也就是保存引用类型时,栈中的value值。这个链接对应着云盘中的某个资源,比如说某个5tb的大视频。也就是堆中保存的value。那么所有拿到链接的人都可以获得我在云盘中存储的5tb大视频。而如果我将云盘的这个5tb的视频进行更改,链接依然不变,那其他人重新下载的资源就变成了我更换的视频了。如下图

在这里插入图片描述

那么为什么需要区分值类型和引用类型呢?

这个问题就需要回到之前提到的关于云盘的问题,云盘的出现解决了很多问题。但是我们就存储大文件这一项进行讨论,我们只需要拥有一个链接就可以通过网络下载存在云盘中的大文件,可以实现多个本地电脑在需要的时候可以拷贝云盘上的同一份文件,而不是所有电脑都拷贝一份,这样会节省大量的硬盘空间。所以对于大文件我们一般采用云盘备份,这样可以有效的减少硬盘占用。而小文件直接保存本地就好,方便。

那换而言之,回到变量类型中来,值类型大多是简单类型,占用的内存小,而引用类型则是复杂对象类型(一个对象类型通常会包含很多个基本类型)。通常会占用很大的内存,对这样的内存进行多份拷贝其实是很损耗内存空间的,而且高频率的赋值拷贝会带来性能问题。所以js引擎对这两种类型区分开来。他们的拷贝方式也是区分开来了。这样就可以有效的提高性能与大大减低内存空间的损耗。

希望各位小伙伴看到这里可以点个赞,多多交流。走 你~。