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