JavaScript中的深复制和浅复制

时间:2020-12-22 19:54:05

     在谈javascript的浅复制和深复制之前,我们先来看看js的数据类型:有Number,Boolean,String,Null,Undefined,Object五种类型。而Object又包含Function,Array和Object自身。前面的五种类型叫做基本类型,而Object是引用类型。之所以要先了解 这几种数据类型,是因为JS 中的浅拷贝与深拷贝,其实只是针对复杂数据类型(ObjectArray)的复制问题。

     我们首先来看看浅复制和深复制的简洁定义:

  • 浅拷贝:浅拷贝是拷贝引用,拷贝后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响;
  • 深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。
     由深复制的定义来看,深复制要求如果源对象存在对象属性,那么需要进行递归复制,从而保证复制的对象与源对象完全隔离。然而还有一种可以说处在浅复制和深复制的粒度之间,也是jQuery的extend方法在deep参数为false时所谓的“浅复制”,这种复制只进行一个层级的复制:即如果源对象中存在对象属性,那么复制的对象上也会引用相同的对象。这不符合深复制的要求,但又比简单的复制引用的复制粒度有了加深。
1、浅复制      我们先来看一个简单的例子:
var a = {c:1};
var b = a;
console.log(a === b); // 输出true
//修改源对象
a.c = 2;
console.log(b.c); // 输出 2
//修改复制对象
b.c = 3;
console.log(b.c); // 输出 3
     我们声明一个变量a; var b = a;把a复制给b。b通过a获得{c:1}这个对象,但它们并非两个不同的对象,实际上他们的指针都是指向同一个对象(上面输出true)。所以当我们通过a重新给{c:1} 赋值的时候,我们可以看到b也对应的改变了(上面输出2),同时通过b重新给{c:1} 赋值的时候,我们可以看到a也对应的改变了(上面输出3)。

2、深复制      因为对象相对较为复杂,所以我们先来看对数组的深拷贝的问题。
    2.1 Array的slice和concat方法
          Array的slice和concat方法都会返回一个新的数组实例,但是这两个方法对于数组中的对象元素却没有执行深复制,而只是复制了引用了,因此这两个方法并不是真正的深复制,通过以下代码进行理解:
var array = [1,2,3]; 
var array_shallow = array;
var array_concat = array.concat();
var array_slice = array.slice(0);
console.log(array === array_shallow); //true
console.log(array === array_slice); //false
console.log(array === array_concat); //false
      从输出结果,我们可以看出,concat和slice返回的不同的数组实例,这与直接的引用复制是不同的。
      如果我们想实现一个数组的深复制,可以采用遍历数组,并依次复制的方式。
//深复制
var a = [1,2,3];
var deepArry = [];

function deepCopy(arry1, arry2){
for(var i = 0; i < arry1.length; i ++){
arry2[i] = arry1[i];
}
}

deepCopy(a,deepArry);
console.log(a);
console.log(deepArry);

deepArry[0] =5;
console.log(a);
console.log(deepArry);

2.2 JSON对象的parse和stringify

    JSON对象是ES5中引入的新的类型(支持的浏览器为IE8+),JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,借助这两个方法,也可以实现对象的深复制。

var source = { name:"source", child:{ name:"child" } } 
var target = JSON.parse(JSON.stringify(source));
console.log(source === target); //false
target.name = "target"; //改变target的name属性
console.log(source.name); //source
console.log(target.name); //target
target.child.name = "target child"; //改变target的child
console.log(source.child.name); //child
console.log(target.child.name); //target child

     从代码的输出可以看出,复制后的target与source是完全隔离的,通过结果可以看出,这两个是不同的对象,二者不会相互影响。这个方法使用较为简单,可以满足基本的深复制需求,而且能够处理JSON格式能表示的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深复制(而且会直接丢失相应的值),同时如果对象中存在循环引用的情况也无法正确处理。
      2.3 jQuery中的extend方法

jQuery.extend( target [, object1 ] [, objectN ] )
      extend方法需要至少传入一个参数,第一个必需,后面的都是可选参数。若传给extend是两个或两个以上的参数都是对象类型,那么就会把后面所有对象的内容合并给target(第一个对象)上。

     值得注意的是,不过jQuery.extend()也提供了深度拷贝的方法:jQuery.extend( [deep ], target, object1 [, objectN ] )。若第一个参数是boolean类型,且值是true,那么就会把第二个参数作为目标参数进行合并。
    现附上一段其他大牛对jQuery中extend的源码解析:

// 为与源码的下标对应上,我们把第一个参数称为`第0个参数`,依次类推
jQuery.extend = jQuery.fn.extend = function() {
var options, name, src, copy, copyIsArray, clone,
target = arguments[0] || {}, // 默认第0个参数为目标参数
i = 1, // i表示从第几个参数凯斯想目标参数进行合并,默认从第1个参数开始向第0个参数进行合并
length = arguments.length,
deep = false; // 默认为浅度拷贝

// 判断第0个参数的类型,若第0个参数是boolean类型,则获取其为true还是false
// 同时将第1个参数作为目标参数,i从当前目标参数的下一个
// Handle a deep copy situation
if ( typeof target === "boolean" ) {
deep = target;

// Skip the boolean and the target
target = arguments[ i ] || {};
i++;
}

// 判断目标参数的类型,若目标参数既不是object类型,也不是function类型,则为目标参数重新赋值
// Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
target = {};
}

// 若目标参数后面没有参数了,如$.extend({_name:'wenzi'}), $.extend(true, {_name:'wenzi'})
// 则目标参数即为jQuery本身,而target表示的参数不再为目标参数
// Extend jQuery itself if only one argument is passed
if ( i === length ) {
target = this;
i--;
}

// 从第i个参数开始
for ( ; i < length; i++ ) {
// 获取第i个参数,且该参数不为null和undefind,在js中null和undefined,如果不区分类型,是相等的,null==undefined为true,
// 因此可以用null来同时过滤掉null和undefind
// 比如$.extend(target, {}, null);中的第2个参数null是不参与合并的
// Only deal with non-null/undefined values
if ( (options = arguments[ i ]) != null ) {

// 使用for~in获取该参数中所有的字段
// Extend the base object
for ( name in options ) {
src = target[ name ]; // 目标参数中name字段的值
copy = options[ name ]; // 当前参数中name字段的值

// 若参数中字段的值就是目标参数,停止赋值,进行下一个字段的赋值
// 这是为了防止无限的循环嵌套,我们把这个称为,在下面进行比较详细的讲解
// Prevent never-ending loop
if ( target === copy ) {
continue;
}

// 若deep为true,且当前参数中name字段的值存在且为object类型或Array类型,则进行深度赋值
// Recurse if we're merging plain objects or arrays
if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
// 若当前参数中name字段的值为Array类型
// 判断目标参数中name字段的值是否存在,若存在则使用原来的,否则进行初始化
if ( copyIsArray ) {
copyIsArray = false;
clone = src && jQuery.isArray(src) ? src : [];

} else {
// 若原对象存在,则直接进行使用,而不是创建
clone = src && jQuery.isPlainObject(src) ? src : {};
}

// 递归处理,此处为2.2
// Never move original objects, clone them
target[ name ] = jQuery.extend( deep, clone, copy );

// deep为false,则表示浅度拷贝,直接进行赋值
// 若copy是简单的类型且存在值,则直接进行赋值
// Don't bring in undefined values
} else if ( copy !== undefined ) {
// 若原对象存在name属性,则直接覆盖掉;若不存在,则创建新的属性
target[ name ] = copy;
}
}
}
}

// 返回修改后的目标参数
// Return the modified object
return target;
};
     看完了解析,下面写一个简单的例子:

var obj = { name:'Wendy', score:80};
var obj1 = { score:{ English:80, math:90}}
$.extend(true, obj, obj1);
obj.score.English = 100;
console.log(obj.score.English); // 100
console.log(obj1.score.English); // 80
     执行后我们发现,无论怎么修改obj.score里的值,都不会影响到obj1.score了。