深度探讨-js深克隆

2017-09-07  本文已影响0人  Taniffer

js深克隆一直是一个很奇妙的问题。也是面试中常问的一个问题。但是可能大多数人的解决方案都是有问题的。但是并不是解决方案本身的问题,是这个问题本身的问题。从一个角度来看,这个问题其实是无解的。但是换一个角度,这个问题已然有了最优解。下面就来说一下这个问题的各种** 解决方案 **。

1.使用JSON

JSON.parse(JSON.stringify(obj))

看一个例子

obj={a:1,b:{b:1}}
JSON.parse(JSON.stringify(obj))//obj={a:1,b:{b:1}}

深克隆完毕。。是不是觉得很简单,很惊艳,从某种程度来说,没错,这种方法是很好,而且如果是简单结构的话比jquery的extend效率要高10%-20%。但是问题出在哪里呢,JSON的深克隆不会克隆NAN,undefined这些,更别说function,Data了。所以又有了递归遍历这种操作。

2.递归

//返回传递给他的任意对象的类
    function isClass(o) {
        return Object.prototype.toString.call(o).slice(8, -1);
    }

    //深度克隆
    function deepClone(obj) {
        var result, oClass = isClass(obj);
        //确定result的类型
        if (oClass === "Object") {
            result = {};
        } else if (oClass === "Array") {
            result = [];
        } else {
            return obj;
        }
        for (key in obj) {
            if (obj.hasOwnProperty(key)) { //原型链属性不克隆
                var copy = obj[key];
                if (isClass(copy) === "Object") {
                    result[key] = arguments.callee(copy);//递归调用
                } else if (isClass(copy) === "Array") {
                    result[key] = arguments.callee(copy);
                } else {
                    result[key] = obj[key];
                }
            }
        }
        return result;
    }

突然就感觉解决了所有问题吧。数组,和对象的情况都考了到了呢。而且原型链上的繁杂东西也没有带出来。是不是觉得又完美了。
然而,打击总是一直存在的,在已知数据结构的情况下你可以这样使用,但是,如果不知道就不行。比如。如果有环?然后就有了死循环,无法避免的栈溢出报错。。。
而且,原型链也没有复制,从一定程度来说也不算深克隆。。那如果已知数据结构,还不如直接用for in循环来的快一些。。

所以说真正的深克隆需要考虑的问题有这些

1.JSON 克隆不支持函数、引用、undefined、Date、RegExp 等
2.递归克隆要考虑环
3.要考虑 等特殊对象的克隆方式
4.要不要克隆 proto,如果要克隆,就非常浪费内存;如果不克隆,就不是深克隆。

3.JQuery中较为完美的实现

仔细想想这些问题可能会爆炸。。。。看看jquery的那些大佬怎么实现的吧。。代码挺长的,这里有一个相近的实现。 下面的代码可以先跳过。

$ = function() {  
    var copyIsArray,  
        toString = Object.prototype.toString,  
        hasOwn = Object.prototype.hasOwnProperty;  
  
    class2type = {  
        '[object Boolean]' : 'boolean',  
        '[object Number]' : 'number',  
        '[object String]' : 'string',  
        '[object Function]' : 'function',  
        '[object Array]' : 'array',  
        '[object Date]' : 'date',  
        '[object RegExp]' : 'regExp',  
        '[object Object]' : 'object'  
    },  
  
    type = function(obj) {  
        return obj == null ? String(obj) : class2type[toString.call(obj)] || "object";  
    },  
  
    isWindow = function(obj) {  
        return obj && typeof obj === "object" && "setInterval" in obj;  
    },  
  
    isArray = Array.isArray || function(obj) {  
        return type(obj) === "array";  
    },  
  
    isPlainObject = function(obj) {  
        if (!obj || type(obj) !== "object" || obj.nodeType || isWindow(obj)) {  
            return false;  
        }  
  
        if (obj.constructor && !hasOwn.call(obj, "constructor")  
                && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {  
            return false;  
        }  
  
        var key;  
        for (key in obj) {  
        }  
  
        return key === undefined || hasOwn.call(obj, key);  
    },  
  
    extend = function(deep, target, options) {  
        for (name in options) {  
            src = target[name];  
            copy = options[name];  
  
            if (target === copy) { continue; }  
  
            if (deep && copy  
                    && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) {  
                if (copyIsArray) {  
                    copyIsArray = false;  
                    clone = src && isArray(src) ? src : [];  
  
                } else {  
                    clone = src && isPlainObject(src) ? src : {};  
                }  
  
                target[name] = extend(deep, clone, copy);  
            } else if (copy !== undefined) {  
                target[name] = copy;  
            }  
        }  
  
        return target;  
    };  
  
    return { extend : extend };  
}();  

与之前相比。多了这些有趣的代码

if (target === copy) { continue; }  //避免了死循环

在isPlainObject函数中

if (!obj || type(obj) !== "object" || obj.nodeType || isWindow(obj)) {  
    return false;  
} 
//以下情况不复制
  1. 对象为undefined; 
  2. 转为String时不是"[object Object]"; 
  3. obj是一个DOM元素; 
  4. obj是window。

之所以不对DOM元素和window进行深复制,可能是因为它们包含的属性太多了;尤其是window对象,所有在全局域声明的变量都会是其属性,更不用说内置的属性了。

if (obj.constructor && !hasOwn.call(obj, "constructor")  
              && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {  
      return false;  
  }  
//如果对象具有构造函数,但却不是自身的属性,说明这个构造函数是通过prototye继承来的,这种情况也不进行深复制。
var key;  
for (key in obj) {  
}  
return key === undefined || hasOwn.call(obj, key);  
// 这几句代码是用于检查对象的属性是否都是自身的,因为遍历对象属性时,
会先从自身的属性开始遍历,所以只需要检查最后的属性是否是自身的就可以了。

上面的代码和和jQuery的实现就很相似。做到了,各种类型的判断,死循环的避免,而对于复杂的原型拷贝问题,它做了部分浅复制(关于原型做深复制很不必要,而且浪费大量时间和内存)。大部分情况下是深拷贝,也是一种较为完美的解决方案了。

4.优雅实现-immutable.js

深复制已经解决到这个层面了,可以说在这个角度没有更优的解决方案了吧。回归到问题本身。我们为什么要深复制这个对象。如果要求一样,直接使用原来的对象不行吗?真的不一样的话,为什么不新建一个对象,为什么要用这么浪费性能和内存的深复制?可能你复制之后的对象的大部分属性还是没有用的。这就牵扯到了一个js库。。。

在React出来的同时出了一个js库-immutable.js,怎么说呢,它真的是一个很厉害的库,也是一个很厉害的想法,但是react优点遮挡了他的光辉吧。这也是一个react的官方推荐库。一般它会和react一起使用,但是它单独也可以使用,它是一种结构复用的思想。也就是数据不可变,什么意思呢,就是用immutable建立的对象不会被改变,会产生一个新分支来共用它的结构,下图这样。

immutable.gif

具体了解的话来看一下这两篇文章。
Immutable 详解及 React 中实践
官方文档

const { Map } = require('immutable')
const map1 = Map({ a: 1, b: 2, c: 3 })
const map2 = map1.set('b', 50)
map1.get('b') // 2
map2.get('b') // 50

这种结构复用的解决方案基本上已经脱离了深复制的层面,不是从解决深复制,而是单纯的不让深复制产生。从根源上解决了这个问题,实现了时间复杂度和空间复杂度的双优化。但是对于已有的含复杂原型链的对象,可能它也没有太好的解决方法

上一篇下一篇

猜你喜欢

热点阅读