深拷贝VS浅拷贝

深拷贝VS浅拷贝

本文主要对深拷贝&浅拷贝的解释及实现做一下简单记录。

之所以会有深拷贝与浅拷贝之分,是因为不同数据类型的数据在内存中的存储区域不一样。

堆和栈是计算机中划分出来用来存储的区域,其中堆(heap)则是动态分配的内存,大小不定也不会自动释放;而栈(stack)为自动分配的内存空间,它由系统自动释放。存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接访问。

众所周知,JavaScript中的数据分为(基本类型和引用类型)。五种基本类型(boolean,number,undefined,null,string,)的数据的原始值是大小确定不可变的,所以放在栈内存中;而引用类型(object)是放在堆内存中的,对应赋值的变量只是一个存放在栈内存中的指针,也就是一个指向引用类型存放在堆内存中的地址。

image-20180811123105339

引用类型比较与基本类型比较的区别

引用类型是引用的比较,例如

// a与b是两个引用类型,但其指针指向的是两个不同的引用
let a = [1,2,3];
let b = [1,2,3];
a===b; //false

// 引用类型的赋值操作,b的指针也是指向与a同样的引用
let a = [1,2,3];
let b = a;
a===b; //true

而基本类型间的比较是值的比较,例如

let a = 1;
let b = 1;
a===b; //true

let a = 1;
let b = a;
a===b; //true

赋值操作:传值与传址的区别

在对基本类型进行赋值操作的时候实际是传值,即在栈内存中新开一个区域给新的变量,然后再把值赋给它。所以基本类型赋值的变量是两个互相独立变量。

let a = 10;
let b = a;
b++;
console.log(a); //10
console.log(b); //11
image-20180811140720986

而引用类型的赋值操作是传址,只是在栈内存中新增了一个变量,同时赋值给这个变量的只是保存在堆内存中对象的地址,也就是指针的指向。因此这两个变量的指针指向同一个地址,相互操作也就会有影响。

let a = {};
let b = a;

a.name = 'slevin';
console.log(b.name); //'slevin'
console.log(a.name); //'slevin'
console.log(a===b); //true
引用类型赋值操作

需要注意的是,引用类型的赋值并不算是浅拷贝,因为赋值操作只相当于是引用,两个变量还是指向同一对象,任何一方改变了都会影响到另一方;但浅拷贝出来的变量与源变量已经不同,在不包含子对象的情况下(此情况即为深拷贝),一方改变不会影响到另一方。如下赋值操作:

let a = {};
let b = a;

b.name = 'slevin';
console.log(a.name); //'slevin';对b操作影响到了a
console.log(a===b); //true;因为两者指向同一个对象

浅拷贝实现方法

  1. 自定义实现方法:

    // 浅拷贝的方法
    function shallowCopy(srcObj) {
      let copy = {};
      for (let key in srcObj) {
          //只拷贝自身的属性,__proto__上继承来的属性不做拷贝,也可去掉这个限制
          if (srcObj.hasOwnProperty(key)) {
              copy[key] = srcObj[key];
          }
      }
      return copy;
    }
    
    let obj = {
      name: 'slevin',
      age: 18,
      language: {
          'english': 'good',
          'mandarin': 'wonderful',
      }
    }
    
    let copy = shallowCopy(obj);
    copy.age = 28;
    
    console.log(obj.age); // 18; 并没有改变源对象
    console.log(obj === copy); //false;两者指向不是同一个对象
    
  2. 利用Object.assign(target,...sources)可将一个或多个源对象上可枚举属性的值复制到目标对象,并返回目标对象。但注意,只能做一层属性的浅拷贝

    let obj = {
      name: 'slevin',
      age: 18,
      language: {
          english: 'good',
          mandarin: 'wonderful',
          test: [1, 2, 3]
      },
      fn:function(){
          console.log('this:',this.name);
      }
    }
    
    let copy = Object.assign({},obj);
    //不会改变源对象
    copy.age = 22;
    //会改变源对象
    copy.language.english = 'bad';
    console.log('copy===obj:',copy===obj);//false
    console.log('copy:',copy);
    console.log('obj:',obj);
    
  3. 对于数组来说,也可以使用Array.prototype.slice()方法和Array.prototype.concact()方法

    let obj = [1,2,['a','b','c']]
    let copy = obj.slice();
    // 不会改变源对象
    copy[0]=111;
    // 会改变源对象
    copy[2][0]='aaa';
    console.log('copy===obj:',copy===obj);//false
    console.log('copy:',copy);
    console.log('obj:',obj);
    

深拷贝及实现方法

简单来说,深拷贝就是把对象以及对象的子对象进行拷贝。因为浅拷贝只复制了一层对象的属性,而如果对象的数值也为引用类型,那么拷贝过来依然是个引用地址,在拷贝对象上对子对象的值进行操作会改变原始数据中的值。

例如上面obj浅拷贝得到copy对象,如果在copy对象上改变子对象copy.language上的属性值,会影响到源对象obj

copy.language.english = 'bad';
copy.language.mandarin = 'bad';
console.log(obj.language); // {'english': 'bad','mandarin': 'bad',}

那么如何深拷贝呢?思路就是递归调用刚刚的浅拷贝,遍历所有值为引用类型的属性,然后依次赋值给另外一个对象即可。

  1. 方法一,自定义实现:

    /**将源对象source深度合并到目标对象target上
    * source 源对象
    * target 目标对象,默认{}
    * deep 是否深度合并,默认true
    */
    function deepCopy (source,target={},deep=true) {
      for (let key in source){
          // 深拷贝,而且只拷贝自身属性.(默认传入的source为对象)
          if (deep && source.hasOwnProperty(key)){
              if (typeof(source[key])==='object'){
                  // // source[key] 是对象,而 target[key] 不是对象, 则 target[key] = {} 初始化一下,否则递归会出错的
                  // if (!(target[key] instanceof Object) && (source[key] instanceof Object)){
                  // target[key] = {};
                  // }
                  // // source[key] 是数组,而 target[key] 不是数组,则 target[key] = [] 初始化一下,否则递归会出错的
                  // if (!Array.isArray(target[key]) && Array.isArray(source[key])) {
                  // target[key] = [];
                  // }
                  // 上面的代码可以简化为以下:
                  target[key] = Array.isArray(source[key]) ? [] : {};
    
                  // 递归执行拷贝
                  deepCopy(source[key],target[key],true);
              } else {
                  target[key] = source[key];
              }
          }
      }
      return target;
    }
    
    
    let obj = {
      name: 'slevin',
      age: 18,
      language: {
          english: 'good',
          mandarin: 'wonderful',
          test:[1,2,3]
      }
    }
    let copy = deepCopy(obj);
    copy.language.test[0] = 111;
    
    console.log('copy:',copy); //111 改变目标对象的子对象属性值
    console.log('obj:',obj); //1 对应源对象上没有改变
    
  2. 利用JSON.parse(JSON.stringify(copyObj))方法

    let obj = {
      name: 'slevin',
      age: 18,
      language: {
          english: 'good',
          mandarin: 'wonderful',
          test: [1, 2, 3]
      }
    }
    let copy = JSON.parse(JSON.stringify(obj))
    copy.language.test[0]=111;
    
    console.log('copy===obj:',copy===obj);//false
    console.log('copy.language.test[0]:',copy.language.test[0]);//111
    console.log('obj.language.test[0]:',obj.language.test[0]);//1
    

    但要注意,此方法有两个缺点

    • 如果源对象属性值有函数,无法拷贝下来
    • 无法拷贝源对象原型链上的属性和方法
    let obj = {
      name: 'slevin',
      age: 18,
      language: {
          english: 'good',
          mandarin: 'wonderful',
          test: [1, 2, 3]
      },
      fn:function(){
          console.log('this:',this.name);
      }
    }
    let copy = JSON.parse(JSON.stringify(obj));
    console.log('copy===obj:',copy===obj);//false
    console.log('obj.fn:',obj.fn); //fn(){}...
    console.log('copy.fn:',copy.fn); //undefined
    

最后,再补一张引用类型的赋值操作、浅拷贝深拷贝对比图,加深印象

引用类型的赋值操作、浅拷贝深拷贝对比

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容