美文网首页
JS的浅克隆和深克隆

JS的浅克隆和深克隆

作者: 十八岁的天空_b2de | 来源:发表于2020-07-29 15:27 被阅读0次

    一说到深层克隆,大家多会跃跃欲试的表明,大家都会,但是大家知道真 正的深层克隆是什么样子的么,本篇文章将会告诉你。

    浅层克隆与深度克隆

    浅层克隆也被称为浅克隆,浅克隆之所以被称为浅克隆,是因为对象只会被克隆最外部的一层,至于更深层的象,则依然是通过引用指向同一块堆内存.

    浅层克隆代码实现:

    function shallowClone(o) {
     const obj = {};
     for ( let i in o) {
     obj[i] = o[i];
     }
     return obj;
     }
     // 被克隆对象
     const oldObj = {
     name: 'pwd',
     list: [ 'e', 'f', 'g' ],
     obj: { h: { i: 2 } }
     };
     
     const newObj = shallowClone(oldObj);
     console.log(newObj.obj.h, oldObj.obj.h); // { i: 2 } 
    { i: 2 }
     console.log(oldObj.obj.h === newObj.obj.h); // true
    

    我们可以看到,很明显,虽然 oldObj.obj.h 被克隆了,但是它还与 oldObj.obj.h相等,这表明他们依然指向同一段堆内存,这就造成了如果对 newObj.obj.h进行修改,也会影响 oldObj.obj.h,这就不是一版好的克隆.

    newObj.c.h.i = 'change';
    console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 'change' }
    

    我们改变了newObj.c.h.i的值,oldObj.c.h.i也被改变了,这就是浅克隆的问题所在.
    当然有一个新的 API :Object.assign()也可以实现浅复制,但是效果跟上面没有差别,所以我们不再细说了。
    在上面很明显我们想要的克隆,浅层克隆是远远不够的,我们的目标致力于:两个长的一模一样的对象,但是彼此之间没有关联。那么接下来开始我们的重头戏——深层克隆
    废话不多说,先上代码(大家最最常见的深层克隆代码):

    常见深层克隆

    // 克隆函数
    function deepClone(obj, newObj) {
        if (obj instanceof Array) {
            // 判断是否是数组
            newObj = [];
            return deepCloneArray(obj, newObj);
        } else if (obj instanceof Object) {
            // 判断是否是对象
            newObj = {};
            return deepCloneObject(obj, newObj);
        } else {
            return (newObj = obj);
        }
    }
    
    // 克隆对象
    function deepCloneObject(obj, newObj) {
        for (var temp in obj) {
            if (obj.hasOwnProperty(temp)) { // 过滤原型属性
                if (obj[temp] instanceof Object || obj[temp] instanceof Array) { // 如果还是对象或者数组继续递归深层克隆
                    var tempNewObj = {};
                    newObj[temp] = deepClone(obj[temp],
                        tempNewObj);
                } else { // 不是直接赋值
                    newObj[temp] = obj[temp];
                }
            }
        }
        return newObj;
    }
    
    // 克隆数组
    function deepCloneArray(arr, newArr) {
        for (var i = 0; i < arr.length; i++) {
            if (arr[i] instanceof Object || arr[i] instanceof Array) { // 如果还是对象或者数组继续递归深层克隆
                var tempNewObj;
                newArr[i] = deepClone(arr[i], tempNewObj);
            } else { // 不是直接赋值
                newArr[i] = arr[i];
            }
        }
        return newArr;
    }
    

    这个代码是大家见到最常见的 “克隆代码”,对于一些简单的克隆是可以的,比如下面的:

    var obj = {
        name: "panda",
        sex: 18,
        msg: {
            a: 1,
            b: 2
        },
        list: [1, 2, 3, 4]
    }
    var obj1 = deepClone(obj)
    console.log(obj1.list, obj.list) // [ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ]
    console.log(obj1.list == obj.list) // false
    

    以上的克隆对于一般数组和对象有效,但是我们的工作中对象不单纯只有普通对象把。当我们遇到函数,正则,日期对象会怎么样,看以下的问题:
    1. 可以克隆函数么?
    2. 可以克隆正则,Date 对象么?
    3. 可以克隆原型么?
    4. 可以解决循环引用么(环)
    我们来测试一下
    克隆函数:

    var obj = {fn: function () {}}
    var newObj = deepClone(obj)
    console.log(newObj, obj)
    //结果:{ fn: {} } { fn: [Function: fn] } 失败。
    

    克隆正则

    var obj = /abc/g
    var newObj = deepClone(obj)
    console.log(newObj, obj)
    // 结果: {} /abc/g, 失败。
    

    克隆 Date 对象

    var obj = new Date()
    var newObj = deepClone(obj)
    console.log(newObj, obj)
    // 结果: {} 2019-04-23T05:47:22.133Z 失败。
    

    克隆对象原型

    function Person() {}
    Person.prototype.getMsg = function () {}
    var p = new Person()
    var obj = p
    var newObj = deepClone(obj)
    console.log(newObj.__proto__ == obj.__proto__)
    // 结果: false 失败
    

    在我们的对象中出现循环引用,又会怎么样?

    var a={"name":"zzz"};
    var b={"name":"vvv"};
    a.child=b; b.parent=a;
    var newObj = deepClone(b)
    // 结果 :RangeError: Maximum call stack size exceeded (爆栈了)
    

    这是我们的深层克隆函数,虽说有诸多的问题,我们在后面进行解决。接 下来我们在看一中简便的方法(江湖流传的妙招)。

    JSON.parse 方法

    前几年微博上流传着一个传说中最便捷实现深克隆的方法,JSON 对象parse 方法可以将 JSON 字符串反序列化成 JS 对象,stringify 方法可以将 JS对象序列化成 JSON 字符串,这两个方法结合起来就能产生一个便捷的深克隆.

    const newObj = JSON.parse(JSON.stringify(oldObj));
    const oldObj = {
        a: 1,
        b: ['e', 'f', 'g'],
        c: {
            h: {
                i: 2
            }
        }
    };
    const newObj = JSON.parse(JSON.stringify(oldObj));
    console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
    console.log(oldObj.c.h === newObj.c.h); // false
    newObj.c.h.i = 'change';
    console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 2 }
    

    果然,这是一个实现深克隆的好方法,但是这个解决办法是不是太过简单了.确实,这个方法虽然可以解决绝大部分是使用场景,但是却有很多坑.
    1.他无法实现对函数 、RegExp 等特殊对象的克隆。
    2.会抛弃对象的 constructor,所有的构造函数会指向 Object。
    3.对象有循环引用,会报错。
    对于这里的问题和之前的问题差不多,我就不一一测试了,同学们可以回 去玩一玩,是具有这样的问题。
    说了这么多,把几种常见的深层克隆的方法个大家讲解后,遗留了一堆问题。
    接下来我们要写个能解决以上问题的深层克隆。
    首先我们分析一下遗留问题,其实上面的问题我们可以分为三类:
    1.对象类型问题,
    2.原型问题,
    3.循环引用问题
    那我们就逐个击破就好了。

    1. 对象类型问题:
      由于要面对不同的对象(正则、数组、Date 等)要采用不同的处理方式,我们需要实现一个对象类型判断函数。
    function isType(obj, type) {
        if (typeof obj !== 'object') return false;
        var typeString = Object.prototype.toString.call(obj);
        var flag;
        switch (type) {
            case 'Array':
                flag = typeString === '[object Array]';
                break;
            case 'Date':
                flag = typeString === '[object Date]';
                break;
            case 'RegExp':
                flag = typeString === '[object RegExp]';
                break;
            default:
                flag = false;
        }
        return flag;
    };
    

    通过这个函数 isType 我们可以根据 type 确定 obj 是不是对应的对象类型。这样我们就可以对特殊对象进行类型判断了,从而采用针对性的克隆策略。
    对于正则对象,我们在处理之前要先补充一点新知识。
    我们需要通过正则语法解到 flags 属性等等,因此我们需要实现一个提取
    flags 的函数。

    function getRegExp(re) {
        var flags = '';
        if (re.global) flags += 'g';
        if (re.ignoreCase) flags += 'i';
        if (re.multiline) flags += 'm';
        return flags;
    };
    
    1. 原型问题:
      我们利用 Object.getPrototypeOf(); 获取对象原型,在利用 Object.create切断原型链即可。
    2. 循环引用问题:
      对于引用值,我们通过数组进行记录,每次碰到引用值后,遍历数组进行判断。然后处理。

    做好了这些准备工作,我们就可以进行深克隆的实现了。
    全部代码:

    function getRegExp(re) {
        var flags = '';
        if (re.global) flags += 'g';
        if (re.ignoreCase) flags += 'i';
        if (re.multiline) flags += 'm';
        return flags;
    };
    
    function isType(obj, type) {
        if (typeof obj !== 'object') return false;
        var typeString = Object.prototype.toString.call(obj);
        var flag;
        switch (type) {
            case 'Array':
                flag = typeString === '[object Array]';
                break;
            case 'Date':
                flag = typeString === '[object Date]';
                break;
            case 'RegExp':
                flag = typeString === '[object RegExp]';
                break;
            default:
                flag = false;
        }
        return flag;
    };
    
    function clone(parent) {
        // 维护两个储存循环引用的数组
        var parents = [];
        var children = [];
    
        var _clone = parent => {
            if (parent === null) return null;
            if (typeof parent !== 'object') return parent;
    
            var child, proto;
    
            if (isType(parent, 'Array')) {
                // 对数组做特殊处理
                child = [];
            } else if (isType(parent, 'RegExp')) {
                // 对正则对象做特殊处理
                child = new RegExp(parent.source,
                    getRegExp(parent));
                if (parent.lastIndex) child.lastIndex =
                    parent.lastIndex;
            } else if (isType(parent, 'Date')) {
                // 对 Date 对象做特殊处理
                child = new Date(parent.getTime());
            } else {
                // 处理对象原型
                proto = Object.getPrototypeOf(parent);
                // 利用 Object.create 切断原型链
                child = Object.create(proto);
            }
    
            // 处理循环引用
            var index = parents.indexOf(parent);
            if (index != -1) {
                // 如果父数组存在本对象,说明之前已经被引用过,直接返回此
                对象
                return children[index];
            }
            parents.push(parent);
            children.push(child);
            for (var i in parent) {
                // 递归
    
                if (parent.hasOwnProperty(i)) { // 过滤原型属性
                    child[i] = _clone(parent[i]);
                }
            }
            return child;
        };
        return _clone(parent);
    };
    

    当然,我们这个深克隆还不算完美,例如 Buffer 对象、Promise、Set、Map 可能都需要我们做特殊处理,另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间,不过一个基本的深克隆函数我们已经实现了。
    实现一个深克隆是面试中常见的问题的,可是绝大多数面试者的答案都是不完整的,甚至是错误的,这个时候面试官会不断追问,看看你到底理解不理解深克隆的原理,很多情况下一些一知半解的面试者就原形毕漏了。

    相关文章

      网友评论

          本文标题:JS的浅克隆和深克隆

          本文链接:https://www.haomeiwen.com/subject/qbzcrktx.html