前言
學(xué)習(xí)這一部分我們要明白:
- 基本類(lèi)型和引用類(lèi)型的區(qū)別
- 什么是深/淺拷貝,他們跟賦值有何區(qū)別?
- 深/淺拷貝的實(shí)現(xiàn)方式有幾種耗式?
其實(shí)深拷貝和淺拷貝都是針對(duì)的引用類(lèi)型,JS中的變量類(lèi)型分為值類(lèi)型(基本類(lèi)型)和引用類(lèi)型;對(duì)值類(lèi)型進(jìn)行復(fù)制操作會(huì)對(duì)值進(jìn)行一份拷貝纽什,而對(duì)引用類(lèi)型復(fù)制措嵌,則會(huì)進(jìn)行地址的拷貝躲叼,最終兩個(gè)變量指向同一份數(shù)據(jù)
淺拷貝是創(chuàng)建一個(gè)新對(duì)象芦缰,這個(gè)對(duì)象有著原始對(duì)象屬性值的一份精確拷貝。如果屬性是基本類(lèi)型枫慷,拷貝的就是基本類(lèi)型的值让蕾,如果屬性是引用類(lèi)型,拷貝的就是棧內(nèi)存地址 或听,所以如果其中一個(gè)對(duì)象改變了這個(gè)地址探孝,就會(huì)影響到另一個(gè)對(duì)象。
深拷貝是將一個(gè)對(duì)象從內(nèi)存中完整的拷貝一份出來(lái),從堆內(nèi)存中開(kāi)辟一個(gè)新的區(qū)域存放新對(duì)象,且修改新對(duì)象不會(huì)影響原對(duì)象誉裆。
總而言之顿颅,淺拷貝只復(fù)制指向某個(gè)對(duì)象的指針,而不復(fù)制對(duì)象本身足丢,新舊對(duì)象還是共享同一塊內(nèi)存粱腻。但深拷貝會(huì)另外創(chuàng)造一個(gè)一模一樣的對(duì)象,新對(duì)象跟原對(duì)象不共享內(nèi)存斩跌,修改新對(duì)象不會(huì)改到原對(duì)象绍些。
基本類(lèi)型和引用類(lèi)型的區(qū)別
Js常見(jiàn)的六種數(shù)據(jù)類(lèi)型
Object、Number耀鸦、String柬批、Blooean、Undefined袖订、Null
基本類(lèi)型
Number氮帐、String、Blooean洛姑、Undefined上沐、Null、Symbol(ES6)
基本數(shù)據(jù)類(lèi)型保存在棧內(nèi)存中吏口,因?yàn)榛緮?shù)據(jù)類(lèi)型占用空間小奄容、大小固定,通過(guò)按值來(lái)訪問(wèn)产徊,屬于被頻繁使用的數(shù)據(jù)昂勒。
基本數(shù)據(jù)類(lèi)型的復(fù)制就是在棧內(nèi)存中開(kāi)辟出了一個(gè)新的存儲(chǔ)區(qū)域用來(lái)存儲(chǔ)新的變量,這個(gè)變量有它自己的值舟铜,只不過(guò)和前面的值一樣戈盈,所以如果其中一個(gè)的值改變,則不會(huì)影響到另一個(gè)。
引用類(lèi)型(復(fù)雜類(lèi)型)
Object(對(duì)象):包括Function塘娶、Array归斤、Date、RegExp都屬于對(duì)象的引用類(lèi)型
引用數(shù)據(jù)類(lèi)型存儲(chǔ)在堆內(nèi)存中刁岸,因?yàn)橐脭?shù)據(jù)類(lèi)型占據(jù)空間大脏里、占用內(nèi)存不固定。 如果存儲(chǔ)在棧中虹曙,將會(huì)影響程序運(yùn)行的性能迫横; 引用數(shù)據(jù)類(lèi)型在棧中存儲(chǔ)了指針,該指針指向堆中該實(shí)體的起始地址酝碳。 當(dāng)解釋器尋找引用值時(shí)矾踱,會(huì)首先檢索其在棧中的地址,取得地址后從堆中獲得實(shí)體
復(fù)制給另一個(gè)對(duì)象的過(guò)程其實(shí)是把該對(duì)象的地址復(fù)制給了另一個(gè)對(duì)象變量疏哗,兩個(gè)指針都指向同一個(gè)堆內(nèi)存對(duì)象呛讲,所以若其中一個(gè)修改了,則另一個(gè)也會(huì)改變返奉。
棧內(nèi)存(stack)和堆內(nèi)存(heap)
棧內(nèi)存:是一種特殊的線性表贝搁,它具有后進(jìn)先出的特性,存放基本類(lèi)型衡瓶。
堆內(nèi)存:存放引用類(lèi)型(在棧內(nèi)存中存一個(gè)基本類(lèi)型值保存對(duì)象在堆內(nèi)存中的地址徘公,用于引用這個(gè)對(duì)象)。
賦值和深/淺拷貝的區(qū)別
這三者的區(qū)別如下哮针,不過(guò)比較的前提都是針對(duì)引用類(lèi)型:
當(dāng)我們把一個(gè)對(duì)象賦值給一個(gè)新的變量時(shí)关面,賦的其實(shí)是該對(duì)象的在棧中的地址,而不是堆中的數(shù)據(jù)十厢。也就是兩個(gè)對(duì)象指向的是同一個(gè)存儲(chǔ)空間等太,無(wú)論哪個(gè)對(duì)象發(fā)生改變,其實(shí)都是改變的存儲(chǔ)空間的內(nèi)容蛮放,因此缩抡,兩個(gè)對(duì)象是聯(lián)動(dòng)的。
淺拷貝:基本數(shù)據(jù)類(lèi)型重新在堆中創(chuàng)建內(nèi)存包颁,所以拷貝前后對(duì)象的基本數(shù)據(jù)類(lèi)型互不影響瞻想,但拷貝前后對(duì)象的引用類(lèi)型因共享同一塊內(nèi)存,會(huì)相互影響娩嚼。
深拷貝:從堆內(nèi)存中開(kāi)辟一個(gè)新的區(qū)域存放新對(duì)象蘑险,對(duì)對(duì)象中的子對(duì)象進(jìn)行遞歸拷貝,拷貝前后的兩個(gè)對(duì)象互不影響。
看下面的例子岳悟,對(duì)比賦值與深/淺拷貝得到的對(duì)象修改后對(duì)原始對(duì)象的影響:
// 對(duì)象賦值
let obj1 = {
name: 'cobe',
arr: [1, 2, [3, 5], 4]
}
let obj2 = obj1 // obj1的值賦值給obj2
obj2.name = 'curry'
obj2.arr[2] = [1, 8, 1]
console.log(obj1) // obj1 { name: 'curry', arr[1, 2, [1, 8, 1], 4]}
console.log(obj2) // obj2 { name: 'curry', arr[1, 2, [1, 8, 1], 4]}
// 淺拷貝
let obj1 = {
name: 'cobe',
arr: [1, 2, [3, 5], 4]
}
let obj3 = shallowClone(obj1)
obj3.name = 'James'
obj3.arr[2] = [3, 8]
// 這是個(gè)淺拷貝的方法
function shallowClone(source) {
var target = {};
for(var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
}
console.log(obj1); // obj1 { name: 'cobe', arr[1, 2, [3, 8], 4]}
console.log(obj3); // obj3 { name: 'James', arr[1, 2, [3, 8], 4]}
// javascript的引用數(shù)據(jù)類(lèi)型是保存在堆內(nèi)存中的對(duì)象佃迄。
// 與其他語(yǔ)言的不同是泼差,你不可以直接訪問(wèn)堆內(nèi)存空間中的位置和操作堆內(nèi)存空間。只能操作對(duì)象在棧內(nèi)存中的引用地址呵俏。
// 所以堆缘,引用類(lèi)型數(shù)據(jù)在棧內(nèi)存中保存的實(shí)際上是對(duì)象在堆內(nèi)存中的引用地址。
// 通過(guò)這個(gè)引用地址可以快速查找到保存中堆內(nèi)存中的對(duì)象普碎。
// obj1賦值給onj3吼肥,實(shí)際上這個(gè)堆內(nèi)存對(duì)象在棧內(nèi)存的引用地址復(fù)制了一份給了obj2
// 但是實(shí)際上他們共同指向了同一個(gè)堆內(nèi)存對(duì)象。實(shí)際上改變的是堆內(nèi)存對(duì)象随常。
// 深拷貝
let obj1 = {
name: 'cobe',
arr: [1, 2, [3, 5], 4]
}
let obj4=deepClone(obj1)
obj4.name = "Allan";
obj4.arr[2] = [5,6,7] ; // 新對(duì)象跟原對(duì)象不共享內(nèi)存
// 這是個(gè)深拷貝的方法
function deepClone(obj) {
if (obj === null) return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (typeof obj !== "object") return obj;
let cloneObj = new obj.constructor();
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 實(shí)現(xiàn)一個(gè)遞歸拷貝
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}
console.log(obj1); // obj1 { name: 'cobe', arr[1, 2, [3, 5], 4]}
console.log(obj4); // obj4 { name: 'Allan', arr[1, 2, [5, 6, 7], 4]}
淺拷貝的方式
1. Object.assign()
Object.assign() 方法可以把任意多個(gè)的源對(duì)象自身的可枚舉屬性拷貝給目標(biāo)對(duì)象潜沦,然后返回目標(biāo)對(duì)象。
let obj1 = { person: { name: "kobe", age: 41 }, sports: 'basketball' }
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
2. 函數(shù)庫(kù)lodash的_.clone方法
該函數(shù)庫(kù)也有提供_.clone用來(lái)做 Shallow Copy,后面我們會(huì)再介紹利用這個(gè)庫(kù)實(shí)現(xiàn)深拷貝绪氛。
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true
3. 展開(kāi)運(yùn)算符...
展開(kāi)運(yùn)算符是一個(gè) es6 特性,它提供了一種非常方便的方式來(lái)執(zhí)行淺拷貝涝影,這與 Object.assign ()
的功能相同枣察。
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
4. Array.prototype.concat()
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
5. Array.prototype.slice()
let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]
深拷貝的方式
1. JSON.parse(JSON.stringify())
let arr = [1, 3, {
username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr) // [1, 3, { username: 'kobe' }]
console.log(arr4) // [1, 3, { username: 'duncan' }]
這也是利用JSON.stringify
將對(duì)象轉(zhuǎn)成JSON字符串,再用JSON.parse
把字符串解析成對(duì)象燃逻,一去一來(lái)序目,新的對(duì)象產(chǎn)生了,而且對(duì)象會(huì)開(kāi)辟新的棧伯襟,實(shí)現(xiàn)深拷貝猿涨。
這種方法雖然可以實(shí)現(xiàn)數(shù)組或?qū)ο笊羁截?但不能處理函數(shù)和正則,因?yàn)檫@兩者基于JSON.stringify
和JSON.parse
處理后姆怪,得到的正則就不再是正則(變?yōu)榭諏?duì)象)叛赚,得到的函數(shù)就不再是函數(shù)(變?yōu)閚ull)了。
2. 函數(shù)庫(kù)lodash的_.cloneDeep方法
該函數(shù)庫(kù)也有提供_.cloneDeep用來(lái)做 Deep Copy
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
3. jQuery.extend()方法
jquery 有提供一個(gè)$.extend可以用來(lái)做 Deep Copy
$.extend(deepCopy, target, object1, [objectN])
//第一個(gè)參數(shù)為true,就是深拷貝
var $ = require('jquery');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
4. 遞歸
遞歸方法實(shí)現(xiàn)深度克隆原理:遍歷對(duì)象稽揭、數(shù)組直到里邊都是基本數(shù)據(jù)類(lèi)型俺附,然后再去復(fù)制,就是深度拷貝溪掀。
有種特殊情況需注意就是對(duì)象存在循環(huán)引用的情況事镣,即對(duì)象的屬性直接的引用了自身的情況,解決循環(huán)引用問(wèn)題揪胃,我們可以額外開(kāi)辟一個(gè)存儲(chǔ)空間璃哟,來(lái)存儲(chǔ)當(dāng)前對(duì)象和拷貝對(duì)象的對(duì)應(yīng)關(guān)系,當(dāng)需要拷貝當(dāng)前對(duì)象時(shí)喊递,先去存儲(chǔ)空間中找随闪,有沒(méi)有拷貝過(guò)這個(gè)對(duì)象,如果有的話直接返回册舞,如果沒(méi)有的話繼續(xù)拷貝蕴掏,這樣就巧妙化解的循環(huán)引用的問(wèn)題。
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不進(jìn)行拷貝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是對(duì)象或者普通的值 如果是函數(shù)的話是不需要深拷貝
if (typeof obj !== "object") return obj;
// 是對(duì)象的話就要進(jìn)行深拷貝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所屬類(lèi)原型上的constructor,而原型上的 constructor指向的是當(dāng)前類(lèi)本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 實(shí)現(xiàn)一個(gè)遞歸拷貝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 對(duì)象存在循環(huán)引用的情況
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);