说起拷贝就不得不提到js 的数据类型,这是一个老生常谈的问题了,如果不了解的小伙伴请移步这儿。数据类型,就像一片肥沃的土壤,虽平平无奇,却总能开出鲜艳的花。深浅拷贝,就是其中一朵。
数据存储
众所周知,js 分为 基本数据类型 和 引用数据类型,基本数据类型存储在 栈 里面,引用数据类型存储在 堆 里面(引用地址存在栈里,它指向存储对象的内存地址)
如图所示
奇怪的现象
我们先来看一个案例
let name1 = '王二麻子';
let name2 = name1;
name2 = 'constRen';
let obj1 = {
name: "赵六",
age: "22"
};
let obj2 = obj1;
obj2.name = '小貂禅'
obj2.age = 18;
console.log('name1', name1);
console.log('name2',name2);
console.log('---------------------');
console.log('obj1', obj1);
console.log('obj2', obj2);
同样是赋值过去,name2 重新赋值之后,name1 的值没有被影响,改了 obj2 的属性重新赋值之后,obj1 的值就被影响了,这是为什么呢?
原因就是 两个对象指向了相同的内存地址,所以修改其中一个对象时,即修改了内存地址里的对象,其他关联对象也会改变(打个比方,你和你老婆用的是同一张银行卡,无论你俩谁动了里面的钱,那么对方的余额都会变化),我们再去改变obj1 ,瞧,obj2 也跟着变化了(像不像玄幻小说里面签订的契约,主角强了,他体内的怪物也就变强,主角死了那怪物也死那种)
现在他们之间的关系是这样的
所以说 引用数据类型简单复制的时候是 引用地址的拷贝 而不是开辟一个新的数据空间
浅拷贝
浅拷贝是指拷贝对象的时候,只对第一层键值对进行独立的拷贝,如果对象内存在 嵌套 的情况,则只能拷贝嵌套对象的地址(这句话不理解的同学再想想刚刚的案例)。常见的就是 for in 、Object.assign、slice、concat、...语法等等
举个例子
// 浅拷贝
var user1 = {
name: "老大",
age: 11,
other: {
text: 666
}
};
var user2 = {};
for (let key in user1) {
user2[key] = user1[key];
};
console.log('user1', user1); // user1 { name: '老大', age: 11, other: { text: 666 } }
console.log('user2', user2); // user2 { name: '老大', age: 11, other: { text: 666 } }
// 目前为止,拷贝的第一层 非常完美
user2.name = "laosi";
user1.other.text = 888
// 第一层的 name 是没有受到影响的 因为是基本数据类型
// 但是 other是引用数据类型,只能拷贝引用地址 所以 text 就从 666 变成 888 了
console.log('user1', user1); // user1 { name: '老大', age: 11, other: { text: 888 } }
console.log('user2', user2); // user2 { name: 'laosi', age: 11, other: { text: 888 } }
var user3 = {
age: 22,
sex: "nan"
}
console.log('--------------------------------');
// Object.assign 也是浅拷贝
var user3 = Object.assign({}, user1, user3);
console.log('user3',user3); // user3 { name: '老大', age: 22, other: { text: 888 }, sex: 'nan' }
user3.name = 111;
user3.other.text = 999;
console.log('user3',user3); // user3 { name: 111, age: 22, other: { text: 999 }, sex: 'nan' }
// 这儿和上面是一样的 name 改了 不受影响
// 但是 引用数据类型 other 改了之后 其他数据也就受到了影响 text 边成了 999
console.log('user1',user1); // user1 { name: '老大', age: 11, other: { text: 999 } }
console.log('user2',user2); // user2 { name: 'laosi', age: 11, other: { text: 999 } }
还有数组的例子(只要不修改原数组, 重新返回一个新数组就可以实现浅拷贝,像map、filter、reduce等方法),这儿就不一 一举例和贴图了
// 案例1
let a = [1,2,3,4];
let b = a;
b.push(5);
console.log('a',a); // a [ 1, 2, 3, 4, 5 ]
console.log('b',b); // a [ 1, 2, 3, 4, 5 ]
// 案例2
let a = ['constRen', { val: 666 }];
let b = [...a];
console.log('a', a); // a [ 'constRen', { val: 666 } ]
console.log('b', b); // b [ 'constRen', { val: 666 } ]
b[0] = 'rx';
b[1].val = 888;
console.log('----------------');
console.log('a', a); // a [ 'constRen', { val: 888 } ]
console.log('b', b); // b [ 'rx', { val: 888 } ]
// 案例3
let a = ['constRen', { val: 666 }];
let b =a.map(i=>i);
console.log('a', a); // a [ 'constRen', { val: 666 } ]
console.log('b', b); // b [ 'constRen', { val: 666 } ]
b[0] = 'rx';
b[1].val = 888;
console.log('----------------');
console.log('a', a); // a [ 'constRen', { val: 888 } ]
console.log('b', b); // b [ 'rx', { val: 888 } ]
深拷贝
深拷贝就是不管你嵌套多少层,也不管你是基本数据类型还是引用数据类型,都重新拷贝一份,两者相互分离,不存在共用数据的现象,那种修改一个对象的属性,会影响另一个的日子就此远去了
举个例子
var user1 = {
name: "老大",
age: 11,
other: {
text: 666
}
};
var user2 = JSON.parse(JSON.stringify(user1));
console.log('user1', user1);
console.log('user2', user2);
console.log('------------------');
user2.other.text = 888;
user2.name = 'constRen';
console.log('user1', user1);
console.log('user2', user2);
是不是很暴力呢?但是他还是优缺点的
let deepClone = function (obj) {
return JSON.parse(JSON.stringify(obj))
}
let a = {
name: 'constRen',
age: 28,
hobby: ['play', 'sing', { type: 'sports', value: 'run' }],
myGrade: {
grades: 'A',
},
run: function () { },
walk: undefined,
money: NaN,
fly: null,
date: [new Date(1681999956175)],
myRegExp: new RegExp('\\w+'),
}
let b = deepClone(a)
console.log("b", b)
JSON.pase 和 JSON.stringify 的缺点
- 不会拷贝对象上值为
undefined的键值对 - 不会拷贝函数键值对
NaN被转为nullDate对象会被转化为 字符串,而不是时间对象RegExp对象会被转化为 空对象
所以还是我们自己搞一个吧,毕竟 递归 也不是吃干饭的
递归实现 深拷贝
function copyFunction(func) {
// 普通函数有 prototype, 箭头函数没有 prototype
let fnStr = func.toString();
// 这儿普通函数就成了字符串 function () { },箭头函数就是() => { },但是普通函数需要把他变为function 类型
// 不懂就看下面的ps1
return func.prototype ? eval(`(${fnStr})`) : eval(fnStr)
}
function myDeepCopy(obj) {
// 处理函数
if (typeof obj === 'function') {
return copyFunction(obj)
}
// 处理null 和 基础数据类型
if (obj === null || typeof obj !== 'object') {
return obj
}
// 这种方法不明白的请看ps2的链接
// 处理 Date 对象
if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj)
// 处理 RegExp 对象
if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj)
// 处理 Error 对象
if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj)
let cloneObj = Array.isArray(obj) ? [] : {}
Object.keys(obj).forEach(key => {
cloneObj[key] = myDeepCopy(obj[key])
})
return cloneObj
};
let fun = () => { };
let a = {
name: 'constRen',
age: 28,
hobby: ['play', 'sing', { type: 'sports', value: 'run' }],
myGrade: {
grades: 'A',
},
run: function () { },
walk: undefined,
money: NaN,
fly: null,
date: [new Date(1681999956175)],
myRegExp: new RegExp('\\w+'),
run1: fun,
run2: () => { }
}
let b = myDeepCopy(a);
console.log('b', b);
完美实现!
拓展
eval 函数:可以将传入的字符串作为脚本代码来执行,传入的参数不是字符串,它直接返回这个函数。如果参数是字符串,它会把字符串当成JavaScript代码进行编译。牛逼的是可以将 json 字符串转化为 json 对象
var code1 = '"a" + 2';
var code2 = '{a:2}';
var code3 = { a: 2 };
console.log('eval(code1)', eval(code1));// a2
console.log('eval(code2)', eval(code2)); // 2
console.log('eval(( + code2 + ))', eval('(' + code2 + ')')); // {a: 2} 注意 这是一个对象 不再是字符串了
console.log('eval(( + code2 + ))', eval('(' + code2 + ')').a); // 2
console.log('eval(code3)', eval(code3)); // {a: 2}
ps1:
ps2
看这儿的类型检测
官方的 structuredClone
后面还可以期待官方的实现 可以看MDN的描述