# js深浅拷贝详解与实现
# JS中数据类型
- 基本数据类型: Number、String、undefined、null、Boolean、和Symbol(ES6)
- 引用数据类型: Object(Array, Function, Object, Date, RegExp, Math)
# 深浅拷贝
深浅拷贝只是针对引用类型的,因为引用类型是存放在堆内存中,在栈地址有一个或者多个地址来指向推内存的某一数据
对数据类型的内存存储还不明白的,可以看看我另外一篇文章 内存管理
# 什么是浅拷贝?
顾名思义,浅拷贝就是流于表面的拷贝方式;当属性值为对象类型时,只拷贝了对象数据的引用,导致新旧数据没有完全分离,还会互相影响
# 浅拷贝实现方式
# 1.= 赋值。
不多说,最基础的赋值方式,只是将对象的引用赋值。
let arr = [22, 44, 66, 88];
let newArr = arr;
newArr[0] = 11;
console.log(arr, newArr);
结果如图:
我们本来想把 arr 赋值给 newArr ,当我们修改newArr数组中第一个元素时,却发现了原始数组arr发生了改变 很显然,这就是一个浅拷贝的例子
原理:
- 对于引用类型,赋值操作符只是把存放在栈内容中的指针赋值给另外一个变量。
- 所以在赋值完成后,在栈内存就有两个指针指向堆内存同一个数据。
- 也就可以说两个变量在共用着同一个数据,这就是浅拷贝。
# 2. 数组的concat() 或者 slice()
let arr1 = [1, [2, 2], 3];
let arr2 = arr1.concat();
arr2[0] = '哈';
arr2[1][1] = '变';
console.log('arr1 =>', arr1);
console.log('arr2 =>', arr2);
let arr1 = [1, [2, 2], 3];
let arr2 = arr1.slice(0);
arr2[0] = '哈';
arr2[1][1] = '变';
console.log('arr1 =>', arr1);
console.log('arr2 =>', arr2);
结果如图:
- 通过结果可以看出,concat()是可以复制一份新数组出来, 可当元素为对象类型时,只拷贝了对象数据的引用,导致新旧数据没有完全分离,
- arr2[0]的元素是一个简单类型的值,改变它就不会影响原理的数组对应的值,
- arr[1]的元素则是引用类型的值,所以改变arr[1][1]则也影响了原来数组的值
# 3.对象的Object.assign()
Object.assign是ES6的新函数。Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
Object.assign(target, ...sources)
- 参数:
- target:目标对象。
- sources:任意多个源对象。
- 返回值:目标对象会被返回。
let obj = { a: {a: "hello", b: 21} };
let initalObj = Object.assign({}, obj);
initalObj.a.a = "changed";
console.log(obj.a.a); // "changed"
上面例子可以看到obj原对象的a.a的值也改变了
需要注意的是:
Object.assign()可以处理一层的深度拷贝,如下:
let obj1 = { a: 10, b: 20, c: 30 };
let obj2 = Object.assign({}, obj1);
obj2.b = 100;
console.log(obj1);
// { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2);
// { a: 10, b: 100, c: 30 }
# 4.封装个浅拷贝函数(支持数组和对象)
function shallowCop(target){
//判断参数是不是引用类型, 不是就原样返回
if(typeof target !== 'object') return target;
//判断目标类型,是数组就用[], 其他就用 {}
var newObj = target instanceof Array ? [] : {};
for(var item in target){
//只复制元素自身的属性,不复制原生链上的
if(target.hasOwnProperty(item)){
newObj[item] = target[item]
}
}
return newObj;
}
let test = [1, { name: 'yu' }];
let copyarr = shallowCop(test);
copyarr[1].name = 'YY';
console.log(test);
console.log(copyarr);
结果如图:
# 5. 使用ES6扩展运算符 ...
let arr = [1 , [2, 3]]
let arr1 = [...arr];
arr[1][0] = 222;
console.log(arr); // [1, [222, 3]]
console.log(arr1);// [1, [222, 3]]
ES6扩展运算符,也只是深拷贝了一层
# 真正深拷贝的实现
从浅拷贝解释基本可以明白,深拷贝就是 ‘完全’拷贝,拷贝之后新旧数据完全分离,不再共用对象类型的属性值,不会互相影响。
# 1. JSON实现深拷贝
let obj1 = { body: { a: 10 } };
let 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
原理:
- JSON.parse() 方法用于将一个 JSON 字符串转换为对象。
- JSON.stringify() 方法用于将 JavaScript 值(通常为对象或数组)转换为 JSON 字符串。
- 在JSON.stringify()完成后,对象就转为了字符串(简单类型),也就可以说实实在在的复制了一个值,不存在引用之说。
- 再利用JSON.parse()转为对象,这样达到深拷贝的目的
这样做是真正的Deep Copy,这种方法简单易用。
但是这种方法也有不少坏处,譬如它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。
这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。RegExp对象或者Function对象是无法通过这种方式深拷贝。
也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON。
let obj = {
nul: null,
und: undefined,
sym: Symbol('sym'),
str: 'str',
bol: true,
num: 45,
arr: [1, 4],
reg: /[0-9]/,
dat: new Date(),
fun: function() {},
}
console.log(JSON.parse(JSON.stringify(obj)))
结果如图:
通过我们可以发现有些属性被忽略或者改变了:
- dat对象改成字符串了,reg也改变了,
- undefined, symbol,function 忽略了
可以看得出来JSON实现深拷贝也有不足之处
# 封装个递归深拷贝
function deepCopy(target) {
// 判断不是引用类型则原样返回
if (typeof target !== 'object') return target;
//判断目标类型,是数组用 [], 其他用 {}
var newObj = target instanceof Array ? [] : {};
for (var item in target) {
//只复制元素自身的属性,不复制原生链上的
if (target.hasOwnProperty(item)) {
//判断属性值类型,是引用类型则继续调用自身函数,其他直接赋值
newObj[item] = typeof target[item] === 'object' ? deepCopy(target[item]) : target[item];
}
}
return newObj;
}
let obj = {
nul: null,
und: undefined,
sym: Symbol('sym'),
str: 'str',
bol: true,
num: 45,
arr: [1, 4],
reg: /[0-9]/,
dat: new Date(),
fun: function () { },
}
let newObj = deepCopy(obj);
console.log(obj);
console.log(newObj);
结果如图:
通过结果看出,拷贝虽然是拷贝了,可是Null类型,Date类型,RegExp类型的值都改变了,都改成了{},
所以我们得再优化下代码判断,最终代码如下:
function deepCopy(target) {
// 判断不是引用类型则原样返回
if (typeof target !== 'object') return target;
//判断目标类型,是数组用 [], 其他用 {}
var newObj = target instanceof Array ? [] : {};
for (var item in target) {
//只复制元素自身的属性,不复制原生链上的
if (target.hasOwnProperty(item)) {
//判断属性值类型,是引用类型(并且不是Null、Date、RegExp)则继续调用自身函数,其他直接赋值
newObj[item] =
typeof target[item] === 'object' &&
Object.prototype.toString.call(target[item]) !== '[object Date]' &&
Object.prototype.toString.call(target[item]) !== '[object Null]' &&
Object.prototype.toString.call(target[item]) !== '[object RegExp]' ?
deepCopy(target[item]) : target[item];
// newObj[item] = typeof target[item] === 'object' ? deepCopy(target[item]) : target[item];
}
}
return newObj;
}
let obj = {
nul: null,
und: undefined,
sym: Symbol('sym'),
str: 'str',
bol: true,
num: 45,
arr: [1, 4],
reg: /[0-9]/,
dat: new Date(),
fun: function () { },
}
let newObj = deepCopy(obj);
console.log(obj);
console.log(newObj);
结果如图:
哦耶,完美深拷贝,所有数据类型都覆盖到
参考文献
js浅拷贝与深拷贝方法 (opens new window)
感谢大家耐心看完,以上纯属个人见解,各位小伙伴有什么想法,更好的办法,欢迎留言交流