前端面试 第三篇 js之路 深拷贝与浅拷贝

19,968 阅读7分钟

JavaScript学习之路: 艰难且漫长, 摸爬滚打这些年,还剩头上三根毛,这是我学习的见证呀😎

开始我的学习之路~~🙆‍♀️

想要完全了解深拷贝与浅拷贝,首要我们要先了解数据类型
详情点击: 基础数据类型简介

深拷贝与浅拷贝

想必你也有以下疑问:

  • 什么是拷贝(copy)
  • 那深拷贝与浅拷贝什么
  • 如何实现深拷贝与浅拷贝呢
  • ......

接下来,就让我们一起去探索!
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。

深拷贝

深拷贝: 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象(新旧对象不共享同一块内存),且修改新对象不会影响原对象(深拷贝采用了在堆内存中申请新的空间来存储数据,这样每个可以避免指针悬挂)

浅拷贝

浅拷贝: 如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址(新旧对象共享同一块内存),所以如果其中一个对象改变了这个地址,就会影响到另一个对象(只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时,会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃);

那么此时我们会想到:浅拷贝和直接赋值难道不是一样的嘛❓有什么区别❓

赋值和浅拷贝的区别

  • 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的
  • 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源

堆内存与栈内存的概念理解:

1661163137209.jpg

赋值和浅拷贝举例:

对比直接赋值和浅拷贝对象带来的改变有哪些❓

// 对象赋值
let obj1 = {
    name: 'Chen',
    age: 18,
    hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}

let obj2 = obj1;
obj2.name = 'Forever';
obj2.hobby[1] = 'swim';
obj2.hobby[2] = 'alpinism';
console.log('obj1===>', obj1);
console.log('obj2===>', obj2);

572773a588fae0482225a2d32efbc02.jpg

// 浅拷贝
let obj1 = {
    name: 'Chen',
    age: 18,
    hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}

let obj3 = {...obj1};
obj3.name = 'Forever';
obj3.hobby[1] = 'swim';
obj3.hobby[2] = 'alpinism';
console.log('obj1===>', obj1);
console.log('obj3===>', obj3);

d40c9cb1d1d49976fc0ae6ddf2ed165.jpg

上述例子,obj1是原数据,obj2是直接赋值得到的数据,obj3是通过浅拷贝得到的; 可清晰对比其对原数据的影响

对原始数据的影响
--和原数据是否指向同一对象第一层数据未基本数据类型原数据包含子对象(引用数据类型)
赋值赋值后的数据改变,会使原数据一同改变赋值后的数据改变,会使原数据一同改变
浅拷贝浅拷贝后的数据改变,不会使原数据一同改变赋值后的数据改变,会使原数据一同改变

浅拷贝的实现 注意:当拷贝对象只有一层的时候,是深拷贝

  • 展开运算符...
// 展开运算符... 实现浅拷贝
let obj1 = {
    name: 'Chen',
    hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}

let obj2 = {...obj1};
obj2.hobby[1] = 'swim';
obj2.hobby[2] = 'alpinism';
obj2.name = 'Forever';
console.log('obj1===>', obj1); // obj1===> { name: 'Chen',hobby: [ 'see a film', 'swim','alpinism', 'tourism']}
console.log('obj2===>', obj2); // obj2===> { name: 'Forever',hobby: [ 'see a film', 'swim','alpinism', 'tourism']}
  • Object.assign()
// Object.assign() 实现浅拷贝
let obj1 = {
  name: "Chen",
  hobby: ["see a film", "write the code", "play basketball", "tourism"],
};

let obj2 = Object.assign({}, obj1);
obj2.hobby[1] = "swim";
obj2.hobby[2] = "alpinism";
obj2.name = "Forever";
console.log("obj1===>", obj1); // obj1===> {name: 'Chen',hobby: [ 'see a film', 'swim', 'alpinism', 'tourism' ]}
console.log("obj2===>", obj2); // obj2===> {name: 'Forever',hobby: [ 'see a film', 'swim', 'alpinism', 'tourism' ]}

image.png

当object只有一层的时候,是深拷贝;所以当原数据进行浅拷贝,改变obj2的name 原数据obj1中的name不会改变;

  • Array.prototype.concat()
// Array.prototype.concat() 实现浅拷贝
let arr1 =  [
    {
        name: 'Chen'
    },
    'see a film', 
    'write the code', 
    'play basketball', 
    'tourism'
];
let arr2 = arr1.concat([]);
arr2[0].name = 'Forever';
arr2[1] = 'play games';
console.log('arr1===>', arr1); // arr1===> [{ name: 'Forever' },'see a film','write the code','play basketball', 'tourism']
console.log('arr2===>', arr2); // arr2===> [{ name: 'Forever' },'play games','write the code', 'play basketball', 'tourism']
  • Array.prototype.slice()
// Array.prototype.concat() 实现浅拷贝
let arr1 =  [
    {
        name: 'Chen'
    },
    'see a film', 
    'write the code', 
    'play basketball', 
    'tourism'
];
let arr2 = arr1.slice();
arr2[0].name = 'Forever';
arr2[1] = 'play games';
console.log('arr1===>', arr1); // arr1===> [{ name: 'Forever' },'see a film','write the code','play basketball', 'tourism']
console.log('arr2===>', arr2); // arr2===> [{ name: 'Forever' },'play games','write the code', 'play basketball', 'tourism']

image.png

当Array只有一层的时候,是深拷贝;所以当原数据进行浅拷贝,改变arr2的arr[1],而原数据arr1中的arr1[1]没有改变;

深拷贝的实现

  • JSON.parse(JSON.stringify())
//  JSON.parse(JSON.stringify())实现深拷贝Object
let obj1 = {
    name: 'Chen',
    hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1 === obj2); // false
obj2.name = 'Forever';
obj2.hobby[1] = 'swim';
obj2.hobby[2] = 'alpinism';

console.log('obj1===>', obj1); // obj1===> { name: 'Chen',hobby: ['see a film', 'write the code', 'play basketball', 'tourism']}
console.log('obj2===>', obj2); // obj2===> { name: 'Forever',hobby: ['see a film', 'swim', 'alpinism', 'tourism']}

image.png

//  JSON.parse(JSON.stringify())实现深拷贝Array
let arr1 =  [
    {
        name: 'Chen'
    },
    'see a film', 
    'write the code', 
    'play basketball', 
    'tourism'
];
let arr2 = JSON.parse(JSON.stringify(arr1));
console.log(arr1 === arr2); // false
arr2[0].name = 'Forever';
arr2[1] = 'play games';
console.log('arr1===>', arr1); // arr1===> [{ name: 'Chen' },'see a film','write the code','play basketball', 'tourism']
console.log('arr2===>', arr2); // arr2===> [{ name: 'Forever' },'play games','write the code', 'play basketball', 'tourism']

image.png

😻 既然Object和Array可以通过JSON.parse(JSON.stringify())实现深拷贝,那么Date与Function可以实现嘛? 我们一起尝试以下,看看会有什么情况出现:

let fun1 = function() {
    console.log('run~');
}
let fun2 = JSON.parse(JSON.stringify(fun1)) // undefined
JSON.parse(fun2) //Error: "undefined" is not valid JSON
let date1 = new Date();
let date2 = JSON.stringify(date1) // undefined
JSON.parse(date2) // Error: "undefined" is not valid JSON

image.png

JSON.parse()  方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象 因此undefined不能被转换并抛出异常:"undefined"不是有效的 JSON;

**为什么function类型与Date类型转换成JSON后 会是undefined, 请参考链接:MDN(感兴趣的可以根据JSON序列化原理,手动实现一下JSON.stringify())

我们平时开发中将JSON.stringify应用最多的可能就是浅层的对象进行深拷贝,也就是进行序列化处理。但是当我们进行手撕代码的时候,需要考虑各种边界情况,这对于我们来说就比较麻烦,作为面试也是对数据类型的全面考察

  • jQuery.extend()方法
//  需要引入jQuery库哦~
let obj = {
    name: 'Chen',
    hobby: [ 
        'see a film', 
        'write the code', 
        'play basketball', 
        'tourism'
    ]
}
let obj1 = jQuery.extend(true, {}, obj);
console.log(obj === obj1); // false
obj2.name = 'Forever';
obj2.hobby[1] = 'swim';
obj2.hobby[2] = 'alpinism';
console.log('obj1===>', obj1); // obj1===> { name: 'Chen',hobby: ['see a film', 'write the code', 'play basketball', 'tourism']}
console.log('obj2===>', obj2); // obj1===> { name: 'Chen',hobby: ['see a film', 'swim', 'alpinism', 'tourism']}

image.png

  • 手写递归方法:(递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝)
// 检测数据类型的功能函数
const checkedType = (target) => Object.prototype.toString.call(target).replace(/\[object (\w+)\]/, "$1").toLowerCase();
// 实现深拷贝(Object/Array)
const clone = (target) => {
    let result;
    let type = checkedType(target);
    if(type === 'object') result = {};
    else if(type === 'array') result = [];
    else  return target;
    for (let key in target) {
        if(checkedType(target[key]) === 'object' || checkedType(target[key]) === 'array') {
            result[key] = clone(target[key]);
        } else {
            result[key] = target[key]; 
        }
    }
    return result;
}

调用一下手写递归实现深拷贝方法:🙊

const obj = {
    name: 'Chen',
    detail: {
        age: '18',
        height: '180',
        bodyWeight: '68'
    },
    hobby: ['see a film',  'write the code',  'play basketball', 'tourism']
}

const obj1 = clone(obj);
console.log(obj1); // { name: 'Chen',detail: { age: '18', height: '180', bodyWeight: '68' },  hobby: [ 'see a film', 'write the code', 'play basketball', 'tourism' ]}
console.log(obj1 === obj); // false

循环引用

什么是循环引用: 一般指对象直接或间接地引用了自身;

循环引用一般分为下列几种情况:

  • 父级引用:自身(obj)中的属性对应的值指向自己(obj);
  • 同级引用:自身(obj)中某一属性对应的值 指向(引用)自身(obj);
  • 相互引用:两个对象中的属性相互引用;

递归函数,看似已经解决了我们日常深拷贝的需要, 但是没有考虑到对象'循环引用'问题;

const obj = {
    name: 'Chen',
    detail: {
        age: '18',
        height: '180',
        bodyWeight: '68'
    },
    hobby: ['see a film',  'write the code',  'play basketball', 'tourism']
}
obj.temp = obj; // obj中的属性temp的值指向了obj
const obj1 = clone(obj); // 报错:栈内存溢出

1666061954637.png 以上我们可以看出: obj中新增属性temp属性引用obj, obj中的temp中的temp属性引用了obj, 这就构成了循环引用;clone函数中, 循环调用clone,从而造成一个死循环导致爆栈;


父级引用

const obj = {
    name: 'Chen',
    detail: {
        age: '18',
        height: '180',
        bodyWeight: '68'
    },
    hobby: ['see a film',  'write the code',  'play basketball', 'tourism']
}
obj.temp = obj; // obj中的属性temp的值指向了obj

image.png

同级引用

const obj = {
  name: 'Chen',
  detail: {
      age: '18',
      height: '180',
      bodyWeight: '68'
  },
  hobby: ['see a film',  'write the code',  'play basketball', 'tourism']
}
obj.detail['tempDetail'] = obj.detail; // obj.detail中的属性tempDetail的值指向了obj.detail

image.png

相互引用

const obj = {
   name: 'Chen',
   detail: {
       age: '18',
       height: '180',
       bodyWeight: '68'
   },
   hobby: ['see a film',  'write the code',  'play basketball', 'tourism']
}
const obj1 = {
   name: 'ChenYonx',
   detail: {
       age: '23',
       height: '175',
       bodyWeight: '70'
   },
   hobby: ['Watch the drama',  'Ride']
}
obj.tempDetail= obj1;
obj1.tempDetail = obj;
console.log('obj====>', obj);
console.log('obj1====>', obj1);

image.png 因此针对深拷贝的循环应用问题,对clone函数进行优化:

// 检测数据类型的功能函数
const checkedType = (target) => Object.prototype.toString.call(target).replace(/\[object (\w+)\]/, "$1").toLowerCase();
// 实现深拷贝(Object/Array)
const clone = (target, hash = new WeakMap) => {
    let result;
    let type = checkedType(target);
    if(type === 'object') result = {};
    else if(type === 'array') result = [];
    else  return target;
    if(hash.get(target)) return target;

    let copyObj = new target.constructor();
    hash.set(target, copyObj)
    for (let key in target) {
        if(checkedType(target[key]) === 'object' || checkedType(target[key]) === 'array') {
            result[key] = clone(target[key], hash);
        } else {
            result[key] = target[key];
        }
    }
    return result;
}
   

调用一下优化后的clone(针对循环引用)

const obj = {
  name: 'Chen',
  detail: {
      age: '18',
      height: '180',
      bodyWeight: '68'
  },
  hobby: ['see a film',  'write the code',  'play basketball', 'tourism']
}
obj.tempObj = obj;
const obj1 = clone(obj);
console.log('obj1=====>', obj1);

image.png

哈哈~ 可以完整的将循环引用的数据进行深拷贝下来

至此 深拷贝与浅拷贝就结束咯~

是我对js拷贝的理解了,在闲暇时光做的一个总结归纳~
想不到自己还是写完了~
希望对你有所帮助~

下载 (2).jpg