小言_互联网的博客

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

286人阅读  评论(0)

前言

本身在面试博客里只是想整理一下js的类型,越联想越感觉这块的知识体量大,而且都是相关联的只是,但网上的现有的很多博客繁杂还不太清晰,故此专门记录一下这几个点。

正文

js中的数据类型

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

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

这里只特别关注一下es6新增的类型:symbol

symbol

其他类型的具体细节也特别多,不一一列举了,可以直接参考:
【JS 进阶】你真的掌握变量和类型了吗

可以分辨数据类型之后,我们再看一下在javascript中存储数据的地方:堆内存和栈内存

堆内存和栈内存

在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时,数据存储的位置如下

obj2.name = 'bb'时,改变的是堆中实际的数据!

所以打印出来的obj1和obj2是相同的。

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

  • 栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Nul 和引用数据类型的地址指针,它们都是直接按值存储在栈中的。
  • 堆内存主要用于存储引用类型如对象(Object)、数组(Array)、函数(Function) …,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。

为什么基本数据类型保存在栈中,而引用数据类型保存在堆中?

  • 堆比栈大,栈比堆速度快;
  • 基本数据类型比较稳定,而且相对来说占用的内存小;
  • 引用数据类型大小是动态的,而且是无限的,引用值的大小会改变,不能把它放在栈中,否则会降低变量查找的速度,因此放在变量栈空间的值是该对象存储在堆中的地址,地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响;
  • 堆内存是无序存储,可以根据引用直接获取;

初步理解了堆和栈,那我们来思考一下,计算机的内存就那么大,假设我们一直往里面存东西而不取的话,内存会满吗?如果满了怎么办?

答案肯定是会满啊!为了不让它满,这里就要提一下js的垃圾回收机制

js的垃圾回收机制

javaScript的内存管理不同于其他语言,程序员本身不可以操作而是由系统全自动回收~当然我们平时码字的时候不用管它,但是如果代码有问题的话,还是会造成内存泄漏,长时间积攒下来最终导致内存溢出(就是满了 ~没地儿存了)

通俗理解什么是内存溢出和内存泄漏?

  • 内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
  • 内存泄漏是指你向系统申请分配内存进行使用,可是使用完了以后却不归还(删除),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。

那js怎么全自动回收的?——JavaScript垃圾回收机制

JavaScript垃圾回收机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收系统(GC)会按照固定的时间间隔,周期性的执行。

var a = 'test11'
var b =  'test222'
var a = b 

这就是简单的回收,“test11”这个字符串失去了引用(之前是被a引用),系统检测到这个事实之后,就会释放该字符串的存储空间以便这些空间可以被再利用。针对这个解释,为方便理解,提出两个问题:
1. 什么才算是不再使用的变量?
js对这类变量有两种定义方式:标记清除法和引用计数法;

常用的是标记清除,它的过程可以分为几步:

  1. 当存储在内存中的所有变量进入环境时,都被打上标记 “进入环境”
  2. 代码继续执行。。。
  3. 然后去掉环境中使用的变量,去掉被环境中的变量引用的标记。
  4. 代码继续执行。。。
  5. 当变量离开环境执行完时,则将其标记为“离开环境”。
  6. 这些被加上“离开环境”标记的变量将被视为准备删除的变量,环境中其他的变量已经无法访问到这些变量了。
  7. 最后垃圾收集器销毁那些带标记的值,并回收他们所占用的内存空间。

引用计数是指跟踪记录每个值被引用的次数,语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。

  1. 当声明了一个变量并将一个引用类型值(function object array)赋给该变量时,则这个值的引用次数就是1。
  2. 如果同一个值又被赋给另一个变量,则该值的引用次数加1。
  3. 相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。
  4. 当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。
  5. 当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。

如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。

2.这个周期性怎么定义?

递归太深会导致栈溢出!

初步理解了堆和栈,为了加深深入理解,下面我们再看一下 深拷贝和浅拷贝中的应用

深拷贝和浅拷贝

我浏览了很多文档,对于赋值、深浅拷贝说法不一,很多都是赋值和深浅拷贝分不清楚,在此,特意说明一下赋值和拷贝的区别!专门查了书,发现浅拷贝这里的东西没有具体定义,但是我更认可:引用类型的 赋值不等于浅拷贝浅拷贝是一层数据的拷贝,深拷贝是多层的拷贝这个观点。

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

MDN中数组的slice方法中有这句话 ,slice不会修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。原数组的元素会按照下述规则浅拷贝…那就用slice来验证一下什么是浅拷贝:

var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[0] = 2;
b[3].x = 2
console.log(a); // [ 1, 3, 5, { x: 2 } ];
console.log(b); // [ 2, 3, 5, { x: 2 } ];

这里b[0] = 2;时候a[0]没有随着改变,b[3].x = 2时候a[3].x发生了变化。

MDN中splice的规则:
。。
如果该元素是个对象引用 (不是实际的对象),slice会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
。。
对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

  • 综上:浅拷贝:新的数据复制了原数据中 非对象属性的值对象属性的引用,也就是说对象属性并不复制到内存,但非对象属性的值却复制到内存中。
  • 而深拷贝会另外拷贝一份一个一模一样的对象,从堆内存中开辟一个新的区域存放新对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
类型 和原数据是否指向同一地址 原数据只有一层数据 原数据有多层子数据
赋值 新对象改变 使原数据一同改变 改变 使原数据一同改变
浅拷贝 新对象改变 不会 使原数据改变 改变 使原数据一同改变
赋值 新对象改变 不会 使原数据一同改变 改变 不会 使原数据一同改变

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

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指向新的堆内存地址;

obj2.name = 'bb'时obj2指向的内存中的name改变,并不影响obj1中的值,

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指向另一个内存地址,但是这个方法不能使用在多层深拷贝!

5.Array.prototype.slice实现浅拷贝
//例6
var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[0] = 2;
b[3].x = 2
console.log(a); // [ 1, 3, 5, { x: 2 } ];
console.log(b); // [ 2, 3, 5, { x: 2 } ];

上面已经解析过了,不再解析。

6.Array.prototype.concat实现浅拷贝
//例7
let array = [{a: 1}, {b: 2},666];
let array1 = [{c: 3},{d: 4}];
let array2= array.concat(array1);
array1[0].c= 123;
array[0].a = 456
array[2] = 999
console.log(array);// { a: 456 },{ b: 2 },999]
console.log(array1);// [ { c: 123 }, { d: 4 } ]
console.log(array2);// [ { a: 456 }, { b: 2 },666 { c: 123 }, { d: 4 } ]

这里array2就只实现了一层的拷贝,数值类型被复制,所以array[2] = 999时候array2[2] = 666没有改变,但是array2和array内层的对象引用地址相同,所有array[0].a = 456的时候array2也跟着变化了。

7.使用es6的扩展运算符 … 实现浅拷贝
//例8
let obj1 = [{ b:1}, 2]
let obj2 = [...obj1]
obj2[0].b = 'bb2';
obj1[1] = 3
console.log(obj1); // [{ b: "bb2" }, 3]
console.log(obj2); // [{ b: "bb2" }, 2]

扩展运算符只能用在可迭代的对象上,不会改变原数组,只会返回一个浅拷贝了原数组中的元素的一个新数组。

3.通过第三方库Lodash来实现浅拷贝

官网:https://www.lodashjs.com/

var objects = [{ 'a': 1 }, { 'b': 2 }];

var shallow = _.clone(objects);
console.log(shallow[0] === objects[0]);

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

1.通过JSON.parse(JSON.stringify(obj1))实现深拷贝
//例9
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是没有办法实现的!
2.通过例1循环递归赋值实现对象的深拷贝
//例10
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}}}

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

3.通过第三方库Lodash来实现深拷贝

官网:https://www.lodashjs.com/

var objects = [{ 'a': 1 }, { 'b': 2 }];
 
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);//false

这个库还是很好用的,简单操作。

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

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

    区别:浅拷贝是一层数据的拷贝,深拷贝是多层的拷贝

const定义的值能改么?

未完待续
参考链接:js垃圾回收机制JS深拷贝和浅拷贝的实现


转载:https://blog.csdn.net/weixin_43216105/article/details/101024025
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场