![2a1d9a39aba3546fe030271208091f73.gif](https://i-blog.csdnimg.cn/blog_migrate/14d523a82024be1137c07ba7e7681a28.gif)
大家好,我是你们的导师,我每天都会在这里给大家分享一些干货内容(当然了,周末也要允许老师休息一下哈)。上次老师跟大家分享了CSS之 设计资源 UI套件背景图标框架的知识,今天跟大家分享下JS之 深复制的知识。
1 JS之 深复制 之前,我曾写过一篇 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(https://github.com/jashkenas/underscore/blob/e4743ab712b8ab42ad4ccb48b155034d02394e4d/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的文档。(https://api.jquery.com/clone/) 与 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 copyy.b.f === x.b.f // truez.b.f === x.b.f // false
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. jQueryobj2 = $.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. lodashobj2 = _.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]
/** `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 全局对象的 parse 和 stringify 方法来实现深复制也算是一个简单讨巧的方法。 然而使用这种方法会有一些隐藏的坑,它能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。function jsonClone(obj) { return JSON.parse(JSON.stringify(obj));}var clone = jsonClone({ a:1 });
拥抱未来的深复制方法
我自己实现了一个深复制的方法,因为用到了Object.create、Object.isPrototypeOf等比较新的方法,所以基本只能在 IE9+ 中使用。 而且,我的实现是直接定义在 prototype 上的,很有可能引起大多数的前端同行们的不适。 (关于这个我还曾在知乎上提问过:为什么不要直接在Object.prototype上定义方法?:https://www.zhihu.com/question/26924011)只是实验性质的,大家参考一下就好,改成非 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(); //
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]] }); } });}
/*=====================================* * 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.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.prototype * - $clone() *=====================================*/defineMethods([ Number.prototype, Boolean.prototype, String.prototype], { '$clone': function() { return this.valueOf(); }});
比较各个深复制方法
执行效率
为了测试各种深复制方法的执行效率,我使用了如下的测试用例: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);
参考文献:https://jerryzou.com/posts/dive-into-deep-clone-in-javascript/
好了,今天技术部分就讲到这里。
还有一个重要的事,我们现在已开放对外招导师。工资还可以,大概15K-25K,点击下面【
链接
】有详细介绍!(注意!每期我们只招1个人!)
15K~25K招聘导师若干名
![9c9522fe91ae8e1fdeeeb5cf0c50eece.gif](https://i-blog.csdnimg.cn/blog_migrate/f78a185ebaeb90a37f77119ad1946574.gif)