一篇彻底搞定对象的深度克隆 | 包括function和symbol类型

这是我参与更文挑战的第16天,活动详情查看: 更文挑战

前言

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

我的目的是:需要深度克隆对象中所有可能出现的数据类型,包括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 if (typeof (origin[prop]) === "symbol") {

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

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

object类型的克隆

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

image.png

代码实现如下:

function deepClone(origin, target) {
    //origin:要被拷贝的对象
    var target = target || {};
    for (var prop in origin) {
        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 if (typeof (origin[prop]) === "symbol") {

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

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

function deepClone(origin, target) {
    //origin:要被拷贝的对象
    var target = target || {};
    for (var prop in origin) {
        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 if (typeof (origin[prop]) === "symbol") {

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

这里有些人会有个疑问,就是当是数组时,我递归调用deepClone(origin[prop],target[prop]);传递的数组能使用for in遍历?答案是肯定的!for in也可以遍历数组,origin[prop]相当于是origin[0]origin[1]origin[2]origin[3]….

完成了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
复制代码

但是如果函数上面附有许多静态属性,我们可以封装一个专门的函数来实现函数copyFn的深度拷贝:

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

最后,实现symbol类型的拷贝

克隆symbol

温馨提示:不考虑拷贝symbol类型的可以跳过这部分

为了看上去不乱,我分成两部分来说

image.png

了解symbol

先来了解一下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)

console.log(s1.toString()); // "Symbol(foo)"
console.log(s2.toString()); // "Symbol(bar)"
复制代码

可以看到,s1相当于s1.toString()

但是如果Symbol的参数是一个对象,就会调用该对象的toString方法(没有就找原型上的toString方法),将其转为字符串,然后才生成一个 Symbol 值,如下:

const obj = {
  toString() {
    return 'abc';
  }
};
const sym = Symbol(obj);
console.log(sym); // Symbol(abc)
复制代码

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

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

b = a;
console.log(a == b);//true
复制代码

有了上面基础,下面我们看看如何拷贝 Symol 类型呢?

实现克隆symbol

获取所有symbol

通过Object.getOwnPropertySymbols(obj)获得obj对象身上的所有Symbol 类型的属性

思考一下,为什么使用getOwnPropertySymbols不是使用for in?

答案是,for in并不能找出后面添加的symbol值,而getOwnPropertySymbols可以,这点挺有意思,可自己试试!

使用getOwnPropertySymbols需要注意一点,因为所有的对象在初始化的时候不会包含任何的 Symbol,除非你在对象上赋值了 Symbol 否则Object.getOwnPropertySymbols()只会返回一个空的数组。如下

var obj = {};
var a = Symbol("a");
var b = Symbol.for("b");

obj[a] = "localSymbol";
obj[b] = "globalSymbol";

var objectSymbols = Object.getOwnPropertySymbols(obj);

console.log(objectSymbols.length); // 2
console.log(objectSymbols)         // [Symbol(a), Symbol(b)]
console.log(objectSymbols[0])      // Symbol(a)
复制代码

那具体怎么拷贝呢?

我认为,反正拷贝的结果就是看上去和功能都是一模一样,但是彼此独立不关联。而symbol的特性就是独一无二,所以天生不关联,所以我们只要考虑生成的时候传递的一样就行,即Symbol(context)中的context值。

获取context

那我们是不是可以通过toString来获取context呢?
上面我们讲过toString方法,我们深入试试传入不同类型时,返回的值有什么区别:

//Symbol传入不同类型的值
var _string = 'aa';
var _number = 1;
var _boolean = true;
var _undefined = undefined;
var _null= null;
var _array = [1,2];
var _fn = function(){a:1};
var _obj = {name:"alice"};
var _symbol = Symbol("s");
console.log(Symbol(_string).toString()); //Symbol(aa)
console.log(Symbol(_number).toString()); //Symbol(1)
console.log(Symbol(_boolean).toString()); //Symbol(true)
console.log(Symbol(_undefined).toString()); //Symbol()
console.log(Symbol(_null).toString()); //Symbol(null)
console.log(Symbol(_fn).toString()); //Symbol(function(){a:1})
console.log(Symbol(_obj).toString()); //Symbol([object Object])
console.log(Symbol(_symbol).toString()); //test.html:431 Uncaught TypeError: Cannot convert a Symbol value to a string
复制代码

我们通过Symbol传入不同类型的值,发现Symbol传入的值必须是字符串,如果不是,会调用String()发生隐式类型转换,将其转成字符串。特殊的Symbol(undefined).toString())返回是Symbol()

于是我们可以写个方法copySymbol来克隆symbol,如下:

function copySymbol (val){
    var str = val.toString();
    var tempArr = str.split("(");
    var arr = tempArr[1].split(")")[0];
    return Symbol(arr); 
}
复制代码

最终代码

于是,我们有了一个深度克隆对象的最终代码,如下:

function deepClone(origin, target) {
    //origin:要被拷贝的对象
    var target = target || {};
    for (var prop in origin) {
        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
                    target[prop] = null;
                }

            } else if (typeof (origin[prop]) === "function") {//函数
                var _copyFn = function (fn) {
                    var result = new Function("return " + fn)();
                    for (var i in fn) {
                        result[i] = fn[i]
                    }
                    return result
                }
                target[prop] = _copyFn(origin[prop]);
            } else if (typeof (origin[prop]) === "symbol") {//里面的symbol                           
                target[prop] = _copySymbol(origin[prop]);
            } else {
                //除了object、function、symbol,剩下都是直接赋值的原始值number,string,boolean,undefined                        
                target[prop] = origin[prop];
            }
        }
    }
    function _copySymbol(val) {
        var str = val.toString();
        var tempArr = str.split("(");
        var arr = tempArr[1].split(")")[0];
        return Symbol(arr);
    }
    //通过getOwnPropertySymbols找出来的symbol
    var _symArr = Object.getOwnPropertySymbols(origin);   
    if (_symArr.length) {//查找成功
        _symArr.forEach(symKey => {                   
                target[symKey] = origin[symKey];                    
        });
    }
    return target;
}
复制代码

测试一下

var student = {
    name: "alice",
    age: 12,
    isOldPerson: false,
    sex: undefined,
    money: null,
    grader: [{
        English: 120,
        math: 80
    }, 100],
    study: function () {
        console.log("I am a student,I hava to study every day!")
    },
    key: Symbol("s1-key"),
    book: {
        English: true
    }
}
var a = Symbol('a')
var b = Symbol.for("b")
student[a] = "1111111111111"
student[b] = "222222222222222"
var res = deepClone(student);
复制代码

测试结果

完美实现!

总结

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

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

2.gif

image.png

END

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

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