【JS】深拷貝 vs 淺拷貝


本文思維導(dǎo)圖如下:


本文思維導(dǎo)圖

本文首發(fā)于我的個(gè)人網(wǎng)站: http://cherryblog.site/
本文作者: Cherry
本文鏈接: http://cherryblog.site/deepcopy.html
版權(quán)聲明: 本博客所有文章除特別聲明外,均采用 CC BY-NC-SA 3.0 許可協(xié)議贰健。轉(zhuǎn)載請(qǐng)注明出處!!

前言

最近在讀 zepto 的源碼汹桦,深有感觸昨悼,感覺(jué)隨便一段代碼都可以延伸出一大堆的知識(shí)點(diǎn)四濒,在看到深拷貝和淺拷貝的時(shí)候,之前只是了解過(guò)什么是深拷貝什么是淺拷貝琅摩,并沒(méi)有對(duì)齊實(shí)現(xiàn)進(jìn)行探索,所以本文主要講一下什么是深拷貝锭硼、什么是淺拷貝房资、深拷貝與淺拷貝的區(qū)別,以及怎么進(jìn)行深拷貝和怎么進(jìn)行淺拷貝檀头。

堆和棧的區(qū)別

其實(shí)深拷貝和淺拷貝的主要區(qū)別就是其在內(nèi)存中的存儲(chǔ)類型不同轰异。

堆和棧都是內(nèi)存中劃分出來(lái)用來(lái)存儲(chǔ)的區(qū)域。

棧(stack)為自動(dòng)分配的內(nèi)存空間暑始,它由系統(tǒng)自動(dòng)釋放搭独;而堆(heap)則是動(dòng)態(tài)分配的內(nèi)存,大小不定也不會(huì)自動(dòng)釋放廊镜。

ECMAScript 的數(shù)據(jù)類型

在將深拷貝和淺拷貝之前牙肝,我們先來(lái)重新回顧一下 ECMAScript 中的數(shù)據(jù)類型。主要分為

基本數(shù)據(jù)類型(undefined嗤朴,boolean配椭,number,string雹姊,null

基本數(shù)據(jù)類型主要是:undefined股缸,boolean,number吱雏,string敦姻,null瘾境。

基本數(shù)據(jù)類型存放在棧中

存放在棧內(nèi)存中的簡(jiǎn)單數(shù)據(jù)段,數(shù)據(jù)大小確定替劈,內(nèi)存空間大小可以分配寄雀,是直接按值存放的,所以可以直接訪問(wèn)陨献。

基本數(shù)據(jù)類型值不可變

javascript中的原始值(undefined盒犹、null、布爾值眨业、數(shù)字和字符串)與對(duì)象(包括數(shù)組和函數(shù))有著根本區(qū)別急膀。原始值是不可更改的:任何方法都無(wú)法更改(或“突變”)一個(gè)原始值。對(duì)數(shù)字和布爾值來(lái)說(shuō)顯然如此 —— 改變數(shù)字的值本身就說(shuō)不通龄捡,而對(duì)字符串來(lái)說(shuō)就不那么明顯了卓嫂,因?yàn)樽址雌饋?lái)像由字符組成的數(shù)組,我們期望可以通過(guò)指定索引來(lái)假改字符串中的字符聘殖。實(shí)際上晨雳,javascript 是禁止這樣做的。字符串中所有的方法看上去返回了一個(gè)修改后的字符串奸腺,實(shí)際上返回的是一個(gè)新的字符串值餐禁。

基本數(shù)據(jù)類型的值是不可變的,動(dòng)態(tài)修改了基本數(shù)據(jù)類型的值突照,它的原始值也是不會(huì)改變的帮非,例如:

    var str = "abc";

    console.log(str[1]="f");    // f

    console.log(str);           // abc

這一點(diǎn)其實(shí)開(kāi)始我是比較迷惑的,總是感覺(jué) js 是一個(gè)靈活的語(yǔ)言讹蘑,任何值應(yīng)該都是可變的末盔,真是圖樣圖森破,我們通常情況下都是對(duì)一個(gè)變量重新賦值座慰,而不是改變基本數(shù)據(jù)類型的值陨舱。就如上述引用所說(shuō)的那樣,在 js 中沒(méi)有方法是可以改變布爾值和數(shù)字的角骤。倒是有很多操作字符串的方法隅忿,但是這些方法都是返回一個(gè)新的字符串,并沒(méi)有改變其原有的數(shù)據(jù)邦尊。

所以背桐,記住這一點(diǎn):基本數(shù)據(jù)類型值不可變

基本類型的比較是值的比較

基本類型的比較是值的比較蝉揍,只要它們的值相等就認(rèn)為他們是相等的链峭,例如:

    var a = 1;
    var b = 1;
    console.log(a === b);//true

比較的時(shí)候最好使用嚴(yán)格等,因?yàn)?== 是會(huì)進(jìn)行類型轉(zhuǎn)換的又沾,比如:

    var a = 1;
    var b = true;
    console.log(a == b);//true

引用類型

基本數(shù)據(jù)類型存放在堆中

引用類型(object)是存放在堆內(nèi)存中的弊仪,變量實(shí)際上是一個(gè)存放在棧內(nèi)存的指針熙卡,這個(gè)指針指向堆內(nèi)存中的地址。每個(gè)空間大小不一樣励饵,要根據(jù)情況開(kāi)進(jìn)行特定的分配驳癌,例如。

ar person1 = {name:'jozo'};
var person2 = {name:'xiaom'};
var person3 = {name:'xiaoq'};
堆內(nèi)存

引用類型值可變

引用類型是可以直接改變其值的役听,例如:

    var a = [1,2,3];
    a[1] = 5;
    console.log(a[1]); // 5

引用類型的比較是引用的比較

所以每次我們對(duì) js 中的引用類型進(jìn)行操作的時(shí)候颓鲜,都是操作其對(duì)象的引用(保存在棧內(nèi)存中的指針),所以比較兩個(gè)引用類型典予,是看其的引用是否指向同一個(gè)對(duì)象甜滨。例如:


    var a = [1,2,3];
    var b = [1,2,3];
    console.log(a === b); // false

雖然變量 a 和變量 b 都是表示一個(gè)內(nèi)容為 1,2瘤袖,3 的數(shù)組衣摩,但是其在內(nèi)存中的位置不一樣,也就是說(shuō)變量 a 和變量 b 指向的不是同一個(gè)對(duì)象捂敌,所以他們是不相等的艾扮。

引用類型在內(nèi)存中的存儲(chǔ)

(懶癌晚期,不想自己畫(huà)圖了占婉,直接盜圖)

傳值與傳址

了解了基本數(shù)據(jù)類型與引用類型的區(qū)別之后栏渺,我們就應(yīng)該能明白傳值與傳址的區(qū)別了。
在我們進(jìn)行賦值操作的時(shí)候锐涯,基本數(shù)據(jù)類型的賦值(=)是在內(nèi)存中新開(kāi)辟一段棧內(nèi)存,然后再把再將值賦值到新的棧中填物。例如:

var a = 10;
var b = a;

a ++ ;
console.log(a); // 11
console.log(b); // 10
基本數(shù)據(jù)類型的賦值

所以說(shuō)纹腌,基本類型的賦值的兩個(gè)變量是兩個(gè)獨(dú)立相互不影響的變量。

但是引用類型的賦值是傳址滞磺。只是改變指針的指向升薯,例如,也就是說(shuō)引用類型的賦值是對(duì)象保存在棧中的地址的賦值击困,這樣的話兩個(gè)變量就指向同一個(gè)對(duì)象涎劈,因此兩者之間操作互相有影響。例如:

var a = {}; // a保存了一個(gè)空對(duì)象的實(shí)例
var b = a;  // a和b都指向了這個(gè)空對(duì)象

a.name = 'jozo';
console.log(a.name); // 'jozo'
console.log(b.name); // 'jozo'

b.age = 22;
console.log(b.age);// 22
console.log(a.age);// 22

console.log(a == b);// true
引用類型的賦值

淺拷貝

在深入了解之前阅茶,我認(rèn)為上面的賦值就是淺拷貝蛛枚,哇哈哈,真的是圖樣圖森破脸哀。上面那個(gè)應(yīng)該只能算是“引用”蹦浦,并不算是真正的淺拷貝。
一下部分參照知乎中的提問(wèn): javascript中的深拷貝和淺拷貝

賦值(=)和淺拷貝的區(qū)別

那么賦值和淺拷貝有什么區(qū)別呢撞蜂,我們看下面這個(gè)例子:

    var obj1 = {
        'name' : 'zhangsan',
        'age' :  '18',
        'language' : [1,[2,3],[4,5]],
    };

    var obj2 = obj1;


    var obj3 = shallowCopy(obj1);
    function shallowCopy(src) {
        var dst = {};
        for (var prop in src) {
            if (src.hasOwnProperty(prop)) {
                dst[prop] = src[prop];
            }
        }
        return dst;
    }

    obj2.name = "lisi";
    obj3.age = "20";

    obj2.language[1] = ["二","三"];
    obj3.language[2] = ["四","五"];

    console.log(obj1);  
    //obj1 = {
    //    'name' : 'lisi',
    //    'age' :  '18',
    //    'language' : [1,[4,5]],
    //};

    console.log(obj2);
    //obj2 = {
    //    'name' : 'lisi',
    //    'age' :  '18',
    //    'language' : [1,[4,5]],
    //};

    console.log(obj3);
    //obj3 = {
    //    'name' : 'zhangsan',
    //    'age' :  '20',
    //    'language' : [1,[4,5]],
    //};

先定義個(gè)一個(gè)原始的對(duì)象 obj1盲镶,然后使用賦值得到第二個(gè)對(duì)象 obj2侥袜,然后通過(guò)淺拷貝,將 obj1 里面的屬性都賦值到 obj3 中溉贿。也就是說(shuō):

  • obj1:原始數(shù)據(jù)
  • obj2:賦值操作得到
  • obj3:淺拷貝得到

然后我們改變 obj2name 屬性和 obj3name 屬性枫吧,可以看到,改變賦值得到的對(duì)象 obj2 同時(shí)也會(huì)改變?cè)贾?obj1宇色,而改變淺拷貝得到的的 obj3 則不會(huì)改變?cè)紝?duì)象 obj1九杂。這就可以說(shuō)明賦值得到的對(duì)象 obj2 只是將指針改變,其引用的仍然是同一個(gè)對(duì)象代兵,而淺拷貝得到的的 obj3 則是重新創(chuàng)建了新對(duì)象尼酿。

然而,我們接下來(lái)來(lái)看一下改變引用類型會(huì)是什么情況呢植影,我又改變了賦值得到的對(duì)象 obj2 和淺拷貝得到的 obj3 中的 language 屬性的第二個(gè)值和第三個(gè)值(language 是一個(gè)數(shù)組裳擎,也就是引用類型)宪萄。結(jié)果見(jiàn)輸出谒亦,可以看出來(lái),無(wú)論是修改賦值得到的對(duì)象 obj2 和淺拷貝得到的 obj3 都會(huì)改變?cè)紨?shù)據(jù)傻唾。

這是因?yàn)闇\拷貝只復(fù)制一層對(duì)象的屬性谷饿,并不包括對(duì)象里面的為引用類型的數(shù)據(jù)惶我。所以就會(huì)出現(xiàn)改變淺拷貝得到的 obj3 中的引用類型時(shí),會(huì)使原始數(shù)據(jù)得到改變博投。

深拷貝:將 B 對(duì)象拷貝到 A 對(duì)象中绸贡,包括 B 里面的子對(duì)象,

淺拷貝:將 B 對(duì)象拷貝到 A 對(duì)象中毅哗,但不包括 B 里面的子對(duì)象

-- 和原數(shù)據(jù)是否指向同一對(duì)象 第一層數(shù)據(jù)為基本數(shù)據(jù)類型 原數(shù)據(jù)中包含子對(duì)象
賦值 改變會(huì)使原數(shù)據(jù)一同改變 改變會(huì)使原數(shù)據(jù)一同改變
淺拷貝 改變會(huì)使原數(shù)據(jù)一同改變 改變會(huì)使原數(shù)據(jù)一同改變
深拷貝 改變會(huì)使原數(shù)據(jù)一同改變 改變會(huì)使原數(shù)據(jù)一同改變

深拷貝

看了這么半天听怕,你也應(yīng)該清楚什么是深拷貝了吧,如果還不清楚虑绵,我就剖腹自盡(?_?)

深拷貝是對(duì)對(duì)象以及對(duì)象的所有子對(duì)象進(jìn)行拷貝尿瞭。

那么問(wèn)題來(lái)了,怎么進(jìn)行深拷貝呢翅睛?

思路就是遞歸調(diào)用剛剛的淺拷貝声搁,把所有屬于對(duì)象的屬性類型都遍歷賦給另一個(gè)對(duì)象即可。我們直接來(lái)看一下 Zepto 中深拷貝的代碼:

    // 內(nèi)部方法:用戶合并一個(gè)或多個(gè)對(duì)象到第一個(gè)對(duì)象
    // 參數(shù):
    // target 目標(biāo)對(duì)象  對(duì)象都合并到target里
    // source 合并對(duì)象
    // deep 是否執(zhí)行深度合并
    function extend(target, source, deep) {
        for (key in source)
            if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
                // source[key] 是對(duì)象捕发,而 target[key] 不是對(duì)象疏旨, 則 target[key] = {} 初始化一下,否則遞歸會(huì)出錯(cuò)的
                if (isPlainObject(source[key]) && !isPlainObject(target[key]))
                    target[key] = {}

                // source[key] 是數(shù)組爬骤,而 target[key] 不是數(shù)組充石,則 target[key] = [] 初始化一下,否則遞歸會(huì)出錯(cuò)的
                if (isArray(source[key]) && !isArray(target[key]))
                    target[key] = []
                // 執(zhí)行遞歸
                extend(target[key], source[key], deep)
            }
            // 不滿足以上條件,說(shuō)明 source[key] 是一般的值類型骤铃,直接賦值給 target 就是了
            else if (source[key] !== undefined) target[key] = source[key]
    }

    // Copy all but undefined properties from one or more
    // objects to the `target` object.
    $.extend = function(target){
        var deep, args = slice.call(arguments, 1);

        //第一個(gè)參數(shù)為boolean值時(shí)拉岁,表示是否深度合并
        if (typeof target == 'boolean') {
            deep = target;
            //target取第二個(gè)參數(shù)
            target = args.shift()
        }
        // 遍歷后面的參數(shù),都合并到target上
        args.forEach(function(arg){ extend(target, arg, deep) })
        return target
    }

在 Zepto 中的 $.extend 方法判斷的第一個(gè)參數(shù)傳入的是一個(gè)布爾值惰爬,判斷是否進(jìn)行深拷貝喊暖。

$.extend 方法內(nèi)部,只有一個(gè)形參 target撕瞧,這個(gè)設(shè)計(jì)你真的很巧妙陵叽。
因?yàn)樾螀⒅挥幸粋€(gè),所以 target 就是傳入的第一個(gè)參數(shù)的值丛版,并在函數(shù)內(nèi)部設(shè)置一個(gè)變量 args 來(lái)接收去除第一個(gè)參數(shù)的其余參數(shù)巩掺,如果該值是一個(gè)布爾類型的值的話,說(shuō)明要啟用深拷貝页畦,就將 deep 設(shè)置為 true胖替,并將 target 賦值為 args 的第一個(gè)值(也就是真正的 target)。如果該值不是一個(gè)布爾類型的話豫缨,那么傳入的第一個(gè)值仍為 target 不需要進(jìn)行處理独令,只需要遍歷使用 extend 方法就可以。

這里有點(diǎn)繞好芭,但是真的設(shè)計(jì)的很精妙燃箭,建議自己打斷點(diǎn)試一下,會(huì)有意外收獲(玩轉(zhuǎn) js 的大神請(qǐng)忽略)舍败。

而在 extend 的內(nèi)部招狸,是拷貝的過(guò)程。

參考文章:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瓢颅,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子弛说,更是在濱河造成了極大的恐慌,老刑警劉巖翰意,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件木人,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡冀偶,警方通過(guò)查閱死者的電腦和手機(jī)醒第,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)进鸠,“玉大人稠曼,你說(shuō)我怎么就攤上這事】湍辏” “怎么了霞幅?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵漠吻,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我司恳,道長(zhǎng)途乃,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任扔傅,我火速辦了婚禮耍共,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘猎塞。我一直安慰自己试读,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布荠耽。 她就那樣靜靜地躺著钩骇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪骇塘。 梳的紋絲不亂的頭發(fā)上伊履,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音款违,去河邊找鬼唐瀑。 笑死,一個(gè)胖子當(dāng)著我的面吹牛插爹,可吹牛的內(nèi)容都是我干的哄辣。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼赠尾,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼力穗!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起气嫁,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤当窗,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后寸宵,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體崖面,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年梯影,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了巫员。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡甲棍,死狀恐怖简识,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤七扰,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布奢赂,位于F島的核電站,受9級(jí)特大地震影響戳寸,放射性物質(zhì)發(fā)生泄漏呈驶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一疫鹊、第九天 我趴在偏房一處隱蔽的房頂上張望袖瞻。 院中可真熱鬧,春花似錦拆吆、人聲如沸聋迎。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)霉晕。三九已至,卻和暖如春捞奕,著一層夾襖步出監(jiān)牢的瞬間牺堰,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工颅围, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留伟葫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓院促,卻偏偏與公主長(zhǎng)得像筏养,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子常拓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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

  • js底層數(shù)據(jù)類型 堆和棧的區(qū)別 其實(shí)深拷貝和淺拷貝的主要區(qū)別就是其在內(nèi)存中的存儲(chǔ)類型不同渐溶。 堆和棧都是內(nèi)存中劃分出...
    sponing閱讀 617評(píng)論 0 3
  • 內(nèi)存管理 簡(jiǎn)述OC中內(nèi)存管理機(jī)制。與retain配對(duì)使用的方法是dealloc還是release弄抬,為什么茎辐?需要與a...
    丶逐漸閱讀 1,965評(píng)論 1 16
  • 值類型與引用類型 談淺拷貝與深拷貝之前,我們需要先理清一個(gè)概念掂恕,即值類型與引用類型荔茬。 什么是值類型與引用類型?這要...
    franose閱讀 617評(píng)論 1 8
  • *面試心聲:其實(shí)這些題本人都沒(méi)怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來(lái)就是把...
    Dove_iOS閱讀 27,146評(píng)論 30 470
  • 在 JS 中有一些基本類型像是Number竹海、String、Boolean丐黄,而對(duì)象就是像這樣的東西{ name: '...
    tobAlier閱讀 564評(píng)論 0 0