浅拷贝与深拷贝

635 阅读7分钟

深拷贝与浅拷贝

复习:

  • 基本数据类型:基本数据类型指的是简单数据段存放在栈中,按值访问。
  • 引用数据类型:它存放在堆中,它在栈中存放了指针,该指针指向堆中它的数据;

深拷贝与浅拷贝的区别:

  1. 浅拷贝只能拷贝一层对象,如果有对象的嵌套,那么浅拷贝在该嵌套前表现得跟赋值一样。即在拷贝对象里,遇见基本类型,拷贝基本类型的值,遇到引用类型,拷贝的又是内存地址。
  2. 深拷贝解决了浅拷贝只能拷贝一层对象的问题,它将一个对象从内存拷贝出来放进另一个新区域存放新对象。

拷贝01.png

直接赋值

//基本数据类型
let a = 1;
let b = a;
b = 2;
console.log(a, b);
//引用数据类型
let arr = [0,1,2];
let brr = arr;
brr[0] = 1;
console.log(arr, brr);

控制台:

1 2

[1,1,2] [1,1,2]

显然改变brr的时候,它赋的是arr在栈中的指针(地址),所以它跟arr是同一个引用,它们都是指向同一块堆内存。

浅拷贝

Object.assign()

用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。

语法

Object.assign(target, ...sources)

示例

const obj = {name: "dzz", where: "juejin"};
const obj2 = Object.assign({}, obj, {name: "yly"});
console.log(obj, obj2);

控制台:

{name: "dzz", where: "juejin"} {name: "yly", where: "juejin"}

concat浅拷贝数组

用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

const arr = [0,1,2];
const arr2 = arr.concat();
arr2[0] = 1;
console.log(arr, arr2);

slice浅拷贝

返回一个新数组,由begin和end决定的原数组的浅拷贝

const arr = [0,1,2];
const arr2 = arr.slice();
arr2[0] = 1;
console.log(arr, arr2);

...展开运算符

let arr = [0,1,2];
let arr2 = [...arr];
arr2[0] = 1;
console.log(arr, arr2);

自身实现

function shallowCopy(target) {
    //1. 保证传入的target为引用类型
    if(typeof target !== "object" || target === null) {
        return target;
    }
    //2. 判断传入的target是对象还是数组,在创建新的copyTarget
    const copyTarget = Array.isArray(target) ? [] : {};
    for(let item in target) {
        //3.是自身的而不是原型链上的
        if(target.hasOwnProperty(item)) {
            copyTarget[item] = target[item];
        }
    }
    return copyTarget;
}

拓展for...in和for...of

for...in:以任意顺序遍历一个对象的除Symbol以外的可枚举属性

语法:

for (variable in object)

variable:在每次迭代时,variable会被赋值为不同的属性名

object:非Symbol类型的可枚举属性被迭代的对象

最好不要用于Array

for...of:在可迭代对象(包括Array,Map,Set,String,arguments对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句

语法:

for (variable of iterable)

variable:在每次迭代中,将不同属性的值分配给变量。

iterable:被迭代枚举其属性的对象。

测试

let arr = [1,2,3];
for(let item of arr) {
    console.log(item); //123
}
for(let item in arr) {
    console.log(item); //012
}

拷贝02.png

深拷贝

JSON.parse()和JSON.stringfy()

json.stringfy() :将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。

语法:developer.mozilla.org/zh-CN/docs/…

JSON.stringify(value[, replacer [, space]])

value: 将要序列化成 一个 JSON 字符串的值。

replacer: 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。

space: 美化输出,指定空白字符串

注意

  • undefined,任意函数以及Symbol值在序列化过程中会被忽略。但是函数,undefined被单独转换时会返回undefined。
  • 对象包含引用对象执行此方法,会抛出错误
const obj = {val: "juejin"};
obj.target = obj;
console.log(JSON.stringfy(obj));  //报错

控制台:JSON.stringfy is not a function

  • Date 日期调用了 toJSON() 将其转换为了 string 字符串,会被当做字符串处理。

JSON.parse() :解析JSON字符串

语法:developer.mozilla.org/zh-CN/docs/…

JSON.parse(text[, reviver])

text:要被解析成 JavaScript 值的字符串

reviver:用来修改解析生成的原始值,调用时机在 parse 函数返回之前

小结:使用JSON.parse(JSON.stringfy())来深拷贝,不能将函数,undefined,Symbol,Data等进行正确的处理。

const obj = {
    name: "dzz",
    age: 12,
    regexp: /\d/,
    tfn: function() {},
    tunde: undefined,
    tnull: null,
    tdate: new Date(),
    syl: Symbol("juejin"),
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log("obj", obj);
console.log("obj2:", obj2);

控制台:obj {age: 12,name: "dzz",regexp: /\d/,syl: Symbol(juejin),tdate: Tue Aug 31 2021 10:22:18 GMT+0800 (中国标准时间) {},tfn: ƒ () ,tnull: null,tunde: undefined}

obj2{age: 12,name: "dzz",regexp: {},tdate: "2021-08-31T02:22:18.907Z",tnull: null}

由结果知道:正则变成了空对象;函数,undefined,Symbol直接被忽视,日期被当成字符串处理。

这就是它的缺陷(还有刚刚的循环引用)

拷贝03.png

自身实现

先开始想可以利用浅拷贝拷贝当前一层在递归下去(浅拷贝的自身实现)

简单实现

function deepCopy(target) {
    //1. 保证传入的target为引用类型,基本数据类型被返回
    if(typeof target !== "object" || target === null) {
        return target;
    }
    //2. 判断传入的target是对象还是数组,在创建新的copyTarget
    const copyTarget = Array.isArray(target) ? [] : {};
    for(let item in target) {
        //3.是自身的而不是原型链上的
        if(target.hasOwnProperty(item)) {
            //4.将获取当前一层的值进行递归,引用类型被继续往下拷贝,基本数据类型直接被返回
            copyTarget[item] = deepCopy(target[item]);
        }
    }
    return copyTarget;
}
//----------------------------测试
const obj = {
    name: "dzz",
    child: {
        name: "xxx"
    },
    tun: undefined,
    tnull: null,
    a: Symbol("juejin"),
};
const obj2 = shallowCopy(obj);  
const obj3 = deepCopy(obj);
obj.child.name = "xx2";
console.log(obj2);       //name被修改成xx2
console.log(obj3);       //没有被修改

思考需要解决的问题:循环引用

let a = Symbol("juejin");
const obj = {
    name: "dzz",
    child: {
        name: "xxx"
    },
    tun: undefined,
    tnull: null,
    a: 1,
};
//循环引用
obj.target = obj;
const obj2 = shallowCopy(obj);  
const obj3 = deepCopy(obj);
obj.child.name = "xx2";
console.log(obj2);       //name被修改成xx2
console.log(obj3);       //没有被修改

控制台:报错Maximum call stack size exceeded

我们可以将对象的拷贝对象保存到哈希表中,如果当前对象已被拷贝过,那么直接返回哈希表已被拷贝过的

//这里使用WeakMap而不是Map是想让它随时得以回收
function deepCopy(target, myMap = new WeakMap()) {
    //1. 保证传入的target为引用类型
    if (typeof target !== "object" || target === null) {
        return target;
    }
    //6. 判断当前对象是否被拷贝过
    if(myMap.get(target)) {
        return myMap.get(target);
    }
    //2. 判断传入的target是对象还是数组,在创建新的copyTarget
    const copyTarget = Array.isArray(target) ? [] : {};
    //3. 将它的拷贝的对象保存到myMap,也就是哈希表
    myMap.set(target, copyTarget);
    for (let item in target) {
        //4. 是自身的而不是原型链上的
        if (target.hasOwnProperty(item)) {
            //5.进行下一次递归,不是引用类型会直接返回
            copyTarget[item] = deepCopy(target[item], myMap);
        }
    }
    return copyTarget;
}
//---------------------------测试
const obj = {
    name: "dzz",
    child: {
        name: "xxx"
    },
    tun: undefined,
    tnull: null,
};
//循环引用
obj.target = obj;
const obj3 = deepCopy(obj);
obj.child.name = "xx2";
obj.target.name = "xx3";
console.log(obj3);

手写深拷贝解决循环引用.png

WeakMap与Map的区别

  1. WeakMap对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。Map任何值都可以作为一个键或一个值
  2. WeakMap持有的每个键对象的“弱引用”,这使得在没有其他引用存在时垃圾回收能正确进行。它用于映射的 key 只有在其没有被回收时才是有效的,即WeakMap不能被遍历,

拷贝04.png

继续思考:将对象里面的Symbol值和正则表达式也拷贝下来

简单实现版

function deepCopy(target, myMap = new WeakMap()) {
    //1. 保证传入的target为引用类型
    if (typeof target !== "object" || target === null) {
        return target;
    }
    //6. 判断当前对象是否被拷贝过
    if(myMap.get(target)) {
        return myMap.get(target);
    }
    //7. 判断是否Date或者RegExp,这里面还有很多判断没有写出比如String,Number等
    if (target instanceof Date) return new Date(target);
    if (target instanceof RegExp) return new RegExp(target);
    //2. 判断传入的target是对象还是数组,在创建新的copyTarget
    const copyTarget = Array.isArray(target) ? [] : {};
    //3. 将它的拷贝的对象保存到myMap,也就是哈希表
    myMap.set(target, copyTarget);
    for (let item in target) {
        //4. 是自身的而不是原型链上的
        if (target.hasOwnProperty(item)) {
            //5.进行下一次递归,不是引用类型会直接返回
            copyTarget[item] = deepCopy(target[item], myMap);
        }
    }
    return copyTarget;
}

测试:

const obj = {
    name: "dzz",
    child: {
        name: "xxx"
    },
    tun: undefined,
    tnull: null,
    a: Symbol("a"),
    b: Symbol.for("b"),
    fn: function () { },
    tDate: new Date(),
    treg: /\d/,
};
console.log(JSON.parse(JSON.stringify(obj)));
const obj2 = deepCopy(obj);
console.log(obj2);

深拷贝简单实现.png

结语

深拷贝和浅拷贝还有很多需要学习的地方,共勉~!