深拷贝和浅拷贝?
一、对浅拷贝与深拷贝的理解
浅拷贝和深拷贝是针对引用数据类型而言的,对于基本数据类型是没有深浅拷贝的概念。
二、从存储的角度理解
js的基本数据类型(String,Number,Boolean,null,Undefined)是存在栈内存的,当发生赋值b=a时会直接在栈内存中开辟一个新空间。a和b是两块独立的空间。修改b不会影响a的值。
而js对于引用数据类型object。存储时会在栈内存存储引用(堆内存中存值的地址),堆内存存储真正的值,栈内存中的引用指向堆内存的值。
当发生 = 的赋值操作 b = a 时,实际上只是把a的引用复制给了b,也就是说a和b指向的堆内存中同一片地址空间。像这种只拷贝引用的就是浅拷贝
而深拷贝就是在堆内存中开辟一块新的空间,把原来对象拷贝对象的值复制到新的存储空间,引用指向新的存储空间。这样a和b就是两个完全不同的对象。修改b的时候不会同步修改a的值。
手写实现深拷贝和浅拷贝
深拷贝:
json:对于可以直接转成json的object类型。可以通过先将object转成json字符串(JSON.stringify)然后解析(JSON.parse)实现深拷贝。
const obj = { name: 'dd', age: 18, say: { text: 'hello' } } const str = JSON.stringify(obj) let obj2 = JSON.parse(str) obj2.say.text = 'hi' console.log(obj); //{ name: 'dd', age: 18, say: { text: 'hello' } } console.log(obj2); //{ name: 'dd', age: 18, say: { text: 'hi' } }
JSON.stringify 实现深拷贝有些地方需要注意:
- 拷贝的对象的值如果有函数,undefined,symbol 这几种类型,经过 JSON.stringify 序列化后字符串中这个键值对会消失。
- 拷贝 Date 类型会变成字符串
- 无法拷贝不可枚举的属性
- 无法拷贝对象原型链
- 拷贝 RegExp 引用类型会变成空对象
- 对象中含有 NaN、infinity 以及 -infinity,JSON 序列化后的结果变成 null
- 无法拷贝对象的循环应用,即对象成环(obj[key]=obj)
const obj3 = { func: function() {console.log(1)}, obj: { name: 'h' }, arr: [1,2,3], und: undefined, ref: /^123$/, date: new Date(), NaN: NaN, infinity: Infinity, sym: Symbol(1) } console.log(JSON.parse(JSON.stringify(obj3))) // NaN: null // arr: (3) [1, 2, 3] // date: "2021-01-29T16:09:10.788Z" // infinity: null // obj: // name: "h" // ref: {}
从上面代码中可以到例如:function 和 undefined 都消失了。所以,JSON.stringify 实现深拷贝,还是有很多无法实现的功能,但如果只是基础普通的对象类型,使用 stringify 还是非常快捷方便的。
递归
-
优点
-
可以解决JSON方式忽略属性值为undefined和function的属性的问题
-
对象存在循环引用时仍然会爆栈
function deepCopy (obj) { const target = {} for (let key in obj) { if (obj.hasOwnProperty(key)) { // key为对象自身可枚举的属性 if ( Object.prototype.toString.call(obj[key]) === '[object Object]' ) { // 属性值为对象,递归调用 target[key] = deepCopy(obj[key]) } else { target[key] = obj[key] } } } return target } const objtest = { func: function() {console.log(1111)}, obj: { name: 'dd' , data: { fn: function() { console.log('data') }, child: 'child' }}, arr: [1,2,3], und: undefined, ref: /^123$/, date: new Date(), NaN: NaN, infinity: Infinity, sym: Symbol(222) } console.log(deepCopy(objtest)); // func: [Function: func], // obj: { name: 'dd', data: { fn: [Function: fn], child: 'child' } }, // arr: [ 1, 2, 3 ], // und: undefined, // ref: /^123$/, // date: 2022-04-21T02:06:50.712Z, // NaN: NaN, // infinity: Infinity, // sym: Symbol(222)
真正的深拷贝解决上述的几个问题的思路:
- 如果是 Date,RegExp 直接创建一个新的实例并返回
- 如果是循环引用就用 weakMap 解决。
- 原型的上的方法,可以结合 Object.getOwnPropertyDescriptors 获取对象上的所有属性及特性及 Object.getPrototypeOf 原型上的方法和 Object.create 结合使用,创建一个新对象,并继承传入原对象的原型链。
- 对象的不可枚举属性以及Symbol 类型,可以使用 Reflect.ownKeys 方法。
浅拷贝
Object.assign()
ES6 中 Object 的一方法,可以是来合并多个JS对象(能用来实现浅拷贝) 第一个参数拷贝的目标对象,后面的参数是拷贝的来源对象
语法
Object.assign(target, ...sources)
注意: Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。 使用 Object.assign 方法注意以下几点:
- 不会拷贝对象的继承属性
- 不会拷贝对象的不可枚举属性
- 可以拷贝 Symbol 类型的属性
扩展运算法
利用扩展运算法,可以实现浅拷贝的的功能。
拷贝对象
let obj = {a: 1, b: 2} let obj1 = {...obj} obj1.b = 3 console.log(obj1)// {a: 1, b: 3} console.log(obj)// {a: 1, b: 2}
拷贝数组
-
能利用原生的数组的方法实现浅拷贝的有好多个
-
concat 拷贝数组
let arr = [1,2,3] let arr1 = arr.concat() arr1[0] = 5 console.log(arr1) // [5, 2, 3] console.log(arr) // [1, 2, 3]
slice 拷贝数组
let arr = [1,2,3] let arr2 = arr.slice() arr2[2] = 4 console.log(arr2) //[1, 2, 4] console.log(arr) // [1, 2, 3]
Array.from 拷贝数组
let arr = [1,2,3] let arr4 = Array.from(arr) arr4.push(1) console.log('arr4', arr4) // [1,2,3,1] console.log(arr) // [1,2,3]
实现浅拷贝手写浅拷贝的思路:
基础类型做最基本的赋值就可。
引用数据类型,需要开辟一个新的存储,并拷贝一层对象属性。
function shallowCopy(target) { // 引用类型需要开辟一个新的存储地址 if (typeof target === 'object' && target !== null) { const copy = Array.isArray(target) ? [] : {} for (const prop in target) { if (target.hasOwnProperty(prop)) { copy[prop] = target[prop] } } return copy } // 如果是基础类型就直接返回 return target }