js中深拷貝與淺拷貝解析

深拷貝

深拷貝是指源對象與拷貝對象指針指向的內存空間不是同一塊空間,相互獨立,其中任何一個對象的改動都不會對另外一個對象造成影響。

淺拷貝

淺拷貝是指源對象與拷貝對象的指針指向的內存空間是同一塊空間,其中任何一個對象的改動都會對另一個對象造成影響。

在進入正題之前,不知大家對內存空間瞭解多少,若是無法區分棧內存堆內存,建議先去學習一下內存空間的知識:內存空間詳解淺析JS中的堆內存與棧內存

一、數據類型

先來幾個概念:

在js中數據類型分爲基本數據類型(String、Number、Boolean、Null、Undefined、Symbol)和複雜數據類型(Object)。

基本類型值:簡單的數據段。
引用類型值:由多個值構成的對象。

基本類型值是按值訪問的,保存在棧內存中,比如基本數據類型(String、Number、…)。
引用類型值是按引用訪問的,引用保存在棧內存中,真實的值保存在堆內存中,比如複雜數據類型(Object)。
在這裏插入圖片描述

二、淺拷貝與深拷貝

深拷貝和淺拷貝主要是針對Object和Array這樣的複雜數據類型。
仔細觀察下面的示例圖:
在這裏插入圖片描述
淺拷貝後的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中的值都被改變了,變成了相同的值。
在這裏插入圖片描述
淺拷貝:

// 淺拷貝輔助函數
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中的引用類型值發生了改變,基本類型值未發生改變。
在這裏插入圖片描述

由上面的例子我們可以推測出新對象對源數據的影響:

和源數據是否指向同一內存地址 第一層數據爲基本類型值 源數據中包含子對象
對象賦值 改變會使源數據一同改變 改變會使源數據一同改變
淺拷貝 改變不會使源數據改變 改變會使源數據一同改變
深拷貝 改變不會使源數據改變 改變不會使源數據改變
四、淺拷貝的實現方式

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,如下圖:
在這裏插入圖片描述
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));

打印結果如下:
在這裏插入圖片描述
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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章