JavaScript 浅拷贝与深拷贝

就得先从基本数据类型和引用数据类型开始了。

基本数据类型与引用数据类型

基本数据类型

string、number、boolean、undefined、null、symbol

也叫值类型,按值存储。存储变量时直接在栈内存中存储值本身

传递方式:按值传递。

var a = 10;
var b = a;
b = 20;

console.log(a);  // 10
console.log(b);  // 20
image-20200827121448248.png

简单数据类型传参只是把传递了,值与值之间独立存在。在上述例子中,

1)声明变量a则会在栈内存中开辟一个空间,存放a的值10。

2)var b = a,声明变量b则在栈内存中开辟一个新的空间,并把a的值10存放到这个空间。

3)b = 20,找到b在栈内存中对应的空间,把里面存储的值10改成20。

引用数据类型

通过new关键字创建的对象(系统对象、自定义对象),如Object、Array、Date、Function等

按地址存储。存储变量时存储的仅仅是地址(引用)

栈内存中变量保存的是一个指针,指向对应在堆内存中的地址。当访问引用类型的时候,要先从栈中取出该对象的地址指针,然后再从堆内存中取得所需的数据。

传递方式:按引用传递。因为指向的是同一个地址,所以当地址中的数据发生改变,指向该存放地址的所有变量都会发生改变。

function fn(xx){
    console.log(xx.name);
    xx.name = "Han Meimei";
    console.log(xx.name);
}

let p = {
   name: 'Li Ming'
}
console.log(p.name)    // Li Ming
fn(p);   // Li Ming    //Han Meimei
console.log(p.name)    // Han Meimei

1)声明复杂对象p,在栈中开辟一个空间存放p的地址,地址指向堆中的真实数据:

image-20200827155442860.png

2)fn(p), 传参即为xx=p。由于p存的是地址,因此xx=p是把p的地址传给xx,xx和p则指向了同一个地址:

image-20200827155518216.png

3)xx.name = "Han Meimei",改变的是xx的地址所指向的堆内存中存放的数据,因此也同时改变了p:

image-20200827155609265.png

浅拷贝和深拷贝的区别

  • 浅拷贝

    • 浅拷贝只拷贝一层。如果属性是基本类型,拷贝的就是基本类型的值;如果一个键的值是复合类型的值(数组、对象、函数),拷贝的则是这个值的引用(地址),而不是这个值的副本,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

    • 只是将数据中存放的引用拷贝下来,依旧指向同一个存放地址。

  • 深拷贝:

    • 深拷贝是将一个对象从内存中完整的拷贝一份出来, 从堆内存中开辟一个新的区域存放新对象, 且修改新对象不会影响原对象

    • 将数据中所有的数据拷贝下来,而不是引用,若对拷贝下来的数据进行修改,并不会影响原数据。

浅拷贝

使用下面这些函数得到的都是浅拷贝:

  • Object.assign
  • 使用扩展运算符实现的复制
  • Array.prototype.slice(), Array.prototype.concat()
// 需求: 浅拷贝一份 obj 到对象 targetObj
const obj = {
    id:001,
    name:'andy',
    msg:{
        age:18
    }
}

// 需求:浅拷贝一份 arr 到对象 targetArr
let arr = [1, 2, { name:'qq' }]

1:Object.assign(对象)

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

const targetObj = {}

Object.assign(targetObj, obj)
console.log(targetObj);  // {id: 1, name: "andy", msg: {age: 18}}

2:使用扩展运算符(对象)

扩展运算符(...)用于取出参数对象的所有可遍历属性,并将其复制到当前对象之中。

const targetObj = { ...obj };

// 或
const { ...targetObj } = obj;  //解构赋值
console.log(targetObj);  // {id: 1, name: "andy", msg: {age: 18}}

3:for...in(对象)

const targetObj = {}

for (const key in obj) {
     targetObj[key] = obj[key]   // 主要是因为这一步,对于复杂类型,存放的都是指针,赋值的时候得到的也是指针
}
console.log(targetObj); // {id: 1, name: "andy", msg: {age: 18}}

因为浅拷贝的是引用(地址),因此对于复合类型的值(数组、对象、函数),原本和副本都指向同一地址,改变一个会影响另一个。如下代码中,对于基本数据类型,改变targetObj.name, 并不影响源对象,而属性msg是个对象,拷贝的是这个对象的引用(地址),因此targetObj.msg.age = 8 也同时改变了obj.msg.age

targetObj.name = 'mike';
targetObj.msg.age = 8;

console.log(targetObj);  // {id: 1, name: "mike", msg: {age: 8}}
console.log(obj);       // {id: 1, name: "andy", msg: {age: 8}}

4:使用扩展运算符(数组)

let targetArr = [...arr]

5:Array.prototype.slice()

let targetArr = arr.slice(0)

6:Array.prototype.concat()

let targetArr = arr.concat()

同样地,对于数组的每一项拷贝的都是引用,对于值类型,原本和副本互相独立,而对于复杂类型,改变一个会影响另一个。

targetArr[0] = 88;
targetArr[2].name = 'test';
console.log(arr);        // [1, 2, { name:'test' }]
console.log(targetArr);  // [88, 2, { name:'test' }]

深拷贝

深拷贝的实现

  • JSON.parse(JSON.stringify())
  • 手写递归函数
  • 函数库lodash

实现1:使用JSON.parse和JSON.stringify

const targerObj = JSON.parse(JSON.stringify(obj))

// JSON.stringify()首先把obj转化成字符串的形式,字符串在存储的时候会单独开辟空间,再通过JSON.parse()转回成对象,这个过程就断掉了和原始值之间的关系

JSON.parse(JSON.stringify())存在以下问题:

  • 无法解决循环引用问题
  • 无法拷贝特殊的对象,比如:RegExp, Date, Set, Map等在序列化的时候会丢失。
  • 无法拷贝函数

这是因为利用JSON.stringify( )序列化时,所有函数及原型成员都会被有意忽略,不体现在结果中。此外,值为undefined的任何属性也都会被跳过。得到的正则也不再是正则了。

        const obj = {
            id: 1,
            name: undefined,
            fn: function (a, b) {
                return a + b
            },
            msg: {
                age: 18
            },
            color: ['pink', 'red'],

        }
        console.log(JSON.stringify(obj))  // {"id":1,"msg":{"age":18},"color":["pink","red"]}

实现2:手写递归函数

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);

有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

实现3:函数库lodash的_.cloneDeep方法

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

总结

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