一文告诉你,JS的深浅拷贝究竟是怎么回事!

102 阅读5分钟

写在前面

如果要拷贝一个数据,既要看是什么数据类型,也要看用的是什么方法

VN图表示引用数据类型的浅拷贝与深拷贝的关系:

2019111918214116.png

我们都知道数据的类型,分为基本数据类型引用数据类型

基本数据类型

基本类型包括:字符串、布尔值、数字、undefined、null、symbol。

基本类型的存储方式:基本类型以键值对(名-值)的方式,直接存储在  中。

image.png

基本类型的拷贝都是深拷贝

let a = 1;
    b = a;
 
b = 2;
 
b; // 2
a; // 1

当 b=a 时,栈内存会新开辟一个内存:

image.png

当你此时修改 a=2,对 b 并不会造成影响。

引用数据类型

引用数据类型包括:数组、对象、Date、RegExp、函数、特殊的基本包装类型(String、Number、Boolean)以及单体内置对象(Global、Math)。

引用数据类型的存储方式:引用类型以 " **-引用地址-** " 的方式,存在“”内存中,存在“”内存中,但是 “” 内存会提供一个 “引用地址” 指向 “” 内存中的值。

引用类型:将该对象引用地址存储在中,然后对象里面的数据存放在中。(数组、对象、Date、RegExp、函数、特殊的基本包装类型(String、Number、Boolean)以及单体内置对象(Global、Math))。

引用类型的拷贝:引用类型的拷贝有浅拷贝深拷贝之分。

引用类型的浅拷贝和深拷贝分析

(1)、引用类型的浅拷贝分析

对象的浅拷贝,拷贝的仅仅是“引用地址”,不是值。

let a = [0,1,2,3,4], 
    b = a; 
console.log(a === b);     // true 
a[0]=1; 
console.log(a, b);         // a: [1,1,2,3,4] b:[1,1,2,3,4]

我们以上面的例子画个图,初始:

image.png

当 b=a 进行拷贝时,其实复制的是 a 的引用地址,而并非堆里面的值。

image.png

而当我们a[0]=1时进行数组修改时,由于a与b指向的是同一个地址,所以自然b也受了影响,这就是所谓的浅拷贝了。

image.png

【结论】引用类型的浅拷贝,拷贝的仅仅是“引用地址”,两个对象的引用地址对应的还是同一个值,所以,无论改变哪个对象的值,另一个对象对应的值也会改变。

(2)、引用类型的深拷贝分析

对象的深拷贝,把引用地址和值一起拷贝过来。

function deepClone(obj){
    let objClone = Array.isArray(obj)?[]:{};
    if(obj && typeof obj === "object"){
        for(key in obj){
            if(obj.hasOwnProperty(key)){ //判断ojb子元素是否为对象,如果是,递归复制
                if(obj[key] && typeof obj[key] === "object"){
                    objClone[key] = deepClone(obj[key]);
                }else{ //如果不是,简单复制
                    objClone[key] = obj[key];
                }
            }
        }
    }
    return objClone;
}
 
let obj = {
    a: "hello",
    b: {
        a: "hello",
        b: 21
    }
};
 
// 拷贝
let cloneObj = deepClone(obj);
console.log('-----原a', cloneObj.a);               // hello
console.log('-----原b', cloneObj.b);               // {a: "hello", b: 21}

image.png

更改原对象,看看拷贝过来的对象是否变化 :

obj.a = "changed";
obj.b = {a: "world", b: 25}
console.log('-----新a', cloneObj.a);                // hello
console.log('-----新b', cloneObj.b);                // {a: "hello", b: 21}
 
console.log(cloneObj.a === obj.a);                  // false

image.png

【结论】引用类型的深拷贝,把引用地址和值一起拷贝过来,一个对象的值改变,另一个对象的值不受影响。

(3)、引用类型的浅拷贝和深拷贝的方法

只有引用数据类型才有深拷贝与浅拷贝之说。

  • 引用类型的浅拷贝:拷贝的仅仅是“引用地址”,两个对象的引用地址对应的还是同一个值,所以,无论改变哪个对象的值,另一个对象对应的值也会改变。
  • 引用类型的深拷贝:把引用地址和值一起拷贝过来,一个对象的值改变,另一个对象的值不受影响。

1、浅拷贝的方法

(1)、直接赋值法
var obj = {
    a: 1,
    b: {
        c: 2
    }
}
 
var cloneObj = obj;// 直接赋值是浅拷贝
 
cloneObj.a = 3;
cloneObj.b.c = 4;
 
cloneObj;
// {
//     a: 3,
//     b: {
//         c: 4
//     }
// }
 
obj;
// {
//     a: 3,
//     b: {
//         c: 4
//     }
// }
(2)、局部作用域直接使用全局作用域变量

局部作用域内直接使用全局作用域变量(使用前不做处理:比如使用 ES6 的拓展运算符等,关于 ES6 新语法对引用类型拷贝的影响下面会讲到)。

var obj = {
    a: 1,
    b: { c: 2 }
};
 
function test (x) {
    x.a = 3;
    x.b.c = 4
    console.log('---函数作用域 x', x);
}
 
test(obj);
console.log('---全局作用域 obj', obj);
 
// ---函数作用域 x 
// {
//     a: 3,
//     b: {
//         c: 4
//     }
// }
 
// ---全局作用域 obj
// {
//     a: 3,
//     b: {
//         c: 4
//     }
// }

2、深拷贝的方法

(1)、JSON.parse(JSON.stringify(obj))
let obj = {
    a: "hello",
    b: {
        c: 21
    },
    d: ["Bob", "Tom", "Jenny"],
    e: function() {
        alert("hello world");
    }
};
 
let cloneObj = JSON.parse(JSON.stringify(obj));
 
obj.a = "changed";
obj.b.c = 25;
obj.d = [1, 2, 3];
obj.e = () => { alert("changed") };
 
// {
//     a: "hello",
//     b: {
//         c: 21,
//     },
//     d: ["Bob", "Tom", "Jenny"],
//     e: undefined
// }

【拓展】在 JavaScript 中,当对象中的属性值为 undefined 时,在执行 JSON.stringify(obj) 时,这些属性会被忽略,因此最终的 JSON 字符串中不会包含这些属性。对象中的属性值为 null 没问题。例如:

const obj = { a: undefined, b: null };
 
console.log(obj); // 输出 { a: undefined, b: null }
console.log(JSON.stringify(obj)); // 输出 '{ b: null }'
(2)、递归
// 封装一个深拷贝的函数
const deepClone = (obj) => {
  let cloneObj = Array.isArray(obj) ? [] : {};
  for (let k in obj) {
    if (obj.hasOwnProperty(k)) { // 判定 obj 里是否有 k 这个属性。
      if(typeof obj[k] === "object"){ // 判定 k 是不是对象(广义)
        cloneObj[k] = deepClone(obj[k]);
      } else {
        cloneObj[k] = obj[k];
      }
    }
  }
  return cloneObj;
}
 
// 测试
let obj = {
  a: "hello",
  b: {
      c: 21
  },
  d: ["Bob", "Tom", "Jenny"],
  e: function() {
      alert("hello world");
  }
};
const clone = deepClone(obj);
console.log(clone);
 
obj.a = "changed";
obj.b.c = 25;
obj.d = [1, 2, 3];
obj.e = () => { alert("changed") };
console.log(clone);
// 可见,改变原对象并不影响深拷贝的对象:
// {
//     a: "hello",
//     b: {
//         c: 21,
//     },
//     d: ["Bob", "Tom", "Jenny"],
//     e: function(){
//         alert("hello world");
//     }
// }

3、ES6 之深拷贝与浅拷贝的实现

(1)、ES6 的 Object.assign() 方法

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

Object.assign(target, ...sources)

参数:

  • target:目标对象。
  • sources:任意多个源对象。

返回值:

  • 目标对象会被返回。

一层为深拷贝,多层为浅拷贝。

一层为深拷贝:

let obj = {
    a: "hello",
    b: 21
};
 
let cloneObj= Object.assign({}, obj);
 
cloneObj.a = "changed";
cloneObj.b = 25;
 
cloneObj;
// {
//     a: "changed",
//     b: 25
// }
 
obj;
// {
//     a: "hello",
//     b: 21
// }

多层为浅拷贝:

let obj = {
    a: "hello",
    b:{
        c: 21
    }
};
 
let cloneObj= Object.assign({}, obj);
 
cloneObj.a = "changed";
cloneObj.b.c = 25;
 
cloneObj;
// {
//     a: "changed",
//     b: {
//         c: 25
//     }
// }
 
obj;
// {
//     a: "hello",
//     b: {
//         c: 25
//     }
// }
(2)、ES6 的扩展运算符(...)

一层为深拷贝,多层为浅拷贝。

一层为深拷贝:

let obj = { 
    a: 'hello',
    b: 21
}
 
let cloneObj = {...obj};
 
cloneObj.a = 'boy';
cloneObj.b = 25;
 
cloneObj;
// {
//     a: "boy", 
//     b: 25
// }
 
 
obj;
// {
//     a: "hello", 
//     b: 21
// }

多层为浅拷贝:

let obj = { 
    a: 'hello',
    b: {
        c: 21
    }
}
 
let cloneObj = {...obj};
 
cloneObj.a = 'boy';
cloneObj.b.c = 25;
 
cloneObj;
// {
//     a: "boy", 
//     b: {
//         c: 25
//     }
// }
 
obj;
// {
//     a: "hello", 
//     b: {
//         c: 25
//     }
// }