深拷贝和浅拷贝

130 阅读4分钟

详参:如何写出一个惊艳面试官的深拷贝? 作者:ConardLi

浅拷贝

创建一个新对象,这个对象拷贝原始对象属性值。如果属性值是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址。

深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

手写深拷贝

丐版 -- 简单,但拷贝其他引用类型、拷贝函数、循环引用中有很大缺陷

JSON.parse(JSON.stringify());

01. 基础版本

// 浅拷贝易写出
function clone(target) {
    let cloneTarget = {};
    for (const key in target) {
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
}

// 深拷贝,不确定待拷贝的对象有多少层深度,可用递归解决
// 如果是原始类型,无需拷贝,直接返回;
// 引用类型,创建一个新对象,遍历需要克隆的对象,将需克隆对象的属性执行深拷贝后依次添加到新对象上。
function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
}

同样有缺陷,没考虑到数组

02.兼容数组

function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
}

03.兼容对象循环引用

即对象的属性间接或直接的引用了自身的情况

// 测试用例
const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
}
target.target = target;

解决循环引用问题,可额外开辟一个存储空间存储当前对象和拷贝对象的对应关系。

当需要拷贝当前对象时,先去存储空间中找是否拷贝过这个对象。

有 -- 直接返回;没有 -- 继续拷贝

function clone(target, map = new Map()) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);
        for (const key in target) {
            cloneTarget[key] = clone(target[key], map);
        }
        return cloneTarget;
    } else {
        return target;
    }
}

可用 WeakMap 代替 Map ,画龙点睛

Map -- 对象间存在强引用关系,只有手动 obj = null,它才会被垃圾回收机制进行回收

WeakMap -- 弱引用对象,垃圾回收机制会自动帮我们回收

function clone(target, map = new WeakMap()) {
		// ...
}

04. 性能优化

for...in 在遍历时效率非常低

执行效率 while > for > for...in

// 用 while 实现一个通用的 forEach 遍历,iteratee是遍历的回调函数
function forEach(array, iteratee) {
    let index = -1;
    const length = array.length;
    while( ++index < length) {
        iteratee(array[index], index);
    }
    return array;
}

function clone(target, map = new WeakMap()) {
    if (typeof target === 'object') {
        const isArray = Array.isArray(target);
        let cloneTarget = isArray ? [] : {};

        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);

        const keys = isArray ? undefined : Object.keys(target);
        forEach(keys || target, (value, key) => {
            if (keys) {
                key = value;
            }
            cloneTarget[key] = clone(target[key], map);
        });
        return cloneTarget;
    } else {
        return target;
    }
}

05. 其他数据类型

上述只考虑了普通的 objectarray 两种数据类型,引用类型还有很多

首先,判断是否为引用类型,还需考虑 functionnull 两种特殊的数据类型

function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}
if (!isObject(target)) {
  return target;
}
// ...

获取数据类型

可以使用 toString 来获取准确的引用类型

可直接调用 Object 原型上未被覆盖的 toString() 方法,使用 call 来改变 this 指向来达到效果。

function getType(target) {
    return Object.prototype.toString.call(target);
}
value //Object.prototype.toString.call(value)结果
true[Object Boolean]
123[Object Number]
'cc'[Object String]
null[Object Null]
undefined[Object Undefined]
Symbol()[Object Symbol]
{}[Object Object]
function(){}[Object Function]
[][Object Array]
new Error()[Object Error]
new RegExp()[Object RegExp]
Math[Object Math]
JSON[Object JSON]
new document[Object RegExp]
window[Object global]

可将上面的集中类型分为:可继续遍历的类型不可继续遍历的类型

可继续遍历的类型

objectarrayMapSet

获取它们的初始化数据,可通过拿到 constructor 来获取

function getInit(target) {
  const Ctor = target.constructor;
  return new Ctor();
}

改写 clone 函数

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];

function clone(target, map = new WeakMap()) {
    // 克隆原始类型
    if (!isObject(target)) {
        return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target, type);
    } else {
        return cloneOtherType(target, type);
    }

    // 防止循环引用
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, cloneTarget);

    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(clone(value, map));
        });
        return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value, map));
        });
        return cloneTarget;
    }

    // 克隆对象和数组
    const keys = type === arrayTag ? undefined : Object.keys(target);
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value;
        }
        cloneTarget[key] = clone(target[key], map);
    });
    return cloneTarget;
}

不可继续遍历的类型

BoolNumberStringDateError 这几种可直接用构造函数和原始数据创建一个新对象。

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

function cloneOtherType(target, type) {
    const Ctor = target.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(target);
        case regexpTag:
            return cloneReg(target);
        case symbolTag:
            return cloneSymbol(target);
        default:
            return null;
    }
}

function cloneSymbol(target) {
    return Object(Symbol.prototype.valueOf.call(target));
}

function cloneReg(target) {
    const reFlags = /\w*$/;
    const result = new target.constructor(target.source, reFlags.exec(target));
    result.lastIndex = target.lastIndex;
    return result;
}

克隆函数

function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        console.log('普通函数');
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            console.log('匹配到函数体:', body[0]);
            if (param) {
                const paramArr = param[0].split(',');
                console.log('匹配到参数:', paramArr);
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}