一篇搞定对象的深度克隆

5,127 阅读10分钟

前言

此文解决的痛点:网上有很多关于对象深度克隆的文章资料,但是很多讲得都不太全!

我的目的是:需要深度克隆对象中所有可能出现的数据类型,包括functionsymbol类型!

为了不乱,我将本文结构分成两个部分:

image.png

  1. 梳理几种常见的克隆方式
  2. 手写实现完整的深度克隆,包含所有类型的值,如对象中的functionsymbol。(这部分将是本文重点,也是本文的出现意义所在!)

梳理几种常见的克隆方式

这里简单说下3种克隆对象的方式

image.png

  1. 通过扩展运算符...
var a = {name:"aaa"}
var b = {...a}
  1. 通过合并对象的方法Object.assign
var newObj = Object.assign([],oldObj);
  1. 通过json的方法实现
var obj = {a:1};
var str = JSON.stringify(obj); //序列化对象
var newobj = JSON.parse(str); //还原

他们有各自的缺点,比如:

通过扩展运算符...Object.assign实现的拷贝都只是浅度拷贝,即仅拷贝第一层

通过json的方法实现深度克隆时,如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式。而不是时间对象;如果obj里有RegExpError对象,则序列化的结果将只得到空对象;如果obj里有functionundefined,则序列化的结果会把函数或 undefined丢失;如果obj里有NaNInfinity-Infinity,则序列化的结果会变成null

而且还都没考虑symbol

手写实现深度克隆

思路

我的思路很简单,首先梳理对象中可能出现的数据类型,然后逐一克隆

梳理数据类型

为了不乱,我将JavaScript的数据类型分两大类:

  1. 原始值类型,包括numberstringbooleannullundefined
  2. 引用值类型,包括objectfunctionarray

当然ES6 引入的新的数据类型symbol,也要考虑进来

image.png

下面我们将实现上述8种数据类型的克隆

实现

原始值的克隆

事实上,对于原始值的赋值,本质就是值的拷贝

所以,我们需要将对象中的原始类型属性遍历出来,思路如下:

1.gif

  1. 通过for in遍历出对象身上的所有属性,但是for in会遍历到原型上的
  2. 通过hasOwnProperty过滤掉原型上的属性
  3. 通过typeof除去objectfunctionsymbol,剩下都是直接赋值的原始值,包括numberstringboolean

因此,你要先自己学会这三个技术哦

image.png

实现代码如下:

function deepClone(origin, target) {
    //origin:要被拷贝的对象
    var target = target || {};
    for (var prop in origin) {
        if (origin.hasOwnProperty(prop){
            if (typeof (origin[prop]) === "object") {

            } else if (typeof (origin[prop]) === "function") {

            }else {
                //除了object、function,剩下都是直接赋值的原始值
                target[prop] = origin[prop];
            }
        }
    }
}

由于typeof null的值为object,所以,我们将null放在后面实现

但是这里有个大问题:for...in 循环把不可枚举属性漏掉了`

于是,我们建议用Object.getOwnPropertyNames()代替for...in ,因为它既能使用获取对象身上可枚举和不可枚举的所有属性。但是它拿不到symbol,所以后面需要单独处理symbol类型

于是代码修改如下

function deepClone(origin, target) {
    //origin:要被拷贝的对象
    var target = target || {};
    // 拿出所有属性,包括可枚举的和不可枚举的
    var props = Object.getOwnPropertyNames(origin);
    props.forEach((prop, index) => {
        if (origin.hasOwnProperty(prop)) {
            if (typeof (origin[prop]) === "object") {

            } else if (typeof (origin[prop]) === "function") {

            }  else {
                //除了object、function,剩下都是直接赋值的原始值
                target[prop] = origin[prop];
            }
        }
    });
    //需要单独处理symbol
}

object类型的克隆

我们知道typeofobject的类型除了普通对象,还包括数组和null,这里直接通过古老的方式实现,Object.prototype.toString.call()判断object类型是数组、对象还是null

image.png

代码实现如下:

function deepClone(origin, target) {
    //origin:要被拷贝的对象
    var target = target || {};
    // 拿出所有属性,包括可枚举的和不可枚举的
    var props = Object.getOwnPropertyNames(origin);
    props.forEach((prop, index) => {
        if (origin.hasOwnProperty(prop)) {
            if (typeof (origin[prop]) === "object") {
                if (Object.prototype.toString.call(origin[prop]) == "[object Array]") {
                    //数组
                } else if (Object.prototype.toString.call(origin[prop]) == "[object Object]") {
                    //普通对象
                } else {
                    //null
                }
            } else if (typeof (origin[prop]) === "function") {

            } else {
                //除了object、function,剩下都是直接赋值的原始值
                target[prop] = origin[prop];
            }
        }
    });
    //需要单独处理symbol
}

接着,根据是数组还是对象建立相应的数组或对象;但是因为数组和对象一样,可以存放所以类型的变量,所以这两种数据类型得用到递归,调用本身函数deepClone()。如下:

function deepClone(origin, target) {
    //origin:要被拷贝的对象
    var target = target || {};
    // 拿出所有属性,包括可枚举的和不可枚举的
    var props = Object.getOwnPropertyNames(origin);
    props.forEach((prop, index) => {
        if (origin.hasOwnProperty(prop)) {
            if (typeof (origin[prop]) === "object") {
                if (Object.prototype.toString.call(origin[prop]) == "[object Array]") {
                    //数组
                    target[prop] = [];
                    deepClone(origin[prop],target[prop]);
                } else if (Object.prototype.toString.call(origin[prop]) == "[object Object]") {
                    //普通对象
                    target[prop] = {};
                    deepClone(origin[prop],target[prop]);
                } else {
                    //null
                    origin[prop] = null;
                }
            } else if (typeof (origin[prop]) === "function") {

            } else {
                //除了object、function,剩下都是直接赋值的原始值
                target[prop] = origin[prop];
            }
        }
    });
    //需要单独处理symbol    
}

于是我们完成了object类型的克隆,接下来克隆function

克隆function

有两种方法克隆function

image.png

通过eval()new Function()都可以实现,如下:

var a = function(){alert(1)}
var b = eval("0,"+a);//方法一
var c = new Function("return "+a)();//方法二

b();//1
c();//1
alert(a === b);//false
alert(a === c);//false

如果函数上面附有许多静态属性呢?我可以通过for in拿出所有静态属性,并赋值给target。

为了方便,我们可以封装一个专门的函数来实现函数copyFn的深度拷贝,代码如下:

var copyFn = function (fn) {
    var result = eval("0," + fn);
    for (var i in fn) {
        result[i] = fn[i]
    }
    return result
}

上述代码虽然可以拷贝函数,也可以拷贝函数的静态属性,但问题是仅拷贝为原始值类型的属性。于是我们需要给每个属性做深度克隆,即即给函数静态属性递归

var copyFn = function (fn) {
    var result = eval("0," + fn);
    for (var i in fn) {
        deepClone[fn[i]]
    }
    return result
}

最后赋值将结果赋值给target对应的值,于是目前完整代码如下:

function deepClone(origin, target) {
    //origin:要被拷贝的对象
    var target = target || {};
    // 拿出所有属性,包括可枚举的和不可枚举的
    var props = Object.getOwnPropertyNames(origin);
    props.forEach((prop, index) => {
        if (origin.hasOwnProperty(prop)) {
            if (typeof (origin[prop]) === "object") {
                if (Object.prototype.toString.call(origin[prop]) == "[object Array]") {
                    //数组
                    target[prop] = [];
                    deepClone(origin[prop], target[prop]);
                } else if (Object.prototype.toString.call(origin[prop]) == "[object Object]") {
                    //普通对象
                    target[prop] = {};
                    deepClone(origin[prop], target[prop]);
                } else {
                    //null
                    origin[prop] = null;
                }
            } else if (typeof (origin[prop]) === "function") {
                var _copyFn = function (fn) {
                    var result = new Function("return " + fn)();
                    for (var i in fn) {
                        deepClone[fn[i]]
                    }
                    return result
                }
                target[prop] = _copyFn(origin[prop]);
            } else {
                //除了object、function,剩下都是直接赋值的原始值
                target[prop] = origin[prop];
            }
        }
    });
    //需要单独处理symbol    
}

经上所述,我们实现了function类型的克隆,最后再来实现symbol类型的拷贝

克隆symbol

为了看上去不乱,我将此处分成两部分:

image.png

了解symbol

SymbolES6引入了一种新的数据类型,用于表示独一无二的值

它通过Symbol函数生成,但不是new Symbol如:

let s = Symbol();
typeof s;// "symbol"

它类似字符串,所以,对象的属性名可以是字符串也可以是symbol类型。但symbol有一个好处是,凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突!

为了容易区分,Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。如下:

let s1 = Symbol('foo');
let s2 = Symbol('bar');

console.log(s1);// Symbol(foo)
console.log(s2);// Symbol(bar)

而且,Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。

var a = Symbol("a");
var b = Symbol("a");
console.log(a == b);//false

b = a;
console.log(a == b);//true

它就像字符一样,可以作为对象的key。那我们看看主线,看看具体如何拷贝 Symol 类型呢?

实现克隆symbol

获取所有symbol

最开始我使用for in,结果掉坑里了,但是发现for in并不能找出后面添加的symbol值(可以自己试试)。很快我就想到了Object.getOwnPropertySymbols(obj),它获得obj对象身上的所有Symbol 类型的属性。但需要注意的是,当如果没有则返回一个空的数组。如下

具体实现

首先通过getOwnPropertySymbols获取到所有symbol,如果有symbol类型的属性,则将所有symbol属性进行深度克隆并赋值。

实现代码如下:

var symKeys = Object.getOwnPropertySymbols(origin);
if (symKeys.length) { 
    symKeys.forEach(symKey => {
        target[symKey] = deepClone(obj[symKey]);   
    });
}

于是我们实现了symbol的克隆,代码如下:

function deepClone(origin, target) {
    //origin:要被拷贝的对象
    var target = target || {};
    // 拿出所有属性,包括可枚举的和不可枚举的
    var props = Object.getOwnPropertyNames(origin);
    props.forEach((prop, index) => {
        if (origin.hasOwnProperty(prop)) {
            if (typeof (origin[prop]) === "object") {
                if (Object.prototype.toString.call(origin[prop]) == "[object Array]") {
                    //数组
                    target[prop] = [];
                    deepClone(origin[prop], target[prop]);
                } else if (Object.prototype.toString.call(origin[prop]) == "[object Object]") {
                    //普通对象
                    target[prop] = {};
                    deepClone(origin[prop], target[prop]);
                } else {
                    //null
                    origin[prop] = null;
                }
            } else if (typeof (origin[prop]) === "function") {
                var _copyFn = function (fn) {
                    var result = new Function("return " + fn)();
                    for (var i in fn) {
                        deepClone[fn[i]]
                    }
                    return result
                }
                target[prop] = _copyFn(origin[prop]);
            } else {
                //除了object、function,剩下都是直接赋值的原始值
                target[prop] = origin[prop];
            }
        }
    });
    //处理symbol类型
    var symKeys = Object.getOwnPropertySymbols(origin);
    if (symKeys.length) {
        symKeys.forEach(symKey => {
            target[symKey] = deepClone(obj[symKey]);
        });
    }
}

完善优化

优化1: 特殊情况处理

if (origin == null) return origin;  //null 和 undefined 都不用处理
if (origin instanceof Date) return new Date(origin);
if (origin instanceof RegExp) return new RegExp(origin);
if (typeof origin !== 'object') return origin;  // 普通常量直接返回

优化2:处理内存溢出问题

如下情况,可能导致内存溢出:

 var ff = {
     name: "alice",
 }
 ff.aaa = ff
 console.log(ff)
 var c = deepClone(ff)//内存溢出

其实解决循环引用的思路,就是在赋值之前判断当前值是否已经存在,避免循环引用,这里我们可以使用WeakMap来生成一个hash

当出现上述情况时,直接返回拷贝过的对象

if (hash.has(origin)) return hash.get(origin);

优化:3 处理对象中日期对象或者正则对象

增加两个判断即可完成正则和日期对象的拷贝,如下

//...
else if(origin[prop] instanceof Date){
    // 处理日期对象
    target[prop] = new Date(origin[prop])
}else if(origin[prop] instanceof RegExp){
    // 处理正则对象
    target[prop] = new RegExp(origin[prop])
} 
//...

于是我们有了最终的代码

最终代码

function deepClone(origin, target, hash = new WeakMap()) {
    //origin:要被拷贝的对象
    // 需要完善,克隆的结果和之前保持相同的所属类
    var target = target || {};

    // 处理特殊情况
    if (origin == null) return origin;  //null 和 undefined 都不用处理
    if (origin instanceof Date) return new Date(origin);
    if (origin instanceof RegExp) return new RegExp(origin);
    if (typeof origin !== 'object') return origin;  // 普通常量直接返回

    //  防止对象中的循环引用爆栈,把拷贝过的对象直接返还即可
    if (hash.has(origin)) return hash.get(origin);
    hash.set(origin, target)  // 制作一个映射表

    // 拿出所有属性,包括可枚举的和不可枚举的,但不能拿到symbol类型
    var props = Object.getOwnPropertyNames(origin);
    props.forEach((prop, index) => {
        if (origin.hasOwnProperty(prop)) {
            if (typeof (origin[prop]) === "object") {
                if (Object.prototype.toString.call(origin[prop]) == "[object Array]") {
                    //数组                            
                    target[prop] = [];
                    deepClone(origin[prop], target[prop], hash);
                } else if (Object.prototype.toString.call(origin[prop]) == "[object Object]") {
                    //普通对象 
                    target[prop] = {};

                    deepClone(origin[prop], target[prop], hash);
                } else if (origin[prop] instanceof Date) {
                    // 处理日期对象
                    target[prop] = new Date(origin[prop])
                } else if (origin[prop] instanceof RegExp) {
                    // 处理正则对象
                    target[prop] = new RegExp(origin[prop])
                } else {
                    //null                                                
                    target[prop] = null;
                }
            } else if (typeof (origin[prop]) === "function") {
                var _copyFn = function (fn) {
                    var result = new Function("return " + fn)();
                    for (var i in fn) {
                        deepClone[fn[i], result[i], hash]
                    }
                    return result
                }
                target[prop] = _copyFn(origin[prop]);
            } else {
                //除了object、function,剩下都是直接赋值的原始值
                target[prop] = origin[prop];
            }
        }
    });

    // 单独处理symbol            
    var symKeys = Object.getOwnPropertySymbols(origin);
    if (symKeys.length) {
        symKeys.forEach(symKey => {
            target[symKey] = origin[symKey];
        });
    }
    return target;
}

测试代码

let s1 = Symbol('s1');

let obj = {
    a: '100',
    b: undefined,
    c: null,
    d: Symbol(2),
    e: /^\d+$/,
    f: new Date,
    g: true,
    arr: [10, 20, 30],
    school: {
        name: 'cherry',
        [s1]: 's1'
    },
    fn: function fn() {
        console.log('fn');
    }
}

obj.h = obj;
let obj2 = deepClone(obj);
console.log(obj, obj2);

image.png

总体实现,仅遗留了这个问题,也就是处理内存溢出时没处理好,这点待完善!其他应该没问题了

总结

首先我们梳理了三种常用的方式实现克隆,分别是扩展运算符、assignjson,但都有各种的问题。于是我们手动实现了一个克隆方法deepclone,它实现的思路是,首先将对象所有可能出现的类型列出来,然后对应完成各自的克隆。

为了让你更清楚,我将整个过程绘制如下图:

2.gif

image.png

END

如有问题,或者理解不到的位的地方,烦请留言告知,感谢!