在聊JavaScript(如下简称js)深度克隆以前,咱们先来了解一下js中对象的组成。
在 js
中一切实例皆是对象,具体分为 原始类型 和 合成类型 :
原始类型 对象指的是 Undefined
、 Null
、Boolean
、Number
和 String
,按值传递。
合成类型 对象指的是 array
、 object
以及 function
,按址传递,传递的时候是内存中的地址。ios
克隆或者拷贝分为2种: 浅度克隆 、 深度克隆 。
浅度克隆 :基本类型为值传递,对象仍为引用传递。
深度克隆 :全部元素或属性均彻底克隆,并于原引用类型彻底独立,即,在后面修改对象的属性的时候,原对象不会被修改。正则表达式
又或许你刚据说“深度克隆”这个词,简单来讲,就是说有个变量a,a的值是个对象(包括基本数据类型),如今你要建立一个变量b,使得它拥有跟a同样的方法和属性等等。可是a和b之间不能相互影响,即a的值的改变不影响b值的变化。直接赋值可好?segmentfault
var a = 1; var b = a; a = 10; console.log(b); // 1 var a = 'hello'; var b = a; a = 'world'; console.log(b); // hello var a = true; var b = a; a = false; console.log(b); // true
实践证实某些 JavaScript
的原始数据类型,若是要克隆直接赋值便可。
关于 function
的深度复制:查阅了一些资料, function
的深度复制彷佛和原始数据类型的深度复制同样。数组
var a = function () { console.log(1); }; var b = a; a = function () { console.log(2); }; b();
原本我也是这么认为的,直到文章下出现了评论。思考后我以为 function
和普通的对象同样,只是咱们在日常应用中习惯了总体的从新赋值,致使它在深度复制中的表现和原始类型一致:浏览器
var a = function () { console.log(1); }; a.tmp = 10; var b = a; a.tmp = 20; console.log(b.tmp); // 20
因而乎对于 function
类型的深度克隆,直接赋值彷佛并不该该是一种最好的方法(尽管实际应用中足矣)。babel
可是对象呢?数据结构
var a = [0,1,2,3]; var b = a; a.push(4); console.log(b); // [0, 1, 2, 3, 4]
显然与预期不符,为何会这样?由于原始数据类型储存的是对象的实际数据,而对象类型存储的是对象的引用地址。上面的例子呢也就是说a和b对象引用了同一个地址,不管改变a仍是改变b,其实根本操做是同样的,都是对那块空间地址中的值的改变。函数
因而咱们知道了,对于基本的对象来讲,不能只能用 “ = ” 赋值,思索后写下以下代码:测试
// 判断arr是否为一个数组,返回一个bool值 function isArray (arr) { return Object.prototype.toString.call(arr) === '[object Array]'; } // 深度克隆 function deepClone (obj) { if(typeof obj !== "object" && typeof obj !== 'function') { return obj; //原始类型直接返回 } var o = isArray(obj) ? [] : {}; for(i in obj) { if(obj.hasOwnProperty(i)){ o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i]; } } return o; }
注意代码中判断数组的时候用的不是 obj instanceof Array
,这是由于该方法存在一些小问题,详情见http://www.nowamagic.net/librarys/veda/detail/1250this
用一些代码来测试下:
// 测试用例: var srcObj = { a: 1, b: { b1: ["hello", "hi"], b2: "JavaScript" } }; var abObj = srcObj; var tarObj = cloneObject(srcObj); srcObj.a = 2; srcObj.b.b1[0] = "Hello"; console.log(abObj.a); console.log(abObj.b.b1[0]); console.log(tarObj.a); // 1 console.log(tarObj.b.b1[0]); // "hello"
对于上面的方法再进行测试下,以下:
这个没有区分具体的对象,在此问下你们js的对象有哪些呢?相信通常人答不出来4个[object Object]
, [object Array]
, [object Null]
, [object RegExp]
, [object Date]
, [object HTMLXXElement]
, [object Map]
,[object Set]
,...
等等一系列
检测类型使用 Object.prototype.toString.call(xxx)
和 typeof
咱们分析下上面对象中哪些是引用类型须要特殊处理呢?相信你们都不陌生了。[object Object]
和 [object Array]
好!详细你们思路有了,咋们用递归来实现一把吧!
const deepClone = function(obj) { // 先检测是否是数组和Object // let isMap = Object.prototype.toString.call(obj) === '[object Map]; // let isSet = Object.prototype.toString.call(obj) === '[object Set]; // let isArr = Object.prototype.toString.call(obj) === '[object Array]'; let isArr = Array.isArray(obj); let isJson = Object.prototype.toString.call(obj) === '[object Object]'; if (isArr) { // 克隆数组 let newObj = []; for (let i = 0; i < obj.length; i++) { newObj[i] = deepClone(obj[i]); } return newObj; } else if (isJson) { // 克隆Object let newObj = {}; for (let i in obj) { newObj[i] = deepClone(obj[i]); } return newObj; } // 不是引用类型直接返回 return obj; }; Object.prototype.deepClone = function() { return deepClone(this); };
注:先不考虑 对象了原理都是同样MapSetArguments[object XXArrayBuffer]
各类状况分析完了才说算是真克隆
咱们在控制台看下
是否是解决了? 在此并无结束。 专一的伙伴们相信发现了对象中包含了个 deepClone
方法,具体细节咱们在此就很少说了,咱们给 Object
添加了个 Object.prototype.deepClone
方法致使了每一个对象都有了此方法。
原则上咱们不容许在原型链上添加方法的,由于在循环中 for in
, Object.entries
, Object.values
, Object.keys
等方法会出现自定义的方法。
相信熟悉 Object
文档的伙伴人已经知道解决方案了,
Object.defineProperty
这个方法给你们带来了福音 具体参考 Object 文档。咱们使用一个enumerable
(不可枚举)属性就能够解决了。
在原来基础上添加如下代码便可。
Object.defineProperty(Object.prototype, 'deepClone', {enumerable: false});
再看控制台
一样上面方法中也是没法克隆一个不可枚举的属性。
完整代码以下:
const deepClone = function(obj) { // 先检测是否是数组和Object // let isArr = Object.prototype.toString.call(obj) === '[object Array]'; let isArr = Array.isArray(obj); let isJson = Object.prototype.toString.call(obj) === '[object Object]'; if (isArr) { // 克隆数组 let newObj = []; for (let i = 0; i < obj.length; i++) { newObj[i] = deepClone(obj[i]); } return newObj; } else if (isJson) { // 克隆Object let newObj = {}; for (let i in obj) { newObj[i] = deepClone(obj[i]); } return newObj; } // 不是引用类型直接返回 return obj; }; Object.prototype.deepClone = function() { return deepClone(this); }; Object.defineProperty(Object.prototype, 'deepClone', {enumerable: false});
注: 为了兼容低版本浏览器须要借助 babel-polyfill
;
附: 其余深拷贝方式选择:https://blog.csdn.net/ios99999/article/details/77646594
一维数据结构的深拷贝方法建议使用:Object.assign();
二维数据结构及以上的深拷贝方法建议使用:JSON.parse(JSON.stringify());
特别复杂的数据结构的深拷贝方法建议使用:Loadsh.cloneDeep();
JSON.parse(JSON.stringify(obj))是最简单粗暴的深拷贝,可以处理JSON格式的全部数据类型,可是对于正则表达式类型、函数类型等没法进行深拷贝,并且会直接丢失相应的值,还有就是它会抛弃对象的constructor。也就是深拷贝以后,无论这个对象原来的构造函数是什么,在深拷贝以后都会变成Object。同时若是对象中存在循环引用的状况也没法正确处理:
var obj = { a: {a: "hello"}, b: 33 }; var newObj = JSON.parse(JSON.stringify(obj)); newObj.b = "hello world"; console.log(obj); // { a: "hello", b: 33 }; console.log(newObj); // { a: "hello world", b: 33}; console.log(obj==newObj); // false console.log(obj===newObj); // false
参考连接: