js深浅拷贝梳理

79 阅读3分钟
const originArray = [1, 2, [6, 7], 3, 4, 5];
let shallowCopyArr = [];
let deepCopyArr = [];

const originObject = { a: 1, b: {c: 3} };
let shallowCopyObj = {};
let deepCopyObj = {}

const arrayLike = {
  0: "AAA",
  1: [1, 2],
  2: "BBB",
  length: 3
};
let shallowCopyArrLike = [];
let deepCopyArrLike = [];

1. 浅拷贝

引用的复制

  • 浅拷贝实现:赋值操作符

  • "首层浅拷贝"实现:

对象:遍历、Object.assign、扩展运算符

Object.assign拷贝注意事项

  • 只会拷贝源对象的自身可枚举属性
  • 借助源对象get和目标对象set
  • 源对象为基本类型时会被包装为对象(null, undefined被忽略)

数组(类数组对象):遍历、扩展运算符、Array.from、slice、concat、filter、map、reduce等数组方法 (注意:所有的数组标准方法都适用于数组和类数组对象,只有concat例外, 具体使用参考js伪数组对象)

// 对象以遍历为例,数组以concat为例
for (const prop in originObject) {
    if (originObject.hasOwnProperty(prop)) {
        shallowCopyObj[prop] = originObject[prop];
    }
}
shallowCopyObj.a = 2;
shallowCopyObj.b.c = 4;
shallowCopyObj  // { a: 2, b: {c: 4} }
originObject    // { a: 1, b: {c: 4} }

------------------------------------------

arrayLike[Symbol.isConcatSpreadable] = true;
shallowCopyArrLike = [].concat.call(arrayLike, [7, 8]);
shallowCopyArrLike.push(8)
shallowCopyArrLike[1].push(3)
shallowCopyArrLike   // ['AAA', [1,2,3], 'BBB', 7, 8, 8]
arrayLike
// const arrayLike = {
//  0: "AAA",
//  1: [1, 2, 3],
//  2: "BBB",
//  length: 3
// };

2. 深拷贝

堆内存的重新分配

实现:对象、数组、类数组对象通用

  • 递归
  • JSON.parse, JSON.stringify
  • jquery.extend()

JSON.parse, JSON.stringify实现:

const originObject = { b: { c: 3 }, d: undefined, e: Array, f: Symbol('foo'), [Symbol("foo")]: "foo" };
const originArray = [[6, 7], 5, undefined, Array, new Boolean(false)];
const originException = { a: new Date(2006, 0, 2, 15, 4, 5), b: new Error('xxx'), c: new RegExp("\\w+") };

deepCopyObj = JSON.parse(JSON.stringify(originObject));
deepCopyArr = JSON.parse(JSON.stringify(originArray));
deepCopyExp = JSON.parse(JSON.stringify(originException));

deepCopyObj.b.c = 4;
deepCopyObj  // { b: {c: 4} }
deepCopyArr  // [[6, 7], 5, null, null, false]
deepCopyExp  // { a: '2006-01-02T07:04:05.000Z', b: {}, c: {} }

实现限制:
1. 非数组对象的属性值为undefined、函数、Symbol值(含作为键名情况)时在序列化时被忽略, 数组中会转化为null
2. 无法处理循环引用的对象或转换BigInt类型的值
3. 一些特定的其他限制,例如Map/Set/WeakMap/WeakSet等仅序列化可枚举属性(递归使用for...in可遍历原型链上的属性,避免该问题)
4. NaN, null转为null
5. Date对象转换为字符串,正则对象、Error对象转换为空对象

递归实现:.....

关键点在于

  • 拷贝环问题
  • Date、Reg等引用类型的拷贝
  • 函数的拷贝

(1) 解决拷贝环问题

使用Map/WeakMap,推荐使用WeakMap

const obj = {
    a:1,
    c: [1,2],
    d: {
        e: 1,
    },
    f: null,
};
obj.g = obj;
function isObject(target) {
  return (typeof target === 'object' || typeof target === 'function') && target !== null
}
function clone(target) {
    // 使用WeakMap结构存储当前对象和拷贝对象的对应关系,当碰到循环引用的对象时,通过存储的对应关系进行复现
    // WeakMap对键名的引用为弱引用,有利于垃圾回收机制
    const vm = new WeakMap();
    function deepClone(target) {
        if (isObject(target)) {
            if (vm.has(target)) return vm.get(target);
            let cloneTarget = Array.isArray(target) ? [] : {};
            vm.set(target, cloneTarget);
            for (const key in target) {
                cloneTarget[key] = deepClone(target[key]);
            }
            return cloneTarget;
        } else {
            return target;
        }
    };
    return deepClone(target);
}
const obj1 = clone(obj)
obj1.c[1] = 5;
obj1.f = [1,2];
console.log(obj1)  // {a: 1, c: [1,5], d: {e: 1}, f: [1,2], g: {...} }

(2) Date、Reg、Error等一些其他引用类型的拷贝

使用constructor/Object.prototype.toString.call判断类型,根据不同引用类型做相应处理 未完待续........

(3) 函数、不可枚举属性等的拷贝

使用现成的工具库lodash.cloneDeep

jquery.extend实现:

排除window对象、dom对象、null、undefined、通过继承方式创建的对象

jQuery.extend(true, target, object1 [, objectN ] )

参考链接: JSON序列化