这一次,让你把深拷贝和浅拷贝刻进骨子里

321 阅读6分钟

说起拷贝就不得不提到js 的数据类型,这是一个老生常谈的问题了,如果不了解的小伙伴移步这儿。数据类型,就像一片肥沃的土壤,虽平平无奇,却总能开出鲜艳的花。深浅拷贝,就是其中一朵。

数据存储

众所周知,js 分为 基本数据类型引用数据类型,基本数据类型存储在 里面,引用数据类型存储在 里面(引用地址存在栈里,它指向存储对象的内存地址) 如图所示 image.png

奇怪的现象

我们先来看一个案例

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);

image.png

同样是赋值过去,name2 重新赋值之后,name1 的值没有被影响,改了 obj2 的属性重新赋值之后,obj1 的值就被影响了,这是为什么呢?

原因就是 两个对象指向了相同的内存地址,所以修改其中一个对象时,即修改了内存地址里的对象,其他关联对象也会改变(打个比方,你和你老婆用的是同一张银行卡,无论你俩谁动了里面的钱,那么对方的余额都会变化),我们再去改变obj1 ,瞧,obj2 也跟着变化了(像不像玄幻小说里面签订的契约,主角强了,他体内的怪物也就变强,主角死了那怪物也死那种) image.png

现在他们之间的关系是这样的 image.png

所以说 引用数据类型简单复制的时候是 引用地址的拷贝 而不是开辟一个新的数据空间

浅拷贝

浅拷贝是指拷贝对象的时候,只对第一层键值对进行独立的拷贝,如果对象内存在 嵌套 的情况,则只能拷贝嵌套对象的地址(这句话不理解的同学再想想刚刚的案例)。常见的就是 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 } }

image.png

还有数组的例子(只要不修改原数组, 重新返回一个新数组就可以实现浅拷贝,像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);

image.png

是不是很暴力呢?但是他还是优缺点的

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)

image.png

JSON.paseJSON.stringify 的缺点
  • 不会拷贝对象上值为 undefined 的键值对
  • 不会拷贝函数键值对
  • NaN 被转为 null
  • Date 对象会被转化为 字符串,而不是时间对象
  • 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);

image.png

完美实现!

拓展

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}

image.png

ps1:

image.png

ps2

看这儿的类型检测

官方的 structuredClone

后面还可以期待官方的实现 可以看MDN的描述