深拷貝和淺拷貝

前言

學(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ì)象)。

微信圖片_20211106163008.png

賦值和深/淺拷貝的區(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.stringifyJSON.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);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市盛杰,隨后出現(xiàn)的幾起案子挽荡,更是在濱河造成了極大的恐慌,老刑警劉巖即供,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件定拟,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡逗嫡,警方通過(guò)查閱死者的電腦和手機(jī)青自,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)驱证,“玉大人延窜,你說(shuō)我怎么就攤上這事∧ǔ” “怎么了逆瑞?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)伙单。 經(jīng)常有香客問(wèn)我获高,道長(zhǎng),這世上最難降的妖魔是什么吻育? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任念秧,我火速辦了婚禮,結(jié)果婚禮上布疼,老公的妹妹穿的比我還像新娘摊趾。我一直安慰自己,他們只是感情好缎除,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布严就。 她就那樣靜靜地躺著,像睡著了一般器罐。 火紅的嫁衣襯著肌膚如雪梢为。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天轰坊,我揣著相機(jī)與錄音铸董,去河邊找鬼。 笑死肴沫,一個(gè)胖子當(dāng)著我的面吹牛粟害,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播颤芬,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼悲幅,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼套鹅!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起汰具,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤卓鹿,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后留荔,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體吟孙,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年聚蝶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了杰妓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡碘勉,死狀恐怖巷挥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情恰聘,我是刑警寧澤句各,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站晴叨,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏矾屯。R本人自食惡果不足惜兼蕊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望件蚕。 院中可真熱鬧孙技,春花似錦、人聲如沸排作。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)妄痪。三九已至哈雏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間衫生,已是汗流浹背裳瘪。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留罪针,地道東北人彭羹。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像泪酱,于是被迫代替她去往敵國(guó)和親派殷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子还最,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容