真正的大师永远都怀着一颗学徒的心。 --无极剑圣
浅拷贝和深拷贝作为前端开发中一个重要的知识点,不管在面试中还是在日常开发工作中都会经常遇到。 因此,弄清楚什么是浅拷贝什么是深拷贝,浅拷贝和深拷贝的区别以及它们的实现方式还是很有必要的! ! ! 下面就开始吧。
一、数据类型
弄懂深拷贝和浅拷贝之前,我们需要对JS的数据类型有一个基本的了解。
1、数据类型
基本数据类型:string、number、boolean、null、undefined、symbol、bigInt。
引用(复杂)数据类型:object(Array、Date、Function、RegExp等)。
2、基本数据和引用数据类型的区别
基本数据类型:存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接访问。
引用数据类型:引用类型(object)是存放在堆内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。每个空间大小不一样,要根据情况来进行特定的分配。引用数据类型保存在堆内存中,然后在栈内存中保存了一个对堆内存中实际对象的引用,即数据在堆内存中的地址,JS对引用数据类型的操作都是操作对象的引用而不是实际的对象,如果obj1拷贝了obj2,那么这两个引用数据类型就指向了同一个堆内存对象,具体操作是obj1将栈内存的引用地址复制了一份给obj2,因而它们共同指向了一个堆内存对象。
例如:
var a = 1;
// 栈内存会开辟一个新的内存空间,此时b和a都是相互独立的
b = a;
b = 2;
console.log(a); // 1
例如:
var arr = [1, 2, 3, 4];
//arr将栈内存的引用地址复制了一份给arr1,因而它们共同指向了一个堆内存对象;
newArr = arr;
newArr[2] = 9;
console.log(arr); //[1,2,9,4]
基本数据类型值不可变,JS中的原始值(undefined、null、boolean、number和string)与对象(包括数组和函数)有着根本区别。
原始值是不可更改的,任何方法都无法更改一个原始值。对数字和布尔值来说显然如此,改变数字的值本身就说不通,而对字符串来说就不那么明显了,因为字符串看起来像由字符组成的数组,我们期望可以通过指定索引来假改字符串中的字符。实际上,JS是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。
基本数据类型的值是不可变的,动态修改了基本数据类型的值,它的原始值也是不会改变的。有很多操作字符串的方法,但是这些方法都是返回一个新的字符串,并没有改变其原有的数据。
例如:
var str = "abc";
str[1] = "d";
console.log(str); //abc
引用类型的值是可变的。
例如:
var a = [1, 2, 'a', 'b'];
a[1] = 3;
console.log(a[1]); // 3
二、浅拷贝和深拷贝
通过上面第一部分我们对数据类型有了一个基本了解后,在学习浅拷贝和深拷贝的区别就容易多了。
假如有一个管理后台的列表页,现在需要去编辑修改某个字段A,在对话框中修改字段A的值,会发现列表中对应当前行的字段A的值,也会随着变化。理论上应该是我们修改提交保存后,刷新列表字段A的值才会更新。这就看起来很奇怪,这不是我们想要的效果。
我们往往需要拷贝的是一个新的对象,改变新对象值的同时不改变原对象的值,这时就需要用到深拷贝和浅拷贝了。
区别
浅拷贝和深拷贝都只针对于引用数据类型,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创造一个一模一样的对象,新对象跟旧对象不共享内存,修改新对象不会改到原对象;浅拷贝只复制对象的第一层属性,深拷贝可以对对象的属性进行递归复制。
三、浅拷贝的实现方式
1、for...in只循环第一层
function shallowCopy(obj) {
let newObj = Array.isArray(obj) ? [] : {};
for (let i in obj) {
newObj[i] = obj[i];
}
return newObj;
}
let obj = {
name: '张三',
age: 18,
score: {
chinese: 96,
english: 72
},
likes: ['唱', '跳', 'rap']
}
const obj1 = shallowCopy(obj);
obj1.name = '坤坤';
obj1.age = 20;
obj1.score.english = 90;
obj1.likes[3] = '打篮球';
console.log(obj.name); // 张三
console.log(obj1.name); // 坤坤
console.log(obj.age); // 18
console.log(obj1.age); // 20
console.log(obj.score.english); // 90
console.log(obj1.score.english); // 90
console.log(obj.likes); // ['唱', '跳', 'rap', '打篮球']
console.log(obj1.likes); // ['唱', '跳', 'rap', '打篮球']
如上代码,修改新对象obj1中name和age的属性值,原对象obj中name和age的值没有改变,但修改新对象obj1.score.english和obj1.likes的属性值,原对象obj1.score.english和obj1.likes的值发生了改变。这是为什么呢?这不难理解,obj中的name和age属于基本数据类型,而obj中的score和likes属于引用数据类型,拷贝的是score和likes的内存地址,修改obj1.score.english和obj1.likes的值,原对象的值也对随之变化。
2、Object.assign()
let obj = {
name: '张三',
age: 18,
score: {
chinese: 96,
english: 72
},
likes: ['唱', '跳', 'rap']
}
const obj1 = Object.assign({}, obj);
obj1.name = '坤坤';
obj1.likes[3] = '打篮球';
console.log(obj.name); // 张三
console.log(obj1.name); // 坤坤
console.log(obj.likes); // ['唱', '跳', 'rap', '打篮球']
console.log(obj1.likes); // ['唱', '跳', 'rap', '打篮球']
如上代码,obj中的name和age属于基本数据类型,而obj中的score和likes属于引用数据类型,拷贝的是score和likes的内存地址,修改obj1.likes的值,原对象的值也对随之变化。
3、Object.create()
let obj = {
name: '张三',
age: 18,
score: {
chinese: 96,
english: 72
},
likes: ['唱', '跳', 'rap']
}
const obj1 = Object.create(obj);
obj1.name = '坤坤';
obj1.likes[3] = '打篮球';
console.log(obj.name); // 张三
console.log(obj1.name); // 坤坤
console.log(obj.likes); // ['唱', '跳', 'rap', '打篮球']
console.log(obj1.likes); // ['唱', '跳', 'rap', '打篮球']
四、深拷贝的实现方式
1、JSON.parse(JSON.stringify(obj))
JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象。
let obj = {
name: '张三',
age: 18,
score: {
chinese: 96,
english: 72
},
likes: ['唱', '跳', 'rap'],
sing:function() {
console.log('坤坤爱唱歌')
},
skip: new RegExp("e"),
}
const obj1 = JSON.parse(JSON.stringify(obj))
obj1.name = '坤坤';
obj1.likes[3] = '打篮球';
console.log(obj.name) // 张三
console.log(obj1.name) // 坤坤
console.log(obj.likes) // ['唱', '跳', 'rap']
console.log(obj1.likes) // ['唱', '跳', 'rap', '打篮球']
新对象打印结果如下:
这种方法使用很简单,可以满足基本的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是它却有如下缺点。
缺点:
(1)如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是时间对象;
(2)如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象(打印结果可以看出,skip为{});
(3)如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
(4)如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null;
(5)JSON.stringify()只能序列化对象的可枚举的自有属性,如果obj中的对象是有构造函数生成的,深拷贝后,会丢弃对象的constructor;
(6)如果对象中存在循环引用的情况也无法正确实现深拷贝。
2、递归实现
let data = {
name: '张三',
age: 18,
score: {
chinese: 96,
english: 72
},
likes: ['唱', '跳', 'rap'],
sing:function() {
console.log('坤坤爱唱歌')
},
skip: new RegExp("e"),
}
function judgeType(obj) {
// tostring会返回对应不同的标签的构造函数
const toString = Object.prototype.toString;
const map = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object Object]': 'object',
};
if (obj instanceof Element) {
return 'element';
}
return map[toString.call(obj)];
}
function deepCopy(obj) {
const type = judgeType(obj)
let result;
if (type === "object") {
result = {};
} else if (type === "array") {
result = [];
} else {
return obj;
}
for (const k in obj) {
const e = obj[k];
if (type === "object" || type === "array") {
result[k] = deepCopy(e);
} else {
result[k] = e;
}
}
return result;
}
const obj1 = deepCopy((data))
console.log(obj1)
obj1.name = '坤坤';
obj1.likes[3] = '打篮球';
console.log(data.name) // 张三
console.log(obj1.name) // 坤坤
console.log(data.likes) // ['唱', '跳', 'rap']
console.log(obj1.likes) // ['唱', '跳', 'rap', '打篮球']
3、递归的另一种方法
function deepCopy(obj) {
//判断 传入对象 为 数组 或者 对象
var result = Array.isArray(obj) ? [] : {};
// for in 遍历
for (var key in obj) {
// 判断是否为自身的属性值(排除原型链干扰)
if (obj.hasOwnProperty(key)) {
// 判断 对象的属性值 中 存储的 数据类型 是否为对象
if (obj[key] && typeof obj[key] === 'object') {
// 递归调用
result[key] = deepCopy(obj[key]); //递归复制
}
// 不是的话 直接 赋值 copy
else {
result[key] = obj[key];
}
}
}
// 返回 新的对象
return result;
}
4、函数库lodash
此外,我们可以借助第三方工具库来实现,比如借助函数库lodash提供的_.cloneDeep来实现深拷贝。
5、jquery的$.extend
jquery实现深拷贝,jquery 提供一个$.extend可以用来做深拷贝。
let obj = {
name: '张三',
age: 18,
score: {
chinese: 96,
english: 72
},
likes: ['唱', '跳', 'rap'],
sing:function() {
console.log('坤坤爱唱歌')
},
skip: new RegExp("e"),
}
const newObj = $.extend(true, {}, obj);
五、总结
感谢您的阅读!通过本篇文章的阅读可以对深浅拷贝有一个基本的了解和使用,了解的深拷贝和浅拷贝的区别,可以帮助我们更有效的搬砖。