两者的区别直观图:
先来两段代码热热身:
- 浅拷贝
let Obj = {
name: 'AAA',
age: 18,
haha:{
a:'1254',
b:()=>{
return 'b'
},
c:["sdad",'sad',1545]
}
}
let obj5 = Object.assign({
...Obj
})
console.log(obj5);
obj5.haha.c.push('6666')
console.log(Obj)
复制代码
- 递归深拷贝:
let E = { id: 1, val: "111", arr: [1, 2, { name: '嘻嘻' }], info: { projectName: "项目1" } };
let F = deepClone(E);
console.log(E === F); // false
console.log('E', E); // { id: 1, val: "111", arr:[1,2,{name:'嘻嘻'}], info: { projectName: "项目1" } }
console.log('F', F); // { id: 1, val: "111", arr:[1,2,{name:'嘻嘻'}], info: { projectName: "项目1" } }
let e = {
name: '张三',
sayGoodbye: function () {
console.log('Goodbye!');
}
};
let f = deepClone(e);
e.sayGoodbye();//Goodbye!
f.sayGoodbye();//Goodbye!
// 递归方法:
function deepClone(source) {
const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
for (let keys in source) { // 遍历目标
if (source.hasOwnProperty(keys)) {
if (source[keys] && typeof source[keys] === 'object') { // 如果值是对象,就递归一下
targetObj[keys] = source[keys].constructor === Array ? [] : {};
targetObj[keys] = deepClone(source[keys]);
} else { // 如果不是,就直接赋值
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}
复制代码
看起来都是赋值 那么有啥区别呢???
需求出发
比如在做一些表单提交的时候我们经常 需要再提交那个对象进行一些数据的更改 但又不想影响到页面双向数据绑定的那些属性 (因为如果提交失败 你修改的值会直接影响到页面的回显效果)那当你遇到需要提交一个很复杂的对象(各种类型的数据都有)那么是采用直接赋值还是 采用深拷贝的函数 ?
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
对象赋值
var xiaoDeng = {
name:"lulu",
age:18,
flag:true,
friends:['小李','小明','小红','小爱'],
happy:{
eat:'华莱士',
study:['bilibili','baidu'],
goTo:()=>{
return '去旅行'
}
},
add:(x,y)=>{
return x+y
}
}
let newPeople = xiaoDeng
console.log('newPeople:',newPeople);
复制代码
修改 xiaoDeng.happy.study
xiaoDeng.happy.study.splice(1,1)
console.log('xiaoDeng:',xiaoDeng);
console.log('newPeople:',newPeople);
复制代码
两个都被修改了
浅拷贝
let obj = {
name: 'AAA',
age: 18
}
// 1,新建新对象,复制原来对象的值 如果是引用类型 都会改变
let obj1 = {
name: obj.name,
age: obj.age
}
obj1.name = 'BBB'
// console.log(obj.name); // AAA
// console.log(obj1.name); // BBB
// 2,新建对象,循环添加
// for (let key in obj) {
// obj2[key] = obj[key]
// }
function shallowClone(source) {
var target = {};
for (var i in source) {
if (source.hasOwnProperty(i)) { //该方法会忽略掉那些从原型链上继承到的属性。
target[i] = source[i];
}
}
return target;
}
let obj2 = shallowClone(obj1)
console.log(obj.name); // AAA
obj2.name = 'CCC'
console.log(obj2.name); // CCC
// 3, Object.assign()
let obj3 = Object.assign({}, obj) // 第一个参数目标对象要加上
obj3.name = 'DDD'
console.log(obj.name); // AAA
console.log(obj3.name); // DDD
// 4,点语法展开
// console.log({
// ...obj
// })
// {name: "AAA", age: 18}
let obj4 = Object.assign({
...obj
}) //只有目标对象。
obj4.name = 'EEE'
console.log(obj.name); // AAA
console.log(obj4.name); // EEE
复制代码
网上的这种说法其实不是很妥当的:
简单的理解就是拷贝了对象的第一层属性,如果对象的某个属性还有第二层,第三层的数据,浅拷贝是访问不到的。 比如说某个属性的值是对象,那浅拷贝无法复制该对象的数据。
这能说 JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;
在 JavaScript 中,数组有两个方法 concat 和 slice 是可以实现对原数组的拷贝的,这两个方法都不会修改原数组,而是返回一个修改后的新数组。同时,ES6 中 引入了 Object.assgn 方法和 … 展开运算符也能实现对对象的拷贝
concat
let originArr = [1, [1, 2, 3], {
a: 1,
sayGoodbye: function () {
console.log('Goodbye!');
}
}];
let copyArr = originArr.concat();
console.log(copyArr === originArr); // false
console.log(copyArr)//concat不传参数 赋值一份相同的
//修改复制出来的
copyArr[2].sayGoodbye(); //Goodbye!
copyArr[1].push(4);
copyArr[2].a = 2;
//原来的那一份也是被修改了
console.log('originArr',originArr); // [1, [1, 2, 3, 4], {a: 2,sayGoodbye: function () {console.log('Goodbye!');}}]
console.log('copyArr', copyArr); // [1, [1, 2, 3, 4], {a: 2,sayGoodbye: function () {console.log('Goodbye!');}}]
复制代码
originArr 中含有数组 [1,2,3] 和对象 {a:1},如果我们直接修改数组和对象,不会影响 originArr,但是我们修改数组 [1,2,3] 或对象 {a:1} 时,发现 originArr 也发生了变化。
结论: concat 只是对数组的第一层进行深拷贝。
slice(包含 begin
,但不包含 end
)
- 局限性:
- 对象:
var arr1 = [{
"name": "weifeng"
}, {
"name": "boy"
}]; //原数组
var arr2 = [].concat(arr1); //拷贝数组
arr1[1].name = "girl";
console.log(arr1); // [{"name":"weifeng"},{"name":"girl"}]
console.log(arr2); //[{"name":"weifeng"},{"name":"girl"}]
复制代码
2. 数组:
结论: slice 也是只对数组的第一层进行深拷贝。
结论:… 实现的是对象第一层的深拷贝。后面的只是拷贝的引用值。
结论:Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。
小结: 浅拷贝的实现方式
- Object.assign()
- 函数库lodash的_.clone方法
- 展开运算符…
- Array.prototype.concat()
- Array.prototype.slice() 等
深拷贝
那么又有几种呢?
1. JSON.parse(JSON.stringify())
利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
缺点:这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。 当深拷贝对象含有undefined、function、symbol 会在转换过程中会被忽略
- 测试:
// 深拷贝
let obj1 = {
name: '浪里行舟',
arr: [1, [2, 3], 4],
add:(x,y)=>{
return x+y
},
str:/^[0-9]{1,20}$/
}
console.log(obj1)
let newObj = JSON.stringify(obj1)
console.log(JSON.parse(newObj))
复制代码
2. 函数库lodash的_.cloneDeep方法 (开发必备)
遇到比较简单的对象我们们应该直接调用clone方法而不是cloneDeep(或者JSON.parse(JSON.stringify(obj))),这样既保险也可以减少性能损耗。
浏览器使用:www.bootcdn.cn/lodash.js/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script>
// var objects = [{
// 'a': 1
// }, {
// 'b': 2
// }];
// var deep = _.cloneDeep(objects);
// console.log(deep[0] === objects[0]);
// => false
var obj = {
id: 1,
name: {
a: 'xx'
},
fn: function () {}
};
var obj2 = _.cloneDeep(obj);
obj2.name.a = 'obj2';
console.log(obj, obj2)
const arr = [5, 2, 3, 4, 1, "a", "c", "b", "d"]
const solveArr = _.chunk(arr,2)
console.log(solveArr)
</script>
</body>
</html>
复制代码
3. jQuery.extend()方法
$.extend(deepCopy, target, object1, [objectN])//第一个参数为true,就是深拷贝
var $ = require('jquery');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f);
// false
复制代码
4. 手写递归方法
ps:对象存在循环引用的情况
方法一:相对全面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
let obj = {
name: '1张三丰',
age: 22,
messige: {
sex: '男',
score: 16
},
color: ['red', 'purple', 'qing'],
fig: true,
fn: function (x) {
console.log('我在打游戏' + x)
},
str: /^[0-9]{1,20}$/
}
let be = {};
// Object.prototype.toString 会返回一个类型字符串 如未被重写则返回'[object String]' 被重写则返回类型
function getType(obj) {
//tostring会返回对应不同的标签的构造函数
// var toString = Object.prototype.toString;
var 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'
};
return map[Object.prototype.toString.call(obj)];
}
function kao(be, obj) {
for (var key in obj) {
// 检测对象中是否有该key
if (obj.hasOwnProperty(key)) {
let type = getType(obj[key])
if (type === 'object') {
be[key] = {}
kao(be[key], obj[key])
} else if (type === 'array') {
be[key] = []
kao(be[key], obj[key])
}else{
be[key]=obj[key]
}
}
}
}
kao(be, obj)
console.log(be)
console.log(obj)
// let obj1 = {
// name: 1,
// address: {
// x: 100
// }
// };
// obj1.o = obj1; // 对象存在循环引用的情况
// let d = kao({},obj1);
// console.log(d);
</script>
</body>
</html>
复制代码
方法二:(个人推荐的)
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj;
// 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
let obj = {
name: 1,
address: {
x: 100
}
};
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);
复制代码
可以复制正则
上面这方法是没有报错的!!!
方法三:(项目够用 )
不能复制正则 也是会无限循环!!!
// 这种方式 适用于多层数据嵌套
// export function deepClone(obj) { //可传入对象 或 数组
function deepClone(obj) { //可传入对象 或 数组
// 判断是否为 null 或 undefined 直接返回该值即可,
if (obj === null || !obj) return obj;
// 判断 是要深拷贝 对象 还是 数组
if (Object.prototype.toString.call(obj) === "[object Object]") { //对象字符串化的值会为 "[object Object]"
let target = {}; //生成新的一个对象
const keys = Object.keys(obj); //取出对象所有的key属性 返回数组 keys = [ ]
//遍历复制值, 可用 for 循环代替性能较好
keys.forEach(key => {
if (obj[key] && typeof obj[key] === "object")
//如果遇到的值又是 引用类型的 [ ] {} ,得继续深拷贝
target[key] = deepClone(obj[key]); //递归
else
target[key] = obj[key];
})
return target //返回新的对象
} else if (Array.isArray(obj)) {
// 数组同理
let arr = [];
obj.forEach((item, index) => {
if (item && typeof item === "object")
arr[index] = deepClone(item);
else
arr[index] = item;
})
return arr
}
}
// 用法
let Obj = {
name: 'AAA',
age: 18,
haha: {
a: '1254',
b: () => {
return 'b'
},
c: ["sdad", 'sad', 1545]
},
str: /^[0-9]{1,20}$/
}
let newObj = deepClone(Obj)
console.log(newObj)//
let obj = {
name: 1,
address: {
x: 100
}
};
// obj.o = obj; // 对象存在循环引用的情况
// let d = deepClone(obj);
// obj.address.x = 200;
// console.log(d);
复制代码
最后总结一下:
1.赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
2.JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;
3.JSON.stringify 实现的是深拷贝,但是对目标对象有要求(非 undefined,function);
4.若想真正意义上的深拷贝,请递归。
复制代码