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

总结

和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含对象
赋值 改变会使原数据一同改变 改变会使原数据一同改变
浅拷贝 改变不会使原数据一同改变 改变会使原数据一同改变
深拷贝 改变不会使原数据一同改变 改变不会使原数据一同改变

你可能感兴趣的:(JavaScript 浅拷贝与深拷贝)