深切理会 JavaScript 的深复制

本文最初宣布于我的个人博客:品味之味

一年前我曾写过一篇 Javascript 中的一种深复制完成,当时写这篇文章的时刻还比较稚嫩,有许多处所没有斟酌细致。为了不误人子弟,我决议连系 Underscore、lodash 和 jQuery 这些主流的第三方库来从新谈一谈这个题目。

第三方库的完成

讲一句唯心主义的话,放之四海而皆准的要领是不存在的,差别的深复制完成要领和完成粒度有各自的好坏以及各自合适的运用场景,所以本文并非在教人人改怎样完成深复制,而是将一些在 JavaScript 中完成深复制所须要斟酌的题目呈献给人人。我们起首从较为简朴的 Underscore 最先:

Underscore —— _.clone()

在 Underscore 中有如许一个要领:_.clone(),这个要领实际上是一种浅复制 (shallow-copy),一切嵌套的对象和数组都是直接复制援用而并没有举行深复制。来看一下例子应该会越发直观:

var x = {
    a: 1,
    b: { z: 0 }
};

var y = _.clone(x);

y === x       // false
y.b === x.b   // true

x.b.z = 100;
y.b.z         // 100

让我们来看一下 Underscore 的源码

// Create a (shallow-cloned) duplicate of an object.
_.clone = function(obj) {
  if (!_.isObject(obj)) return obj;
  return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};

假如目的对象是一个数组,则直接挪用数组的slice()要领,不然就是用_.extend()要领。想必人人对extend()要领不会生疏,它的作用主如果将从第二个参数最先的一切对象,按键值逐一赋给第一个对象。而在 jQuery 中也有相似的要领。关于 Underscore 中的 _.extend() 要领的完成可以参考 underscore.js #L1006

Underscore 的 clone() 不能算作深复制,但它最少比直接赋值来得“深”一些,它创建了一个新的对象。别的,你也可以经由过程以下比较 tricky 的要领来完成单层嵌套的深复制:

var _ = require('underscore');
var a = [{f: 1}, {f:5}, {f:10}];
var b = _.map(a, _.clone);       // <----
b[1].f = 55;
console.log(JSON.stringify(a));  // [{"f":1},{"f":5},{"f":10}]

jQuery —— $.clone() / $.extend()

在 jQuery 中也有这么一个叫 $.clone() 的要领,但是它并非用于平常的 JS 对象的深复制,而是用于 DOM 对象。这不是这篇文章的重点,所以感兴趣的同砚可以参考jQuery的文档。与 Underscore 相似,我们也是可以经由过程 $.extend() 要领来完成深复制。值得光荣的是,我们在 jQuery 中可以经由过程增加一个参数来完成递归extend。挪用$.extend(true, {}, ...)就可以够完成深复制啦,参考下面的例子:

var x = {
    a: 1,
    b: { f: { g: 1 } },
    c: [ 1, 2, 3 ]
};

var y = $.extend({}, x),          //shallow copy
    z = $.extend(true, {}, x);    //deep copy

y.b.f === x.b.f       // true
z.b.f === x.b.f       // false

jQuery的源码 – src/core.js #L121 文件中我们可以找到$.extend()的完成,也是完成得比较简约,而且不太依赖于 jQuery 的内置函数,稍作修正就可以拿出来零丁运用。

lodash —— _.clone() / _.cloneDeep()

在lodash中关于复制的要领有两个,分别是_.clone()_.cloneDeep()。个中_.clone(obj, true)等价于_.cloneDeep(obj)。运用上,lodash和前二者并没有太大的区分,但看了源码会发明,Underscore 的完成只要30行摆布,而 jQuery 也不过60多行。可 lodash 中与深复制相干的代码却有上百行,这是什么原理呢?

var $ = require("jquery"),
    _ = require("lodash");

var arr = new Int16Array(5),
    obj = { a: arr },
    obj2;
arr[0] = 5;
arr[1] = 6;

// 1. jQuery
obj2 = $.extend(true, {}, obj);
console.log(obj2.a);                            // [5, 6, 0, 0, 0]
Object.prototype.toString.call(obj2);           // [object Int16Array]
obj2.a[0] = 100;
console.log(obj);                               // [100, 6, 0, 0, 0]

//此处jQuery不能准确处置惩罚Int16Array的深复制!!!

// 2. lodash
obj2 = _.cloneDeep(obj);                       
console.log(obj2.a);                            // [5, 6, 0, 0, 0]
Object.prototype.toString.call(arr2);           // [object Int16Array]
obj2.a[0] = 100;
console.log(obj);                               // [5, 6, 0, 0, 0]

经由过程上面这个例子可以初见端倪,jQuery 没法准确深复制 JSON 对象之外的对象,而我们可以从下面这段代码片断可以看出 lodash 花了大批的代码来完成 ES6 引入的大批新的规范对象。更凶猛的是,lodash 针对存在环的对象的处置惩罚也是非常精彩的。因而相较而言,lodash 在深复制上的行动反应比前两个库好许多,是更拥抱将来的一个第三方库。

/** `Object#toString` result references. */
var argsTag = '[object Arguments]',
    arrayTag = '[object Array]',
    boolTag = '[object Boolean]',
    dateTag = '[object Date]',
    errorTag = '[object Error]',
    funcTag = '[object Function]',
    mapTag = '[object Map]',
    numberTag = '[object Number]',
    objectTag = '[object Object]',
    regexpTag = '[object RegExp]',
    setTag = '[object Set]',
    stringTag = '[object String]',
    weakMapTag = '[object WeakMap]';

var arrayBufferTag = '[object ArrayBuffer]',
    float32Tag = '[object Float32Array]',
    float64Tag = '[object Float64Array]',
    int8Tag = '[object Int8Array]',
    int16Tag = '[object Int16Array]',
    int32Tag = '[object Int32Array]',
    uint8Tag = '[object Uint8Array]',
    uint8ClampedTag = '[object Uint8ClampedArray]',
    uint16Tag = '[object Uint16Array]',
    uint32Tag = '[object Uint32Array]';

借助 JSON 全局对象

比拟于上面引见的三个库的做法,针对纯 JSON 数据对象的深复制,运用 JSON 全局对象的 parsestringify 要领来完成深复制也算是一个简朴讨巧的要领。但是运用这类要领会有一些隐蔽的坑,它能准确处置惩罚的对象只要 Number, String, Boolean, Array, 扁平对象,即那些可以被 json 直接示意的数据结构。

function jsonClone(obj) {
    return JSON.parse(JSON.stringify(obj));
}
var clone = jsonClone({ a:1 });

拥抱将来的深复制要领

我本身完成了一个深复制的要领,由于用到了Object.createObject.isPrototypeOf等比较新的要领,所以基础只能在 IE9+ 中运用。而且,我的完成是直接定义在 prototype 上的,很有能够引发大多数的前端偕行们的不适。(关于这个我还曾在知乎上提问过:为何不要直接在Object.prototype上定义要领?)只是试验性子的,人人参考一下就好,改成非 prototype 版本也是很轻易的,不过就是要不断地去推断对象的范例了。~

这个完成要领详细可以看我写的一个小玩意儿——Cherry.js,运用要领大概是如许的:

function X() {
    this.x = 5;
    this.arr = [1,2,3];
}
var obj = { d: new Date(), r: /abc/ig, x: new X(), arr: [1,2,3] },
    obj2,
    clone;

obj.x.xx = new X();
obj.arr.testProp = "test";
clone = obj.$clone();                  //<----

起首定义一个辅佐函数,用于在预定义对象的 Prototype 上定义要领:

function defineMethods(protoArray, nameToFunc) {
    protoArray.forEach(function(proto) {
        var names = Object.keys(nameToFunc),
            i = 0;

        for (; i < names.length; i++) {
            Object.defineProperty(proto, names[i], {
                enumerable: false,
                configurable: true,
                writable: true,
                value: nameToFunc[names[i]]
            });
        }
    });
}

为了防备和源生要领争执,我在要领名前加了一个 $ 标记。而这个要领的详细完成很简朴,就是递归深复制。个中我须要解释一下两个参数:srcStackdstStack。它们的主要用途是对存在环的对象举行深复制。比方源对象中的子对象srcStack[7]在深复制今后,对应于dstStack[7]。该完成要领参考了 lodash 的完成。关于递归最主要的就是 Object 和 Array 对象:

/*=====================================*
 * Object.prototype
 * - $clone()
*=====================================*/

defineMethods([ Object.prototype ], {
    '$clone': function (srcStack, dstStack) {
        var obj = Object.create(Object.getPrototypeOf(this)),
            keys = Object.keys(this),
            index,
            prop;

        srcStack = srcStack || [];
        dstStack = dstStack || [];
        srcStack.push(this);
        dstStack.push(obj);

        for (var i = 0; i < keys.length; i++) {
            prop = this[keys[i]];
            if (prop === null || prop === undefined) {
                obj[keys[i]] = prop;
            }
            else if (!prop.$isFunction()) {
                if (prop.$isPlainObject()) {
                    index = srcStack.lastIndexOf(prop);
                    if (index > 0) {
                        obj[keys[i]] = dstStack[index];
                        continue;
                    }
                }
                obj[keys[i]] = prop.$clone(srcStack, dstStack);
            }
        }
        return obj;
    }
});

/*=====================================*
 * Array.prototype
 * - $clone()
*=====================================*/

defineMethods([ Array.prototype ], {
    '$clone': function (srcStack, dstStack) {
        var thisArr = this.valueOf(),
            newArr = [],
            keys = Object.keys(thisArr),
            index,
            element;

        srcStack = srcStack || [];
        dstStack = dstStack || [];
        srcStack.push(this);
        dstStack.push(newArr);

        for (var i = 0; i < keys.length; i++) {
            element = thisArr[keys[i]];
            if (element === undefined || element === null) {
                newArr[keys[i]] = element;
            } else if (!element.$isFunction()) {
                if (element.$isPlainObject()) {
                    index = srcStack.lastIndexOf(element);
                    if (index > 0) {
                        newArr[keys[i]] = dstStack[index];
                        continue;
                    }
                }
            }
            newArr[keys[i]] = element.$clone(srcStack, dstStack);
        }
        return newArr;
    }
});

接下来要针对 Date 和 RegExp 对象的深复制举行一些特别处置惩罚:

/*=====================================*
 * Date.prototype
 * - $clone
 *=====================================*/

defineMethods([ Date.prototype ], {
    '$clone': function() { return new Date(this.valueOf()); }
});

/*=====================================*
 * RegExp.prototype
 * - $clone
 *=====================================*/

defineMethods([ RegExp.prototype ], {
    '$clone': function () {
        var pattern = this.valueOf();
        var flags = '';
        flags += pattern.global ? 'g' : '';
        flags += pattern.ignoreCase ? 'i' : '';
        flags += pattern.multiline ? 'm' : '';
        return new RegExp(pattern.source, flags);
    }
});

接下来就是 Number, Boolean 和 String 的 $clone 要领,虽然很简朴,但这也是必不可少的。如许就可以防备像单个字符串如许的对象毛病地去挪用 Object.prototype.$clone

/*=====================================*
 * Number / Boolean / String.prototype
 * - $clone()
 *=====================================*/

defineMethods([
    Number.prototype,
    Boolean.prototype,
    String.prototype
], {
    '$clone': function() { return this.valueOf(); }
});

比较各个深复制要领

特征jQuerylodashJSON.parse所谓“拥抱将来的深复制完成”
浏览器兼容性IE6+ (1.x) & IE9+ (2.x)IE6+IE8+IE9+
可以深复制存在环的对象抛出非常 RangeError: Maximum call stack size exceeded支撑抛出非常 TypeError: Converting circular structure to JSON支撑
对 Date, RegExp 的深复制支撑×支撑×支撑
对 ES6 新引入的规范对象的深复制支撑×支撑××
复制数组的属性×仅支撑RegExp#exec返回的数组效果×支撑
是不是保存非源生对象的范例×××支撑
复制不可枚举元素××××
复制函数××××

实行效力

为了测试种种深复制要领的实行效力,我运用了以下的测试用例:

var x = {};
for (var i = 0; i < 1000; i++) {
    x[i] = {};
    for (var j = 0; j < 1000; j++) {
        x[i][j] = Math.random();
    }
}

var start = Date.now();
var y = clone(x);
console.log(Date.now() - start);

下面来看看各个完成要领的详细效力怎样,我所运用的浏览器是 Mac 上的 Chrome 43.0.2357.81 (64-bit) 版本,可以看出来在3次的试验中,我所完成的要领比 lodash 稍逊一筹,但比jQuery的效力也会高一些。愿望这篇文章对你们有协助~

深复制要领jQuerylodashJSON.parse所谓“拥抱将来的深复制完成”
Test 1475341630320
Test 2505270690345
Test 3456268650332
Average478.7293656.7332.3

参考资料

    原文作者:JerryZou
    原文地址: https://segmentfault.com/a/1190000002801042
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞