浅析js的数据类型、堆内存栈内存、深拷贝浅拷贝、js的垃圾回收机制

前言

本身在面试博客里只是想整理一下js的类型,突然越联想越感觉这块的知识体量比较大,扩展很多,但网上的很多理解繁杂还不太清晰,故此专门记录一下这几个点。

正文

js中的数据类型

基本类型:number ,string,null,Boolen,undefined,symbol
引用类型:object (Array,Function,Date,Regxp在es6中规定都是object类型)

  • 两者的区别:
    基本类型:可以直接操作的实际存在的数据段。存在在内存的栈中,比较的是值的比较!
    引用类型:复制操作是复制的是对象的引用,增加操作时是操作的对象本身。存在在堆内存和栈内存中,比较的是引用的比较!

关于这个具体细节特别多可以直接参考:https://juejin.im/post/5cec1bcff265da1b8f1aa08f

堆内存和栈内存

在v8引擎中对js变量的存储主要有两种位置:堆内存和栈内存,以下简称堆、栈。
下面通过两个例子来理解堆和栈使用

//例1
var num1 = 1 
var num2 = "222"
num2 = num1
num2 = '666'
console.log(num1)// 1
console.log(num2)// '666'

这里可以看出上述的基本数据类型,是真实值在比较。那我们在看一下引用数据类型:

//例2
var obj1 = {name:'aa',age:18}
var obj2 = obj1
obj2.name = 'bb'
console.log(obj1) // { name: "bb", age: 18 }
console.log(obj2) // { name: "bb", age: 18 }

这里我们可以看出当obj2改变的时候,obj1也同时一起改变了!为什么会被影响而不像基本数据类型呢?我们来分析一下例2的过程:

var obj2 = obj1时,数据存储的位置如下
浅析js的数据类型、堆内存栈内存、深拷贝浅拷贝、js的垃圾回收机制
obj2.name = 'bb'时,改变的是堆中实际的数据!
浅析js的数据类型、堆内存栈内存、深拷贝浅拷贝、js的垃圾回收机制
所以打印出来的obj1和obj2是相同的。

通过上述例子我们可以得出堆和栈的区别:

  • 栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null和引用数据类型的地址指针,它们都是直接按值存储在栈中的。
  • 堆内存主要用于存储引用类型如对象(Object)、数组(Array)、函数(Function) …,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。
  • 堆是动态分配内存,内存大小不一,也不会自动释放。
  • 栈中每种类型的数据占用的内存空间的大小是确定的,并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。

初步理解了堆和栈,为了更深入理解,下面我们再看一下 深拷贝和浅拷贝

深拷贝和浅拷贝

我浏览了很多文档,对于赋值、深浅拷贝说法不一,很多都是赋值和深浅拷贝分不清楚,在此,特意说明一下赋值和拷贝的区别!

上述例2的过程是一个赋值操作,赋值的只是对象的引用,如上述obj2=obj1,实际上传递的只是obj1的内存地址,所以obj2和obj1指向的是同一个内存数据,所以这个内存数据中值的改变对obj1和obj2都有影响。这个过程是不同于深浅拷贝的!

类型 和原数据是否指向同一地址 原数据只有一层数据 原数据有多层子数据
赋值 新对象改变 使原数据一同改变 改变 使原数据一同改变
浅拷贝 新对象改变 不会 使原数据改变 改变 使原数据一同改变
赋值 新对象改变 不会 使原数据一同改变 改变 不会 使原数据一同改变
  • 深浅拷贝区别:浅拷贝是拷贝一层,深层次的对象级别的就拷贝引用;深拷贝是拷贝多层,每一级别的数据都会拷贝出来;

那怎么才能不引用同一个堆中的数值呢?这就涉及到了其他拷贝方式,我们来实现一下:

1.使用循环实现只复制一层的浅拷贝
//例3
var obj1 = {name:'aa',age:18}
var obj2 = {}
for(const key in obj1){
	obj2[key] = obj1[key]
}
obj2.name = 'bb'
console.log(obj1) // { name: "aa", age: 18 }
console.log(obj2) // { name: "bb", age: 18 }
2.使用手动复制实现只复制一层的浅拷贝
//例4
var obj1 = {name:'aa',age:18}
var obj2 = {
	name:obj1.name,
	age:obj1.age
}
obj2.name = 'bb'
console.log(obj1) // { name: "aa", age: 18 }
console.log(obj2) // { name: "bb", age: 18 }

在例4中,我们再栈内存var obj2 = {}新建了一个地址指针,通过赋值,在堆中复制了name:‘aa’,age:18,obj2指向新的堆内存地址;
浅析js的数据类型、堆内存栈内存、深拷贝浅拷贝、js的垃圾回收机制
obj2.name = 'bb'时obj2指向的内存中的name改变,并不影响obj1中的值,这就实现了一层的深拷贝。
浅析js的数据类型、堆内存栈内存、深拷贝浅拷贝、js的垃圾回收机制

3.通过Object.assign()实现一层的浅拷贝
//例5
let obj1 = { a: { b:'bb1'}, c: 'bb1'}
let obj2 = Object.assign({},obj1)
obj2.a.b = 'bb2';
obj2.c = 'cc2'
console.log(obj1); // { a:{ b: "bb2" }, c: "bb1" }
console.log(obj2); // { a:{ b: "bb2" }, c: "cc2" }

例5中的ES6中的Object.assign方法,如果对象只有一层的话可以使用,其原理和例4相同是:先新建一个空对象,在堆中复制相同的属性,obj2指向另一个内存地址,但是这个方法不能使用在多层深拷贝!

以上都不能实现真正意义上的深拷贝,下面来看深拷贝:

4.通过JSON.parse(JSON.stringify(obj1))实现深拷贝
//例6
var obj1 = { body: { a: 10 } };
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.body.a = 20;
console.log(obj1);// { body: { a: 10 } }
console.log(obj2);// { body: { a: 20 } }
console.log(obj1 === obj2);// false
console.log(obj1.body === obj2.body);// false

这个方法是开发中最常用也是最简单的,哈哈,but 你以为真的这么简单吗?这个深拷贝也是有缺陷的!

JSON.parse(JSON.stringify(obj1))的原理是:通过JSON.stringify(obj1)把obj1转化为字符串,再用JSON.parse把字符串转化为一个新对象来进行拷贝;

  • 这就只能拷贝数据类型,而拷贝不了对象的原型链,构造函数上面的方法或属性;
  • 而且使用这个方法去拷贝的前提是 数据必须是JSON格式,如果你要拷贝的引用类型为:RegExp,function是没有办法实现的!
5.通过例1循环递归赋值实现对象的深拷贝
//例7
let obj1 = {
    name: 'aa',
    age: 18,
    data: {
        mom: '小红',
        else: {
            money: 9999
        }
    }
}
function clone(params) {
    if (typeof params === 'object') {
        let obj2 = {}
        for (const key in params) {
            obj2[key] = clone(params[key])
        }
        return obj2
    } else {
        return params
    }
}
let obj3 = clone(obj1)
obj3.data.mom = '小明'
obj3.age = 60
obj3.data.else.money = 666
console.log(obj1);
//{"name":"aa","age":18,"data":{"mom":"小红","else":{"money":9999}}}
console.log(obj3);
//{"name":"aa","age":60,"data":{"mom":"小明","else":{"money":666}}}

当然,通过递归就能实现深拷贝,但是还是会有很多性能问题,在此就不一一例举了,可以看这篇文章去加深自己的理解:面试所用的深拷贝,我日常开发真的不想这样写的深拷贝!!

浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

const定义的值能改么?

未完待续

上一篇:C++对象之间的赋值运算符


下一篇:集合排序按照时间以及工具类