JavaScript 克隆
克隆
浅层克隆
深层克隆
一个已有的对象obj,现在把对象obj克隆给一个空对象obj1
var obj = { name : 'abc', age : '123,', sex : 'female' } var obj1 = {} /** * 写一个克隆方法clone(origin, target) * origin Object 原始的对象 * target Object 模板对象 * */ function clone(origin ,target){ for(var prop in origin){ // 循环把origin上面所有的属性都拿出来 target[prop] = origin[prop]; // 每一次循环拿出的属性名origin[prop],让"target[prop] = origin[prop]"。 } } clone(obj ,obj1); // 克隆obj console.log(obj1); // 打印obj1 {name: "abc", age: "123,", sex: "female"}
这个方法再兼容一下(容个错)防止用户不传target。
如果用户传一个空对象就用户传的,如果没传就新建一个空对象,换句话说不传第二个参数也行。
function clone(origin ,target){ var target = target || {}; // 判断target如果传第二个参数就用传进来的,如果没传就自动生成一个空对象。 for(var prop in origin){ target[prop] = origin[prop]; } return target; // 最后把克隆完的反回去 } var obj = { name : 'abc', age : '123,', sex : 'female' } var obj1 = clone(obj); console.log(obj1); // {name: "abc", age: "123,", sex: "female"}
有一个问题,这个克隆是浅层克隆,浅层克隆原始值克隆完后是没问题的。
浅层克隆
function clone(origin ,target){ var target = target || {}; for(var prop in origin){ target[prop] = origin[prop]; } return target; } var obj = { name : 'abc', age : '123,', sex : 'female', } var obj1 = clone(obj) console.log(obj); // obj {name: "abc", age: "123,", sex: "female"} console.log(obj1); // obj1 {name: "abc", age: "123,", sex: "female"} obj.name = 'bcd'; // 现在让obj的name值改成bcd console.log(obj1.name); // obj1的name没有变化还是abc,因为是原始值,原始值就是值的拷贝
原始值就是值的拷贝,现在也让值拷贝,但是让引用值拷贝。
function clone(origin ,target){ var target = target || {}; for(var prop in origin){ target[prop] = origin[prop]; } return target; } var obj = { name : 'abc', age : '123,', sex : 'female', car : ['visa','unionpay'] // 里面加一个数组 } var obj1 = clone(obj); console.log(obj1); // car1 ["visa", "unionpay"] obj1.car.push('master'); // obj1.car是引用值,push进去一个'master'进去 console.log(obj1.car); // obj1.car肯定的会多一个 ["visa", "unionpay", "master"] console.log(obj.car); // 但是obj.car也被变了 ["visa", "unionpay", "master"]
因为是引用值,拷贝过去的是一个引用,两个人指向的是同一个房间,你改我也改。
现在我们想实现"深度克隆","深度拷贝"的意思是,我拿的和你的属性是一模一样的,但是就不是一个人。
你有一个引用值car里面放的是数组,拷贝过来之后我也有car数组,但是拷贝到我这之后,我改你是不会被更改的,两个人用的是两个房间。
深层克隆
浅度克隆非常简单,就是把A拷贝给B。深度克隆和浅度克隆的区别,深度克隆克隆后,不论是引用值还是原始值,都是各自独立的。
深度克隆和浅度克隆一样,也需要两个参数origin, targe
function deepClone(origin, target){ }
对象obj作为原始对象,深度克隆,把obj拷贝给obj1。
var obj = { name : 'abc', age : 123, card : ['vise', 'master'], wife : { name : "lili", son : { } } }
先考虑一下深度克隆的步骤:
1. 遍历对象,判断是不是原始值
// 1). 首先把obj所有的属性值都挨个遍历一遍。 // 2). 每次遍历的时候判断一下,当前值是原始值还是引用值。 // 3). 是原值直接拷贝,原始值之间到拷贝是栈内存的拷贝,直接复制值过去。 // PS:引用值也是栈内存赋值,只不过复制的是一个地址,所以不行。 var obj1 = { name : 'abc', // 原值直接拷贝 age : 123, // 原值直接拷贝 }
2. card是引用值怎么办?
// 1). 判断card是不是原始值,不是原始值是引用值。 // 2). 引用值有两种,判断是数组还是对象 // 3). 如果是数组建立新数组,如果是对象建立新对象。 var obj1 = { name : 'abc', age : 123, card : [], // car不原始值是引用值怎么办?判断是数组还是对象,是数组建立新数组、空数组 }
3. 建立完相应的数组或对象后怎么办?
// 1). 然后,建立一个新的引用值之后,把原始的引用值当做被拷贝对象,把新的空数组当做拷贝对象,再一次进行深度拷贝。 // 2). 这样判断的过程就循环了,重新从开始的第一部开始走了。 // 3). 先判断数组里面第一位是不是引用值,如果是原始值直接拷贝,如果是引用值看看是那种类型,是数组还是对象,到这就开始循环了。 // 4). 数组里面都是原始值,直接拷贝 var obj1 = { name : 'abc', age : 123, card : [obj.car[0], obj.car[1]], // 把原始的引用值当做被拷贝对象,把新的空数组当做拷贝对象,再一次进行深度拷贝。 } // 数组里面都是原始值,直接拷贝
然后,看下一个wife是对象
// 1). 先判定是引用值,然后判定是对象,判定后建立一个空对象{} // 2). 完后,做类似的wife对象到{}空对象的拷贝。 // 3). 紧接着,判定过程又循环了,看看wife对象里面的每一个值是不是原始值, // 4). 第一个name是不是原始值,是原始值直接拷贝。 // 5). 第二个son不是原始值,建立新的对象, // 6). 再把对象obj.wife.son到obj1.wife.son新建空对象的拷贝过程循环一次。 var obj1 = { name : 'abc', age : 123, card : [obj.card[0], obj.card[1]], wife : {} // 然后,看下一个wife是对象,建立一个空对象 } var obj1 = { name : 'abc', age : 123, card : [obj.card[0], obj.card[1]]], wife : { name : "bcd", // 第一个name是原始值直接拷贝。 } } var obj1 = { name : 'abc', age : 123, card : [obj.card[0], obj.card[1]]], wife : { name : "bcd", son : {} // 第二个son不是原始值,建立新的对象 } } var obj1 = { name : 'abc', age : 123, card : [obj.card[0], obj.card[1]]], wife : { name : "bcd", son : { name : "lili", // 再把对象obj.wife.son到这个{}对象的拷贝过程循环一次。 } } }
开始写代码:
遍历对象 for(var prop in obj)
1. 判断是不是原始值 用typeof()判断,如果结果是object,是引用值,如果结果不是,基本上是原始值
2. 判定是数组还是对象 instanceof、toString、constructor
3. 建立相应的数组或对象
主要来说就三步,三步之外就开始循环重复这三步了。
首先要便历对象,遍历对象用"for in"循环,"for in"除了遍历也能遍历数组。
// 数组没有属性名也能"for in"循环 var arr = ['a', 'b', 'c']; for(var prop in arr){ console.log(prop); // prop代表索引位,依次代表0,1,2... }
数组也能"for in",因为数组也算特殊类型的对象。
原始值怎么判断?用typeof()判断,如果结果是object是引用值,如果结果不是,基本上是原始值。
console.log(typeof({})); // object console.log(typeof([])); // object var a = 'abc'; console.log(typeof(a)); // string
数组还是对象怎办判断?有三种方法instanceof、toString、constructor。
Object.prototype.toString.call({}); // [object Object] Object.prototype.toString.call([]); // [object Array] /*** 判断是数组还是对象的三种方法instanceof、toString、constructor,用的时候这三种都行,建议大家用toString。因为instanceof、constructor有个小问题,这个问题基本上一辈子也不会遇到但是确实有。 这个问题是,js学到后期会有父子域的问题,也就是说一个页面里未必只有一个页面,可能还有个子页面,比如"<iframe src="http://baidu.com"></iframe>"有一个父子域。 子域里面的一个数组,放到父域里面的Array,"[] instanceof Array",会打印false,正常来说是true,但父子域里面是false。 只有toString没有父子域的问题。 ***/
做预备工作
1). 先做一个容错"target = traget || {}",后期"或运算符"对值的应用基本上是这样。
2). 会用到"Objecte.prototype.toString()",每次写这么长太麻烦,把它引用出来,这个"toStr.call()"就代表引用了。用的时候toStr.call()就行了。
3). "Object.prototype.toString.call([])"得出的结果要经历比对,比如跟数组比对还是跟对象比对。跟数组比对出来的是"[object Array]",把"[object Array]"弄能字符串储存起来。
function deepClone(origin, target){ var target = traget || {}, toStr = Object.prototype.toString, arrStr = "[object Array]"; }
紧接着开始遍历
// 遍历对象origin function deepClone(origin, target){ var target = traget || {}, toStr = Object.prototype.toString, arrStr = "[object Array]"; for(var prop in origin){ if(origin.hasOwnProperty(prop)){ } } }
第一步,判断是不是原始值
判断每一个值是不是原始值,判断之前但凡是"for in"循环,要跟一个"hasOwnProperty(prop)"判断一下,别把原型链上的东西拿下来。如果是true再进行下一步判断,如果不是true代表是原型上到属性,原型上的属性不拷贝。
判断是不是原始值,判断"typeof origin[prop]"(用方括号的形式)的结果,如果结果是"object"代表引用值,不是基本上是原始值,
是原始值直接赋值拷贝"target[prop] = origin[prop]",这一下就把原始值搞定了。null先不考虑最后再补充。
function deepClone(origin, target){ var target = traget || {}, toStr = Object.prototype.toString, arrStr = "[object Array]"; for(var prop in origin){ if(origin.hasOwnProperty(prop)){ if(typeof origin[prop] == 'Object'){ // 判定是引用值 // 引用值... }else{ target[prop] = origin[prop]; // 原始值直接搞定 } } } } // PS: hasOwnProperty()尽量写上,因为deepClone写的是一个方法的抽象,因为不知道未来传进来的对象是什么样的,所以尽量把方法写全。这是克隆方法的一个抽象,方法都是抽象来的,方法就是要应用于任何的情况。 // 未来传进来的对象有可能就有原型,所以hasOwnProperty()尽量得写(为了避免拿对象原型链上的属性)。 // 原始值上还有包装类,在包装类上建东西也能拿下来。
接下来到引用值,第二步,第三步都是给引用值准备的。第二步判断是数组还是对象,第三步判断完建立相应的数组或对象。
已知"origin[prop]"已经是引用值了,判断是数组的引用值还是对象的引用值,怎么判断?用Object.prototype.toString一下已经写好了引用"toStr.call(origin[prop])"。
当返回值等于"[object Array]",是数组就建立一个"[]"空数组,是对象就建立一个空对象。在哪建立数组/对象?在"target[prop]"身上建立数组或对象。
function deepClone(origin, target){ var target = traget || {}, toStr = Object.prototype.toString, arrStr = "[object Array]"; for(var prop in origin){ if(origin.hasOwnProperty(prop)){ if(typeof origin[prop] == 'object'){ if(toStr.call(origin[prop]) == arrStr){ // "origin[prop]"已经是引用值了,判断是数组的引用值,还是对象的引用值 target[prop] = []; // 建立数组 }else{ target[prop] = {}; // 建立对象 } }else{ target[prop] = origin[prop]; } } } }
第三部完事了,然后递归。
递归的过程,是把已有的原始数组当做拷贝对象,把新建立的空数组当做被拷贝对象。origin[prop],target[prop]再来一遍这样的循环
function deepClone(origin, target){ var target = target || {}, toStr = Object.prototype.toString, arrStr = "[object Array]"; for(var prop in origin){ if(origin.hasOwnProperty(prop)){ if(typeof origin[prop] == 'object'){ if(toStr.call(origin[prop]) == arrStr){ target[prop] = []; }else{ target[prop] = {}; } deepClone(origin[prop], target[prop]); // 递归,origin[prop],target[prop]再来一遍这样的循环。 }else{ target[prop] = origin[prop]; // 递归出口,当origin[prop]是原始值,就是出口了。 } } } }
试一下深层拷贝
function deepClone(origin, target){ var target = target || {}, toStr = Object.prototype.toString, arrStr = "[object Array]"; for(var prop in origin){ if(origin.hasOwnProperty(prop)){ if(typeof origin[prop] == 'object'){ if(toStr.call(origin[prop]) == arrStr){ target[prop] = []; }else{ target[prop] = {}; } deepClone(origin[prop], target[prop]); }else{ target[prop] = origin[prop]; } } } } var obj = { name : 'abc', age : 123, card : ['vise', 'master'], wife : { name : "bcd", son : { name : "aaa" } } } var obj1 = {} deepClone(obj, obj1); console.log(obj1); // {name: "abc", age: 123, card: Array(2), wife: {…}} console.log(obj); // {name: "abc", age: 123, card: Array(2), wife: {…}} obj.card.push('abc'); // obj.card有两个值,再push一个abc进去 console.log(obj1.card); // obj1.card还是两个["vise", "master"],它两个互没影响,深层拷贝实现了。我的就是我的,你的就是你的,引用值也是你的就是你的,我的就是我的,完全分开了。
这就是深度拷贝的一个过程。
再完稍微完善一下,如果没传target,要把target返回出去。还有null的问题,在那判断?
function deepClone(origin, target){ var target = target || {}, toStr = Object.prototype.toString, arrStr = "[object Array]"; for(var prop in origin){ if(origin.hasOwnProperty(prop)){ // null的问题在这判断。 // 什么是引用值?不是null而且typof结果还是object。所以前面还要加一条typeof origin[prop] !== "null"绝对不等于"null",绝对不等于有隐式类型转换都不行。把后面两个条件叠加在一起。 if(typeof origin[prop] !== "null" && typeof origin[prop] == 'object'){ if(toStr.call(origin[prop]) == arrStr){ target[prop] = []; }else{ target[prop] = {}; } deepClone(origin[prop], target[prop]); }else{ target[prop] = origin[prop]; } } } return target; // 如果没传target,要把target返回出去。 } var obj = { name : 'abc', age : 123, card : ['vise', 'master'], wife : { name : "bcd", son : { name : "aaa" } } } var objCopy = deepClone(obj); console.log(objCopy); // {name: "abc", age: 123, card: Array(2), wife: {…}} console.log(obj); // {name: "abc", age: 123, card: Array(2), wife: {…}} obj.card.push('abc'); // obj.card有两个值,再push一个abc进去 console.log(objCopy.card); // objCopy.card还是两个["vise", "master"]