目录
前言
首先要知道什么是值类型和引用类型,这个基础的知识点如果还不知道就自己先去查下。
例如:
- 值类型:数字、字符、undefined、symbol等。
- 引用类型:对象、函数(也可以单独看作是函数类型)、数组、null(指向空地址)等。
因为我们的深浅拷贝就基于那两大类做的,太基础这里咱就不补充了。
存储方式
值类型
学习时,我们可以把抽象的东西具象化,先了解下值类型在栈内存中的存储方式是怎样的,例如一个字符变量和数字类型变量:
let name = '小马'
let age = 12
key | value |
---|---|
name | 小马 |
age | 12 |
就像表格一样,井然有序。
引用类型
那么引用类型的堆存储咱们也可以来具象化一下(地址写法我随便表示下),例如下面两个对象:
let a = {name: '小马'}
let b = {a: 1}
地址 | value |
---|---|
00xx xxxx xxxx xxxx | {name: '小马'} |
001x xxxx xxxx xxxx | {a: 1} |
然后在栈中变量的对应关系是地址而不是值:
key | value |
---|---|
a | 00xx xxxx xxxx xxxx |
b | 001x xxxx xxxx xxxx |
背后原因
这种存储机制就是为了优化空间和性能:
-
空间:例如,栈的空间可以做到很小,因为引用类型放在栈中只是一个引用地址而已。
-
性能:一般来说值类型的数据量不会很大,但是引用类型可以做到很大,例如十几mb的树结构数据,十几mb的图表数组数据等等,引用类型做拷贝时只是复制地址,所以性能较好。
细说浅拷贝
只要对引用类型进行直接赋值拷贝,都会发生浅拷贝现象。因为引用类型存的其实是堆内存中引用特定某个存储空间的地址,而真正内容在这个地址对应的存储空间上。也就是多个引用类型可能共用一个存储空间。
举例
比如,数组
var arr = [1, 2, 3];
var brr = arr; // 将arr拷贝给brr,也就是指针的拷贝
arr[0] = 'zhangsan'; // 改变arr某个值
console.log(arr, brr); // 都发生了改变
再比如对象
var obj1 = {
a: "hello"
}
var obj2 = obj1; // 引用类型拷贝,都指向同一个原生对象
obj2.a = "world";
console.log(obj1.a, obj2.a) // a都变成"world"
属性赋值呢
当然,这样子把对象的中为基本类型的属性,直接赋值给其他变量者不会发生浅拷贝现象:
let obj = {
a: '1'
}
let a = obj.a
a = '2'
consolo.log(obj.a) // 还是1
但如果是值为引用类型的属性直接赋值给其他变量,就会发生浅拷贝现象了。
来个简单的题开个胃:
var obj1 = {
age:18,
arr:[1,2,3] //object类型 引用类型
}
function copy(obj1){ // 函数:把一个对象里的属性遍历赋值给一个新的对象并返出
var obj2 = {}
for(var k in obj1){
obj2[k]=obj1[k] //万变不离其宗,当把属性值为数组的东西赋值给另一个对象的属性,也是引用
}
return obj2;
}
var obj2 = copy(obj1);
obj1.arr[0]='ooo';
console.log(obj1.arr,obj2.arr) // 请自己思考一下再去打印
注意
obj1={a:1}; obj2={a:1}
这两者虽然值是一样的,但是引用地址是不一样的,所以二者不相等。
细说深拷贝
举例
与浅拷贝相对应的就是深拷贝,是单独开一个堆内存空间存放变量的内容。也就是一个变量对应一个单独的内存空间。
所以引用类型想进行深拷贝,就要对其进行新的内存空间开辟。
举个实现深拷贝函数的例子:
function deepClone(value = {}) {
// 递归中,如果是值类型和函数类型直接返回
if (typeof value !== 'object' || value === null) return value
// 开辟新空间
let result = value instanceof Array ? [] : {}
Object.setPrototypeOf(result, Object.getPrototypeOf(value)) // 原型也拷贝
// 循环进行递归
for (let i in value) {
if (value.hasOwnProperty(i)) result[i] = deepClone(value[i]) // 先判断不是原型上的属性才进行递归操作
}
// 递归返回
return result
}
使用:
let arr = [1, 2, 3, [4, 5]]
let brr = deepClone(arr)
arr[3][0] = 6
console.log(arr[3], brr[3]) // [6, 5] [4, 5]
引用类型作为函数入参问题
问:如果把一个引用类型变量作为实参传入一个函数中,函数内部再对其重新赋值,那么函数外的这个变量的引用地址会被改变吗?
var type = "image";
var size = { width: 800, height: 600 };
var format = ['jpg', 'png'];
function change(type, size, format) {
type = 'video';
size = { width: 1024, height: 768 };
format.push('mp4');
}
change(type, size, format); // 考点:函数的传参也会分析参数的引用类型
console.log(type, size, format); // 思考一下再去运行啊
答:把引用类型当做参数去使用时,如果是重新赋值,那么会为它单独创建一个新的堆内存空间。但如果是修改就还是原来那个变量。
不过,在前端规范中,是不建议重新赋值输入参数的,咱们还是需要重新声明一个变量
重点:如果遇到符合这个情况的函数就不要多写一步深拷贝了,但!如果本身就是个递归函数,涉及到了把赋得的值传入递归函数中,就会形成闭包,需要手动在赋值的哪一步进行深拷贝操作!!!!
例如,一个变量需要不断的重复作为入参调用某个函数,就需要进行深拷贝。
连续赋值能把你弄晕
来道连续赋值的题(以下简称连等)
var a = {n:1}
var b = a
a.x = a = {n:2}
// 请问一下打印什么
console.log(a.x)
console.log(b.x)
是不是打印出来有点蒙?哈哈。
先记住规则:
- 连等是从右向左分析
- 连等赋值之前,程序会把变量的引用都保存起来,连等过程都是假设等于,等到全部假设完了再一起赋值
解释:
容我用ppt画几个图。
首先a变量引用堆内存中的{n:1}
,b变量也引用它。
![](https://img-blog.csdnimg.cn/20210103223736155.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3BhZ256b25n,size_16,color_FFFFFF,t_70)
然后执行到连等 a.x = a = {n:2}
先分析右边第一个等,a = {n:2}
,a假设引用{n:2}
(但是还没真正引用,所以a还是在引用{n:1}
)。
然后再分析a.x = a
,a在引用{n:1}
的内存空间内又假设x:{n:2}
。
![](https://img-blog.csdnimg.cn/20210103224617242.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3BhZ256b25n,size_16,color_FFFFFF,t_70)
最后,两个假设全部成立,此时a引用对象成为{n:2}
,b因为自始至终都引用{n:1}
的那个内存空间,所以b为{n:1,x:{n:2}}
。
![](https://img-blog.csdnimg.cn/20210103224902395.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3BhZ256b25n,size_16,color_FFFFFF,t_70)
够清楚了吧,哈哈。
大家常用的深拷贝方法
有时候,浅拷贝使用得当能很灵活的处理一些问题,但如果所有情况都不加以控制,工程量一大,容易导致数据流出问题,下面举例个人在工作中喜欢使用的深拷贝方法。
对象和数组都可以使用
json转换(不推荐)
let obj = JSON.parse(JSON.stringify(obj)) // json转换,但这种方式如果数据量很大的话,会非常影响性能
这个方式的好处是,无论一维还是多维的对象和数组都是可以深拷贝的。
但也有缺点:例如对象中存在函数属性转换后会直接消失,存在Date类型的属性转换后会变成字符串格式,超长的数字字符串转换完后会有精度损失等。所以使用的时候还是要谨慎。
注意,JSON对象上的这些方法,本身就不是专门用来做拷贝的,平时我们开发时不应该使用
es6的…(一维可用)
let arr = [1, 2, 3, ...brr] // es6语法,只能拷贝一维数组
let obj = {...obj1, ...obj2} // 只能拷贝一维对象
这种拷贝方式个人感觉比Object.assign()
要灵活。
Object.assign(一维可用)
let obj = Object.assign({},obj) // 只能拷贝一维对象
let obj = Object.assign([],arr) // 只能拷贝一维数组
这个方式有副作用,如果把原值放在第一个参数,那么原值就会被改变:
let obj = { a: 1 }
Object.assign(obj, { b: 1 })
console.log(obj) // { a: 1, b: 1 }
拆分拷贝deepClone(必装)
具体看上面深拷贝函数实现的例子,但是我们一般不会就这么简单的用在项目上,会对其内部进行扩展改写,例如支持moment对象。
推荐这样子,一般情况下直接使用lodash里的深拷贝方法:
npm i --save lodash.clonedeep
然后直接使用:
import cloneDeep from 'lodash.clonedeep'
let newObj = cloneDeep(Obj) // 深拷贝 支持组件拷贝
然后对于有特殊对象的例如moment对象,就可以用我们自己封装的深拷贝方法(当然也可以先试试lodash是否可行)
structuredClone(了解)
这是个新原生api(也不新了其实),只是我们开发者跟不上前端的更新,多以大多数人都不清楚哈哈。这个可以提前了解下,使用的时候考虑下兼容性。
数组专用
主要还是数组的api
concat与slice(一维可用)
arr1 = [].concat(arr2) // 只能拷贝一维数组
brr = arr.slice() // 只能拷贝一维数组
注意:通过以上方式进行的深拷贝都不会==
或者 ===
于原来的变量。
深拷贝方法的拓展
咱们对上面手写的那个deepClone函数进行一个知识拓展。假设我有个变量,是个对象,这个对象中的其中一个属性赋值为自己。
let obj = {
a: '1'
}
obj.b = obj
console.log('obj', obj);
你会看到对象属性无限嵌套,这时候你再去用deepClone肯定是不行的,无限递归,控制台给你说栈溢出了。
所以需要weakMap这个对象来帮你
但首先我们要了解两个概念:弱引用和强引用
强引用
强引用是最常见的引用,它是指一个对象被一个变量或函数所引用,该对象就具有了强引用。只要有至少一个强引用存在,对象真正的内存占用就不会被垃圾回收回收。如下面的例子:
let obj = {a: 1}
let obj1 = {
a: obj
}
obj = null
console.log(obj1);
是不是黑人问号???在这个例子中,由于obj1持有了obj对象的引用,obj就具有了强引用,obj的值在内存中一直存在。只有当obj1被设置为null后,该值才会被垃圾回收。
冷静下。其实你仔细想想,这样的设计才是符合直觉的,我们的JS默认的就是强引用。
弱引用
弱引用是指一个对象并不因为被引用而被保留在内存中,如果一个对象只被弱引用所引用,那么它就可以被垃圾回收掉。在JavaScript中,WeakMap是一种基于弱引用的映射类型,它的键是弱引用的,键所引用的对象没有强引用时,就会被垃圾回收掉。
例如:
let obj = {a: 1}
let map = new WeakMap()
map.set(obj, 1)
obj = null
console.log(map.get(obj));
可以看到当obj置为null时,内存中的值确实是被销毁了,所以map里的值也没了。
综上:
WeakMap的一个用法是可以用来避免内存泄漏和循环引用问题。在处理需要被回收机制管理的大量数据时,使用WeakMap可以更好地管理程序的性能和内存。它解决了一些应用场景下无法使用普通JavaScript对象的弊端,同时兼顾了性能和开发效率。
把深拷贝方法完善一下
function deepClone(value, map = new WeakMap()){
if (typeof value !== 'object' || value === null) return value
// 如果已经有过引用了,直接返回弱引用的值
if (map.get(value)) {
return map.get(value)
}
let obj = Array.isArray(value) ? [] : {}
Object.setPrototypeOf(obj, Object.getPrototypeOf(value)) // 原型也拷贝
map.set(value, obj) // 每个对象类型的值都存入WeakMap中
for(let key in value) {
// 可以多个hasOwnProperty的判断
obj[key] = deepClone(value[key], map) // 记得把WeakMap也递归
}
return obj
}
这样深拷贝出来的虽然也是无限嵌套对象,但起码不会内存溢出了。
拷贝不是引用传递
什么叫引用传递,例如:
var arr = [1, 2, 3];
var brr = arr;
arr和brr共用一个内存空间才叫引用传递,而我们的JS是给两个变量都创建了独立的栈内存空间,然后引用了相同的堆内存地址而已!
JS唯一有引用传递的是模块化的导入导出,导入后的东西和js源文件里的东西是同一个内存里的东西,所以你会发现你一旦修改了导入的东西,js源文件里的对应地方就会被改变。