js中深拷贝与浅拷贝解析

深拷贝

深拷贝是指源对象与拷贝对象指针指向的内存空间不是同一块空间,相互独立,其中任何一个对象的改动都不会对另外一个对象造成影响。

浅拷贝

浅拷贝是指源对象与拷贝对象的指针指向的内存空间是同一块空间,其中任何一个对象的改动都会对另一个对象造成影响。

在进入正题之前,不知大家对内存空间了解多少,若是无法区分栈内存堆内存,建议先去学习一下内存空间的知识:内存空间详解、浅析JS中的堆内存与栈内存

一、数据类型

先来几个概念:

在js中数据类型分为基本数据类型(String、Number、Boolean、Null、Undefined、Symbol)和复杂数据类型(Object)。

基本类型值:简单的数据段。
引用类型值:由多个值构成的对象。

基本类型值是按值访问的,保存在栈内存中,比如基本数据类型(String、Number、…)。
引用类型值是按引用访问的,引用保存在栈内存中,真实的值保存在堆内存中,比如复杂数据类型(Object)。
js中深拷贝与浅拷贝解析_第1张图片

二、浅拷贝与深拷贝

深拷贝和浅拷贝主要是针对Object和Array这样的复杂数据类型。
仔细观察下面的示例图:
js中深拷贝与浅拷贝解析_第2张图片
浅拷贝后的New List Head的指向与Original List Head的指向其实是同一个Node,因为浅拷贝复制的其实是栈内存中对象的引用,而不是保存在堆内存中真实的值,两个对象还是在共用同一个堆内存地址。而深拷贝则是创造了一个与原来一模一样的对象,指向(引用)不同,堆内存地址也不同,两个对象相互独立,互不影响。

三、浅拷贝与对象赋值

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈内存中的引用地址,而不是堆内存中的数据。也就是两个对象指向的是同一个堆内存空间,无论哪个对象发生改变,其实都是改变的堆内存空间中的数据,因此,两个对象是联动的。

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 。因此,如果其中一个对象改变了这个地址,就会影响到另一个对象。

来看两个例子加深一下印象:

对象赋值:

var obj = {
	name: '王五',
	age: 28,
	gender: ['男','女']
}
var obj2 = obj;

obj2.name = '李四';
obj2.gender[0] = '未知';

console.log('obj',obj);
console.log('obj2',obj2);

打印到控制台,不管是我们修改了obj2中的基本类型值还是引用类型值,obj中的值都被改变了,变成了相同的值。
js中深拷贝与浅拷贝解析_第3张图片
浅拷贝:

// 浅拷贝辅助函数
function shallowCopy(data){
	var copy = {};
	for(var key in data){
		if (data.hasOwnProperty) {
			copy[key] = data[key];
		}
	};
	return copy;
}

var obj = {
	name: '王五',
	age: 28,
	gender: ['男','女']
}
var obj3 = shallowCopy(obj);

obj3.name = '李四';
obj3.gender[0] = '未知';

console.log('obj',obj);
console.log('obj3',obj3);

来看打印结果,当对obj进行浅拷贝生成obj3时,改变obj3中的基本类型值和引用类型值,发现obj中的引用类型值发生了改变,基本类型值未发生改变。
js中深拷贝与浅拷贝解析_第4张图片

由上面的例子我们可以推测出新对象对源数据的影响:

和源数据是否指向同一内存地址 第一层数据为基本类型值 源数据中包含子对象
对象赋值 改变会使源数据一同改变 改变会使源数据一同改变
浅拷贝 改变不会使源数据改变 改变会使源数据一同改变
深拷贝 改变不会使源数据改变 改变不会使源数据改变
四、浅拷贝的实现方式

1、Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

Object.assign()进行的是浅拷贝,拷贝的是对象属性的引用,而不是对象本身。

var source = {
	obj: { a: '哈哈哥', b: 40 },
};
var target = Object.assign({}, source);

target.obj.a = '嘿嘿哥';
target.obj.b = 50;

console.log(source);	// { obj: { a: '嘿嘿哥', b: '50' } }
console.log(target);	// { obj: { a: '嘿嘿哥', b: '50' } }

当Object.assign()对只有一层基本数据类型进行拷贝时,相当于深拷贝:

const source= { a: 1, b: 2 };
const target = Object.assign({}, source);

target.a = 111;
target.b = 222;

console.log(source);	// { a: 1, b: 2 }
console.log(target);	// { a: 111, b: 222 }

2、Array.prototype.concat()

concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

var source = [1, 2, { name: 'CSDN', age: 21 }];
var target = source.concat();

target[0] = 100;
target[2].name = 'csdn';

console.log(source);	// [1, 2, { name: 'csdn', age: 21 }];
console.log(target);	// [100, 2, { name: 'csdn', age: 21 }];

3、Array.prototype.slice()

slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。

var source = [1, 2, { name: 'CSDN', age: 21 }];
var target = source.slice();

target[0] = 100;
target[2].name = 'csdn';

console.log(source);	// [1, 2, { name: 'csdn', age: 21 }];
console.log(target);	// [100, 2, { name: 'csdn', age: 21 }];

Array的slice和concat方法不修改原数组(source),只会返回一个浅拷贝了原数组(source)中元素的一个新数组(target)。当修改新数组(target)中的基本类型值时,原数组(source)中的基本类型值不会改变,当修改新数组(target)中的引用类型值时,原数组(source)中的引用类型值也会被修改。

五、深拷贝的实现方式

1、JSON.parse(JSON.stringify())
此方法就是利用JSON.stringify将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象。

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。

说一下我的理解:对象是引用数据类型,当使用JSON.stringify将js对象序列化为JSON字符串后,对象从引用数据类型变为了基本数据类型(这是重点),原本的引用也就失去了作用;这时候再使用JSON.parse来反序列化JSON字符串,便生成了一个全新的对象。

var source = [1, 2, { name: 'CSDN', age: 21 }];
var target =JSON.parse(JSON.stringify(source));

target[0] = 100;
target[2].name = 'csdn';

console.log(source);	// [1, 2, { name: 'CSDN', age: 21 }];
console.log(target);	// [100, 2, { name: 'csdn', age: 21 }];

JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数。

var source = [1, 2, { name: 'CSDN', age: 21 }, function(){}];
var target =JSON.parse(JSON.stringify(source));

target[2].name = 'csdn';

console.log("source:", source, "target:", target);

当转换的是一个函数时,会被解析为null,如下图:
js中深拷贝与浅拷贝解析_第5张图片
2、递归方法
递归方法实现深拷贝原理:遍历对象、数组,直到里边的值都是基本数据类型,然后赋值。

function deepClone(obj) {
	if (obj && typeof obj === 'object') {
		const result = obj instanceof Array === true ? [] : {};
		for (let key in obj) {
			if (obj.hasOwnProperty(key)) {
				if (obj[key] && typeof obj[key] === 'object') {
					result[key] = deepClone(obj[key]); //此处判断当前值若为引用类型,则继续循环调用
				} else {
					result[key] = obj[key];
				}
			}
		}
		return result;
	} else {
		console.error('输入的参数为空或者不为对象');
		return '输入的参数为空或者不为对象';
	}
}

const obj1 = [1, 2, { name: 'CSDN', age: 21 }];
const obj2 = {
	a: 1,
	b: 2,
	c: { name: 'CSDN', age: 21 },
};
console.log('deepClone1', deepClone(obj1));
console.log('deepClone2', deepClone(obj2));

打印结果如下:
js中深拷贝与浅拷贝解析_第6张图片
3、Lodash.js

Lodash是一个一致性、模块化、高性能的 JavaScript 实用工具库。

可以使用Lodash工具库中的cloneDeep方法
Lodash默认使用 _ 表示,就跟JQuery库中 $ 代表的是jQuery一样。

import _ from 'lodash';
const obj = {
	a: 1,
	b: [2, 3],
	c: { name: 'CSDN', age: 21 },
};
const result = _.cloneDeep(obj);
console.log(result.b === obj.b); // false
console.log(result.c === obj.c); // false

你可能感兴趣的:(js,前端)