JS进阶 - 实现深拷贝

实现 JS 变量的深拷贝

铺垫

首先来了解一下 JS 中的变量赋值。

基本数据类型,其变量的值是存放在栈内存中的,赋值操作就是直接拷贝;

var a = 3;
var b = a;
b++;
console.log(a);  // return 3;
console.log(b);  // return 4;

引用类型,因为值存放于堆内存中,赋值操作其实是拷贝对内存的引用;

var arr = [1,2,3];
var brr = arr;
brr.push(4);
console.log(arr);  // return [1,2,3,4]
console.log(brr);  // return [1,2,3,4]

不过需要注意的一点就是,如果对参数直接进行赋值操作,并不会修改参数的引用地址,但如果对参数的值进行修改,那么会影响参数的值:

var a = [1,2,3];
function foo(x){
    x = [3,2,1];
    x.push(4);
};
foo(a);
console.log(a); // [1,2,3]
var a = [1,2,3];
function foo(x){
    x.push(4);
};
foo(a);
console.log(a); // [1,2,3,4]

原理

对于基本数据类型来说,没有浅拷贝和深拷贝的区别;

对于引用类型来说,需要将其一步步拆解成基本数据类型进行拷贝;

如此循环,就形成了深拷贝。

基础实现

对传入的对象的属性进行循环,依次加到新创建的对象中

function deepClone(source) {
    const target = {};
    for (let key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            target[key] = source[key];
        }
    }
    return target;
}

// 测试用例
const test = {
    a: 1,
    b: {
        c: 2,
        d: 3
    }
}
const instance = deepClone(test);

test.a = 4;
test.b.c = 5;

console.log(instance.a); // 1
console.log(instance.b.c); // 5

缺点:

  1. 没有对传入的数据类型进行判断,对于基础数据类型可以直接返回。
  2. 没有对对象进行深度遍历,对象嵌套对象时,还是会出现浅拷贝。

这里要区分三个方法:for...inObject.keys()Object.getOwnPropertyNames()

for...in:会循环出除了 Symbol 以外的可枚举属性,属性中可能含有来自原型的属性,所以上述代码中用hasOwnProperty判断是否是对象本身的属性;

Object.keys():循环出自身所有可枚举属性,相当于 for…in 和 hasOwnProperty 判断的结合;

Object.getOwnPropertyNames():循环出自身的所有属性(不考虑可枚举属性);

增加判断

function deepClone(source) {
    // 如果 source 为 null/undefined 或者 source 不是引用类型,则直接返回
    if (!source || typeof source !== "object") {
        return source;
    }
    const target = {};
    Object.keys(source).forEach(function(key){
        if (typeof source[key] === "object") {
            target[key] = deepClone(source[key]);
        } else {
            target[key] = source[key];
        } 
    })
    return target;
}

// 测试用例
const test = {
    a: 1,
    b: {
        c: 2,
        d: new RegExp("abc"),
        e: [1,2,3]
    }
};
const instance = deepClone(test);

test.a = 4;
test.b.c = 5;
test.b.e[0] = 4;

console.log(instance.a); // 1
console.log(instance.b.c); // 2
console.log(instance.b.d); // {}
console.log(instance.b.e); // {0:1,1:2,2:3}

从输出可以看到:

  1. 因为正则是特殊的对象,所以他会被再次遍历,然而这是不正确的,所以需要对typeof X === 'object'的情况进行再次的判断,剔除那些特殊的对象;
  2. 对于数组类型,需要进行特殊处理;

处理特殊对象

function cloneRegExp(source) {
    const pattern = source.valueOf();
    let flags = "";
    flags += pattern.global ? "g" : "";
    flags += pattern.ignoreCase ? "i" : "";
    flags += pattern.multiline ? "m" : "";
    return new RegExp(pattern.source, flags);
}

function deepClone(source) {
    if (!source || typeof source !== "object") {
        return source;
    }
    const target = Array.isArray(source) ? [] : {}; // 判断当前是否为数组类型
      Object.keys(source).forEach(function(key){
        if (typeof source[key] === "object") {
            switch (Object.prototype.toString.call(source[key])) {
                case "[object Date]":
                    target[key] = new Date(source[key].valueOf());
                    break;
                case "[object RegExp]":
                    target[key] = cloneRegExp(source[key]);
                    break;
                default:
                    target[key] = deepClone(source[key]);
                    break;
            }
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

// 测试用例
const test = {
    a: new Date(),
    b: new RegExp("abc")
};
const instance = deepClone(test);

console.log(instance.a); // Sun Mar 01 2020 16:19:10 GMT+0800 (中国标准时间)
console.log(instance.b); // /abc/

上面的代码基本解决了对象类型的问题,不过存在循环调用的问题,举个例子:

let a = {};
a.a = a;

如果直接拷贝的话,会出现递归爆栈的情况。

解决循环调用

要解决爆栈的问题,可以使用哈希表,将出现的每个对象都存储在哈希表中,拷贝的时候先查询需要拷贝的对象是否存在表中,如果存在直接取出值返回即可。

哈希表使用 ES6 的 WeakMap 实现:

function cloneRegExp(source) {
    const pattern = source.valueOf();
    let flags = "";
    flags += pattern.global ? "g" : "";
    flags += pattern.ignoreCase ? "i" : "";
    flags += pattern.multiline ? "m" : "";
    return new RegExp(pattern.source, flags);
}

function deepClone(source, hashMap = new WeakMap()) {
    if (!source || typeof source !== "object") {
        return source;
    }
    if (hashMap.has(source)) return hashMap.get(source); // 判断表中是否存在该对象
    const target = Array.isArray(source) ? [] : {};
    hashMap.set(source, target); // 设置对象
      Object.keys(source).forEach(function(key){
        if (typeof source[key] === "object") {
            switch (Object.prototype.toString.call(source[key])) {
                case "[object Date]":
                    target[key] = new Date(source[key].valueOf());
                    break;
                case "[object RegExp]":
                    target[key] = cloneRegExp(source[key]);
                    break;
                default:
                    // 递归时传入当前哈希表
                    target[key] = deepClone(source[key], hashMap); 
                    break;
            }
        } else {
            target[key] = source[key];
        }
    }
    return target;
}
作者

BiteByte

发布于

2020-08-08

更新于

2024-11-15

许可协议