Go to comments

JavaScript 克隆

克隆


一个已有的对象obj,现在把对象obj克隆给一个空对象obj1

01var obj = {
02    name : 'abc',
03    age : '123,',
04    sex : 'female'
05}
06 
07var obj1 = {}
08 
09/**
10 * 写一个克隆方法clone(origin, target)
11 * origin  Object  原始的对象
12 * target  Object  模板对象
13 * */ 
14function clone(origin ,target){
15    for(var prop in origin){         // 循环把origin上面所有的属性都拿出来
16        target[prop] = origin[prop]; // 每一次循环拿出的属性名origin[prop],让"target[prop] = origin[prop]"。
17    }
18}
19 
20clone(obj ,obj1); // 克隆obj
21console.log(obj1); // 打印obj1 {name: "abc", age: "123,", sex: "female"}

这个方法再兼容一下(容个错)防止用户不传target。

如果用户传一个空对象就用户传的,如果没传就新建一个空对象,换句话说不传第二个参数也行。

01function 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 
09var obj = {
10    name : 'abc',
11    age : '123,',
12    sex : 'female'
13}
14 
15var obj1 = clone(obj);
16console.log(obj1); // {name: "abc", age: "123,", sex: "female"}

有一个问题,这个克隆是浅层克隆,浅层克隆原始值克隆完后是没问题的。

浅层克隆

01function 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 
10var obj = {
11    name : 'abc',
12    age : '123,',
13    sex : 'female',
14}
15 
16var obj1 = clone(obj)
17 
18console.log(obj);    // obj  {name: "abc", age: "123,", sex: "female"}
19console.log(obj1);   // obj1 {name: "abc", age: "123,", sex: "female"} 
20 
21obj.name = 'bcd';          // 现在让obj的name值改成bcd
22console.log(obj1.name);    // obj1的name没有变化还是abc,因为是原始值,原始值就是值的拷贝

原始值就是值的拷贝,现在也让值拷贝,但是让引用值拷贝。

01function clone(origin ,target){
02    var target = target || {};
03    for(var prop in origin){
04        target[prop] = origin[prop];
05    }
06    return target;
07}
08 
09var obj = {
10    name : 'abc',
11    age : '123,',
12    sex : 'female',
13    car : ['visa','unionpay'// 里面加一个数组
14}
15 
16var obj1 = clone(obj);
17 
18console.log(obj1);       // car1 ["visa", "unionpay"]
19obj1.car.push('master'); // obj1.car是引用值,push进去一个'master'进去
20 
21console.log(obj1.car);   // obj1.car肯定的会多一个 ["visa", "unionpay", "master"] 
22console.log(obj.car);    // 但是obj.car也被变了 ["visa", "unionpay", "master"]

因为是引用值,拷贝过去的是一个引用,两个人指向的是同一个房间,你改我也改。

现在我们想实现"深度克隆","深度拷贝"的意思是,我拿的和你的属性是一模一样的,但是就不是一个人。

你有一个引用值car里面放的是数组,拷贝过来之后我也有car数组,但是拷贝到我这之后,我改你是不会被更改的,两个人用的是两个房间。

深层克隆

浅度克隆非常简单,就是把A拷贝给B。深度克隆和浅度克隆的区别,深度克隆克隆后,不论是引用值还是原始值,都是各自独立的。

深度克隆和浅度克隆一样,也需要两个参数origin, targe

1function deepClone(origin, target){
2     
3}

对象obj作为原始对象,深度克隆,把obj拷贝给obj1。

01var 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 
6var obj1 = {
7    name : 'abc'// 原值直接拷贝
8    age : 123,    // 原值直接拷贝
9}

2. card是引用值怎么办?

1// 1). 判断card是不是原始值,不是原始值是引用值。
2// 2). 引用值有两种,判断是数组还是对象
3// 3). 如果是数组建立新数组,如果是对象建立新对象。
4 
5var obj1 = {
6    name : 'abc'
7    age : 123, 
8    card : [],    // car不原始值是引用值怎么办?判断是数组还是对象,是数组建立新数组、空数组
9}

3. 建立完相应的数组或对象后怎么办?

01// 1). 然后,建立一个新的引用值之后,把原始的引用值当做被拷贝对象,把新的空数组当做拷贝对象,再一次进行深度拷贝。
02// 2). 这样判断的过程就循环了,重新从开始的第一部开始走了。
03// 3). 先判断数组里面第一位是不是引用值,如果是原始值直接拷贝,如果是引用值看看是那种类型,是数组还是对象,到这就开始循环了。
04// 4). 数组里面都是原始值,直接拷贝
05    
06var 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 
08var obj1 = {
09    name : 'abc',
10    age : 123, 
11    card : [obj.card[0], obj.card[1]],
12    wife : {}                           // 然后,看下一个wife是对象,建立一个空对象
13}
14                                    
15var obj1 = {
16    name : 'abc',
17    age : 123, 
18    card : [obj.card[0], obj.card[1]]],
19    wife : { 
20        name : "bcd",                   // 第一个name是原始值直接拷贝。
21    }
22}
23 
24var 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 
34var 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 in"循环,"for in"除了遍历也能遍历数组。

1// 数组没有属性名也能"for in"循环
2 
3var arr = ['a''b''c'];
4 
5for(var prop in arr){
6    console.log(prop); // prop代表索引位,依次代表0,1,2...
7}

数组也能"for in",因为数组也算特殊类型的对象。

原始值怎么判断?用typeof()判断,如果结果是object是引用值,如果结果不是,基本上是原始值。

1console.log(typeof({})); // object
2 
3console.log(typeof([])); // object
4 
5var a = 'abc';
6console.log(typeof(a)); // string

数组还是对象怎办判断?有三种方法instanceof、toString、constructor。

01Object.prototype.toString.call({}); // [object Object]
02 
03Object.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]"弄能字符串储存起来。

1function deepClone(origin, target){
2    var target = traget || {},
3        toStr = Object.prototype.toString,
4        arrStr = "[object Array]"
5}

紧接着开始遍历

01// 遍历对象origin
02 
03function 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先不考虑最后再补充。

01function 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]"身上建立数组或对象。

01function 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]再来一遍这样的循环

01function 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}

试一下深层拷贝

01function 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 
23var 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 
35var obj1 = {}
36 
37deepClone(obj, obj1);
38 
39console.log(obj1); // {name: "abc", age: 123, card: Array(2), wife: {…}}
40console.log(obj);  // {name: "abc", age: 123, card: Array(2), wife: {…}}
41 
42obj.card.push('abc');   // obj.card有两个值,再push一个abc进去
43 
44console.log(obj1.card); // obj1.card还是两个["vise", "master"],它两个互没影响,深层拷贝实现了。我的就是我的,你的就是你的,引用值也是你的就是你的,我的就是我的,完全分开了。

这就是深度拷贝的一个过程。

再完稍微完善一下,如果没传target,要把target返回出去。还有null的问题,在那判断?

01function 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 
26var 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 
39var objCopy = deepClone(obj);
40 
41console.log(objCopy); // {name: "abc", age: 123, card: Array(2), wife: {…}}
42console.log(obj);  // {name: "abc", age: 123, card: Array(2), wife: {…}}
43 
44obj.card.push('abc');   // obj.card有两个值,再push一个abc进去
45 
46console.log(objCopy.card); // objCopy.card还是两个["vise", "master"]



Leave a comment 0 Comments.

Leave a Reply

换一张