目录
1、引言
2、深拷贝与浅拷贝介绍
2.1、概念
2.2、实现方式
3、手写代码
1、引言
要了解浅拷贝与深拷贝,首先要知道 堆 和 栈 的概念
堆栈: 就是存放数据的地方(不管是定义的数字、字符串、对象还是数组、函数等等,都会在堆或栈中开辟内存空间存放该变量。 而保存在栈内存的必须是大小固定的数据,引用类型的大小不固定,所以只能保存在堆内存中)
基本数据类型(存放在栈中):number、string、boolean、null、undefined
复杂数据类型(存放在堆中):对象、数组、函数
如上图所示,内存空间中存在内存栈和内存堆。如果是基本类型,直接存储它们的值,而如果是引用类型,会在内存堆中开辟空间进行存储,而栈中变量值其实是堆的地址,指向堆中的这个数据
JS的变量类型分为 基本类型 与 引用类型
而深拷贝与浅拷贝的区别,其实主要是针对于 引用类型的
对于基本类型的复制,只是对它们的值进行拷贝;
2、深拷贝与浅拷贝介绍
2.1、概念
浅拷贝: 创建一个新对象,这个新对象有着原始对象属性值的一份拷贝。
- 如果属性是基本类型,拷贝的就是基本类型的值。
- 如果属性是引用类型,拷贝的就是内存地址。
- 拷贝时,开辟一块新的内存,将原始的属性的值(基本数据类型)或地址(引用数据类型)拷贝到新开辟的内存。
- 浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象
深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,新旧对象不共享内存,且修改新对象不会影响原对象
对于基本类型a和引用类型c,深拷贝为a1和c1,其中c1是在堆中额外开辟新空间,并完整的拷贝了c这个数组。 这时候修改c, 是不会影响c1的
2.2、实现方式
那在JS里面对于这两种拷贝各自有什么实现方式呢?
浅拷贝:
- Object.assign
- 数组的concat方法
- 数组的slice方法
- 对象扩展运算符
1、Object.assign(target,source) : 将多个源对象中的属性复制到一个目标对象中
let person_obj = {
person: {
name:'eric',
age: 18
}
}
let obj = Object.assign({},person_obj)
console.log(obj == person_obj)
obj.person.name = 'jerry'
console.log(person_obj)
2、 展开运算符
let man = {
name: 'tutu',
age: 100,
address: {
country: 'china',
city: 'beijing'
}
}
let man_copy = {...man}
man.address.city = 'shanghai'
man.name = 'yiyi'
console.log(man_copy);
3、数组concat方法
let arr = [1,2,3,{name:'lala'}]
let arr2 = arr.concat()
arr2[3].name = 'bibi'
console.log(arr);
4、数组slice方法
let array = [1,2,{address:'beijing'}]
let array1 = array.slice()
array[2].address = 'shanghai'
console.log(array1);
深拷贝:
- JSON.stringify()
- 循环递归
1、JSON.parse(JSON.stringify())
最简单的深拷贝方法,就是把一个对象序列化为JSON的字符串,并将对象里面的内容转为字符串,最后JSON.parse()将其再生成一个对象
let arr = [1,2,{name:'eric'}]
let newArr = JSON.parse(JSON.stringify(arr))
arr[0] = 2
arr[2].name = 'tom'
console.log(arr);
console.log(newArr);
但需要注意:
- 拷贝的对象的值如果有函数,undefined,symbol 这几种类型,经过 JSON.stringify 序列化后字符串中这个键值对会消失。
- 拷贝 Date 类型会变成字符串
- 无法拷贝不可枚举的属性
- 无法拷贝对象原型链
- 拷贝 RegExp 引用类型会变成空对象
- 对象中含有 NaN、infinity 以及 -infinity,JSON 序列化后的结果变成 null
- 无法拷贝对象的循环应用,即对象成环(obj[key]=obj)
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
}
深拷贝
简单版深拷贝
function deepCopy(obj){
let copyObj = {}
for(const key in obj){
// null 的 typeof 也是 object
if(typeof obj[key] === 'object' && obj[key] !== null) {
copyObj[key] = deepCopy(obj[key])
}else{
copyObj[key] = obj[key]
}
}
return copyObj
}
const obj = {
name: 'eric',
age: 18,
address: {
country: 'china',
city: 'beijing'
}
}
const obj1 = deepCopy(obj)
obj1.name = 'tom'
obj1.address.city = 'shanghai'
console.log(obj,obj1);
上面,我们利用递归方式实现了一个简单的深拷贝。但依旧存在一些问题:
考虑数组
考虑循环引用
考虑Date或RegExp
function deepCopy(obj, hash = new WeakMap){
// null或undefined
if(obj == null) return obj
// Data、RegExp、Error
if(obj instanceof Date) return new Date(obj)
if(obj instanceof RegExp) return new RegExp(obj)
if(obj instanceof Error) return new Error(obj.message)
// 基本类型
if(typeof obj !== 'object') return obj
// 对象进行深拷贝,判断是否存在该对象
if(hash.has(obj)) return hash.get(obj)
// 原型上的方法,可以获取对象上所有属性级特性
const desc = Object.getOwnPropertyDescriptor(obj)
// 获取原型上的方法和对象的描述信息,创建新的对象
const copyObj = Object.create(Object.getPrototypeOf(obj), desc)
hash.set(obj, copyObj)
// 循环递归遍历内容
for(const key of Reflect.ownKeys(obj)){ // Reflect.ownKeys 返回所有属性,包括不可枚举属性和Symbol类型
let item = obj[key]
if (typeof item === 'object' && item !== null && typeof item !== 'function') {
copyObj[key] = deepCopy(item)
} else {
copyObj[key] = item
}
}
return copyObj
}
const obj1 = {
func: function() {console.log(1)},
obj: { name: 'h' , 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(1)
}
console.log(deepCopy(obj1));