JavaScript面向对象系列之对象的深浅拷贝

let obj = {
    url: '/api/list',
    method: 'GET',
    cache: false,
    timeout: 1000,
    key: Symbol('KEY'),
    big: 10n,
    n: null,
    u: undefined,
    headers: {
        'Content-Type': 'application/json',
        post: {
            'X-Token': 'xxx'
        }
    },
    arr: [10, 20, 30],
    reg: /^\d+$/,
    time: new Date(),
    fn: function() {
        console.log(this);
    },
    err: new Error('xxx')
};
obj.obj = obj;
复制代码

假设有以上一个对象,包含各种各样类型的属性,并且存在循环引用,那么分别对以上对象进行深浅拷贝会出现什么样的问题?

浅拷贝

仅仅复制对象的第一层。

对象的第一层如果还是一个对象,那么存的是对象的地址,所以如果使用浅拷贝,从第二层开始,就还是原来对象的属性,如果对拷贝完的对象的第二层对象属性进行修改,那么原来的对象的相应属性也会改变

例如对以上对象进行浅拷贝,那么 headers 属性在两个对象上都引用的是同一个内存地址

image。png

浅拷贝几个方法

Object.assign

let new_obj = Object.assign({}, obj);
复制代码

扩展运算符 ...

展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign () 的功能相同。

let new_obj = {
    ...obj
};
复制代码

遍历

function clone(target) {
    let cloneTarget = {};
    for (const key in target) {
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
};
复制代码

数组的浅拷贝

数组本质上也是对象的一种,也有几个可以进行浅拷贝的简单方法

扩展运算符 ...
let arr = [1, 3, {
    username: ' kobe'
}];
let arr2 = [...arr]
arr2[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]
复制代码
Array.prototype.concat() / Array.prototype.slice()

原理本质上是遍历

let arr = [1, 3, {
    username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
复制代码
let arr = [1, 3, {
    username: 'kobe'
}];
let arr2 = arr.slice();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
复制代码

深拷贝

深拷贝是将一个对象从内存中完整的拷贝一份出来,不仅仅拷贝对象的引用,还拷贝各个层级的对象的引用。从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

实现深拷贝的简单方法

let newObj = JSON.parse(JSON.stringify(obj))
复制代码

JSON.stringify :把对象/数组变为JSON字符串
JSON.parse :把JSON字符串变为对象/数组(浏览器需要重新开辟所有内存)

弊端:

  • 不允许出现循环引用,不然会直接报错

  • 属性值不能是 BigInt ,会直接报错 'Uncaught TypeError: Do not know how to serialize a BigInt'

  • 只要属性值是 symbol / undefined / function 这些类型的,会直接丢失

  • 还有信息不准确的,例如:正则->空对象,Error对象->空对象,日期对象->字符串

image。png

手写一个深拷贝

原理:遍历所有属性,遇到对象就递归处理,遇到基本类型就直接复制,特殊类型特殊处理

递归实现

原理在注释

function deepClone(obj) {
    if (obj === null) return obj; // 如果是null直接返回

    if (obj instanceof Date) return new Date(obj); //date特殊处理
    if (obj instanceof RegExp) return new RegExp(obj); //正则特殊处理
    if (obj instanceof Error) return new Error(obj.message);//Error 特殊处理
    
    //如果是函数的话是不需要深拷贝,如果非要处理:
    if (typeof obj === "function") {
            return function () {
                return obj.apply(this, arguments);
            };
        }
    //原始值类型的值处理 原始值类型直接返回(包括Symbal)
    if (typeof obj !== "object") return obj;

    // 是对象的话就要进行深拷贝
    let cloneObj = new obj.constructor();
    // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            // 实现一个递归拷贝
            cloneObj[key] = deepClone(obj[key]);
        }
    }
    return cloneObj;
}
let obj = {
    name: 1,
    address: {
        x: 100
    }
};
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);
复制代码

解决循环引用

参考

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回对应的拷贝的引用,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

将解决循环引用的思路单独写出来

  1. 首先将要拷贝的对象和已拷贝的对象做一个引用的映射 map.set(target, cloneTarget) (需要拷贝的对象的引用<—>已经拷贝对象的引用)
  2. 然后判断。当传入的引用是自己的时候,那么可以根据 map.get(target) 判断出已经有拷贝的映射
  3. 返回那个映射的已拷贝对象的引用 return map.get(target)
function clone(target, map = new Map()) {
    if (typeof target === 'object') {
        let cloneTarget = {};
        if (map.get(target)) { //步骤3判断出应用的使自己
            return map.get(target);
        }
        map.set(target, cloneTarget); //步骤1
        for (const key in target) {
            cloneTarget[key] = clone(target[key], map); //步骤2,这里传入了自己的引用
        }
        return cloneTarget;
    } else {
        return target;
    }
};

const target = {};
target.xxx = target;

console.log(clone(target))
复制代码

知识点:

ES6 Map数据结构

只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。

知识点二:为什么要用 WeakMap参考

了解原理之后的代码:

function deepClone(obj, hash = new WeakMap()) {
    if (obj === null) return obj;
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    if (obj instanceof Error) return new Error(obj.message);
    if (typeof obj === "function") {
            return function () {
                return obj.apply(this, arguments);
            };
        }
    if (typeof obj !== "object") return obj;
    if (hash.get(obj)) return hash.get(obj);
    let cloneObj = new obj.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);
复制代码

参考:jueji.cn/post/684490…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享