JS 深拷贝与浅拷贝

要理解 JS 的深浅拷贝,先熟悉一下 JS 的数据类型:

6种基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(ES6引入)、BigInt(ES2020引入)

复杂数据类型(引用类型):Object(Array、Date、RegExp、Function、Math都属于Object类型)

Undefined:只有一个值,即 undefined。变量被声明了但未赋值时,这个变量的值就是 undefined。函数没有返回值,默认返回 undefined。typeof undefined 返回值为 “undefined”。

Null:只有一个值,即 null。表示一个空对象指针。typeof null 返回值为 “object”。

undefined == null	// true
undefined === null	// false,全等只在两个值不需要类型转换就相等的情况下才返回 true

Symbol()可以生成一个唯一的值,该数据类型仅有的目的是用来作为对象属性的标识符。

基本数据类型是按值访问的,存储在栈内存中,基本数据类型的值是不可变的。

引用类型在栈内存中存储了一个指向堆内存的地址,在堆内存中存储着实际的数据。

浅拷贝

深拷贝和浅拷贝只针对 Object 和 Array 这类复杂数据类型。简单来说,浅复制只复制一层对象的属性,而深复制则递归复制了所有层级。

赋值(=)与浅拷贝的区别

下面的例子实现了赋值操作:

let obj = {
	a: 1,
	arr: [2, 3]
};
let obj1 = obj;
obj1.a = 4;
console.log(obj);
console.log(obj1);

image-20210510225142719

赋值操作是直接复制对象的引用,obj 和 obj1 指向的还是同一个对象,所以当修改 obj1 时,obj 也会跟着改变。

下面的例子实现了浅拷贝:

let obj = {
	a: 1,
	arr: [2, 3]
};
let scObj = shallowCopy(obj);
scObj.a = 5;
scObj.arr[0] = 6;

function shallowCopy (src) {
	let newObj = {};
	for(let prop in src) {
		if(src.hasOwnProperty(prop)) {
			newObj[prop] = src[prop];
		}
	}
	return newObj;
}

console.log(obj);
console.log(scObj);

image-20210510230105996

浅拷贝只会将对象的各个属性进行依次复制,并不会进行递归复制。和赋值操作不同,上面例子中 obj 和 scObj 指向的已经不是同一个对象了。由于 obj.arr 数组是一个引用类型,引用类型存储的是一个地址,所以 scObj 拷贝的是 obj 对象 a 的值和 obj.arr 数组的地址,obj.arr 和 scObj.arr 指向的还是同一个数组。所以当修改 scObj.a 时,obj.a 的值不会改变,而修改 scObj.arr[0] 时,obj.arr[0] 会跟着改变。

其他实现浅拷贝的方法

  1. 数组的 slice()、concat() 方法
let arr1 = [1, 2, [3, 4]];
let arr2 = arr1.slice(0);
let arr3 = arr1.concat();
console.log(arr2);
console.log(arr3);

image-20210510235506976

  1. Object.assign() 方法
let obj1 = {
	name: 'Tom',
	friend: {
		name: 'Jerry',
		age: 18
	}
};
let obj2 = Object.assign({}, obj1);
console.log(obj2);

image-20210511000001727

  1. 对象和数组的扩展运算符(…)
let obj1 = {
	name: 'Tom',
	friend: {
		name: 'Jerry',
		age: 18
	}
};
let obj2 = {...obj1};
console.log(obj2);

image-20210511000001727

深拷贝

深拷贝不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深拷贝的方法递归复制到新对象上。

实现深拷贝的方法

  1. 通过 JSON 的 parse() 和 stringify() 方法

缺点:不会复制 undefined、symbol、函数。

let obj1 = {
	name: 'Tom',
	age: undefined,
	color: Symbol(),
	friend: {
		name: 'Jerry',
		age: 18
	},
	say() {
		console.log('miao miao miao');
	}
};
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.friend.age = 19;
console.log(obj1);
console.log(obj2);

image-20210511001332302

  1. 使用递归实现
    let obj = {
    	a: 1,
    	arr: [{
    		name: 'Tom',
    		age: 16
    	}, {
    		name: 'Jerry',
    		age: 15
    	}]
    };
    let obj2 = deepCopy(obj);
    obj2.arr[0].name = 'Cat';
    console.log(obj);
    console.log(obj2);
    
    function deepCopy(src) {
        if(typeof src !== 'object') {
            return src;
        }
    	let newObj = Object.prototype.toString.call(src) === '[object Array]' ? [] : {};			// 判断是Array还是Object
    	for (let prop in src) {										// 遍历对象上所有实例属性和原型属性
    		if (src.hasOwnProperty(prop)) {							// 只拷贝实例属性
    			if (typeof src[prop] === 'object') {		// 如果属性的值为Array或Object,则对属性的值递归调用deepCopy()函数
    				newObj[prop] = deepCopy(src[prop]);
    			} else {
    				newObj[prop] = src[prop];
    			}
    		}
    	}
    	return newObj;
    }
    image-20210511202827136

参考:https://www.zhihu.com/question/23031215