一篇文章彻底搞懂浅拷贝和深拷贝的区别_深拷贝和浅拷贝的题

2022-11-10 14:59:03 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

强烈推荐30个原生JavaScript的demo,包括canvas时钟特效、自定义视频播放器、搜索栏快速匹配、fetch访问资源、console调试技巧等,先fork后学习,详见​​​​​点击打开查看​​​​​​​,欢迎点赞~~~谢谢,共同进步学习!

由博主《前端初级工程师面试系列一JS基础》文章一JS变量类型引伸的考点,变量类型分为基本类型和引用类型,那么在变量拷贝赋值时,也是不一样的,分为浅拷贝和深拷贝,是面试中常考的知识点,也是实际开发中经常会用到的内容。

目录

  • 浅谈深拷贝和浅拷贝
    • 深拷贝和浅拷贝的区别
    • 为什么要使用深拷贝?
    • 深拷贝的要求程度
    • 怎么检验深拷贝成功
  • 只对第一层级做拷贝
  • 拷贝所有层级
  • 存在大量深拷贝需求的代码——immutable提供的解决方案
  • 补充知识点: obj==null和obj===null的区别、怎么判断数组类型、for..in和for…of及forEach的区别、for…in在遍历对象时的坑

正文

前言: 最开始意识到深拷贝的重要性是在我使用redux的时候(react redux), redux的机制要求在reducer中必须返回一个新的对象,而不能对原来的对象做改动,事实上,当时我当然不会主动犯这个错误,但很多时候,一不小心可能就会修改了原来的对象,例如:var newObj = obj; newObj.xxx = xxx 实际上,这个时候newObj和obj两个引用指向的是同一个对象,我修改了newObj,实际上也就等同于修改了obj,这,就是我和深浅拷贝的第一次相遇。

深拷贝和浅拷贝的区别

1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用

2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”

为什么要使用深拷贝?

我们希望在改变新的数组(对象)的时候,不改变原数组(对象)

深拷贝的要求程度

我们在使用深拷贝的时候,一定要弄清楚我们对深拷贝的要求程度:是仅“深”拷贝第一层级的对象属性或数组元素还是递归拷贝所有层级的对象属性和数组元素?

怎么检验深拷贝成功

改变任意一个新对象/数组中的属性/元素, 都不改变原对象/数组

只对第一层级做拷贝

深拷贝数组(只拷贝第一级数组元素) 

  1. 直接遍历
代码语言:javascript复制
var array = [1, 2, 3, 4];
function copy (array) {
   let newArray = []
   for(let item of array) {
      newArray.push(item);
   }
   return  newArray;
}
var copyArray = copy(array);
copyArray[0] = 100;
console.log(array); // [1, 2, 3, 4]
console.log(copyArray); // [100, 2, 3, 4]

该方法不做解释(逃…)

2. slice()

代码语言:javascript复制
var array = [1, 2, 3, 4];
var copyArray = array.slice();
copyArray[0] = 100;
console.log(array); // [1, 2, 3, 4]
console.log(copyArray); // [100, 2, 3, 4]

slice() 方法返回一个从已有的数组中截取一部分元素片段组成的新数组(不改变原来的数组!)

用法:array.slice(start,end) start表示是起始元素的下标, end表示的是终止元素的下标

当slice()不带任何参数的时候,默认返回一个长度和原数组相同的新数组

3. concat()

代码语言:javascript复制
var array = [1, 2, 3, 4];
var copyArray = array.concat();
copyArray[0] = 100;
console.log(array); // [1, 2, 3, 4]
console.log(copyArray); // [100, 2, 3, 4]

concat() 方法用于连接两个或多个数组。( 该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。)

用法:array.concat(array1,array2,……,arrayN)

因为我们上面调用concat的时候没有带上参数,所以var copyArray = array.concat();实际上相当于var copyArray = array.concat([]);也即把返回数组和一个空数组合并后返回

但是,事情当然不会这么简单,我上面的标题是 “深拷贝数组(只拷贝第一级数组元素)”,这里说的意思是对于一级数组元素是基本类型变量(如number,String,boolean)的简单数组, 上面这三种拷贝方式都能成功,但对第一级数组元素是对象或者数组等引用类型变量的数组,上面的三种方式都将失效,例如:

代码语言:javascript复制
var array = [
  { number: 1 },
  { number: 2 },
  { number: 3 }
];
var copyArray = array.slice();
copyArray[0].number = 100;
console.log(array); //  [{number: 100}, { number: 2 }, { number: 3 }]
console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]

深拷贝对象

1.直接遍历

代码语言:javascript复制
var obj = {
  name: '彭湖湾',
  job: '学生'
}

function copy (obj) {
   let newObj = {};
     for (let item in obj ){
       newObj[item] = obj
     }
     return newObj;
}
 var copyObj = copy(obj);
copyObj.name = '我才不是彭湖湾呢! 哼 (。・`ω´・)';
console.log(obj); // {name: "彭湖湾", job: "学生"}
console.log(copyObj); // {name: "我才不是彭湖湾呢! 哼 (。・`ω´・)", job: Object}

该方法不做解释(逃…)

2.ES6的Object.assign

代码语言:javascript复制
var obj = {
  name: '彭湖湾',
  job: '学生'
}
var copyObj = Object.assign({}, obj);
copyObj.name = '我才不叫彭湖湾呢! 哼  (。・`ω´・)';
console.log(obj);   // {name: "彭湖湾", job: "学生"}
console.log(copyObj);  // {name: "我才不叫彭湖湾呢! 哼  (。・`ω´・)", job: "学生"}

Object.assign:用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target),并返回合并后的target

用法: Object.assign(target, source1, source2); 所以 copyObj = Object.assign({}, obj); 这段代码将会把obj中的一级属性都拷贝到 {}中,然后将其返回赋给copyObj

3.ES6扩展运算符:

代码语言:javascript复制
var obj = {
    name: '彭湖湾',
    job: '学生'
}
var copyObj = { ...obj }
copyObj.name = '我才不叫彭湖湾呢! 哼  (。・`ω´・)'
console.log(obj.name) //   彭湖湾
console.log(copyObj.name)  // 我才不叫彭湖湾呢! 哼  (。・`ω´・)

扩展运算符(…)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中

⚠️注意:实际上,无论是使用扩展运算符(…)还是解构赋值,对于引用类型都是浅拷贝。所以在使用splice()、concat()、…对数组拷贝时,只有当数组内部属性值不是引用类型是,才能实现深拷贝。对多层嵌套对象,也即是存在,很遗憾,上面三种方法,都会失败:

代码语言:javascript复制
var obj = {
   name: {
      firstName: '彭',
      lastName: '湖湾'
   },
   job: '学生'
}
var copyObj = Object.assign({}, obj)
copyObj.name.lastName = '湖水的小浅湾';
console.log(obj.name.lastName); // 湖水的小浅湾
console.log(copyObj.name.lastName); // 湖水的小浅湾

拷贝所有层级

有没有更强大一些的解决方案呢?使得我们能够

1.不仅拷贝第一层级,还能够拷贝数组或对象所有层级的各项值

2. 不是单独针对数组或对象,而是能够通用于数组,对象和其他复杂的JSON形式的对象

请看下面:

下面这一招可谓是“一招鲜,吃遍天”

1.JSON.parse(JSON.stringify(XXXX))

代码语言:javascript复制
var array = [
    { number: 1 },
    { number: 2 },
    { number: 3 }
];
var copyArray = JSON.parse(JSON.stringify(array))
copyArray[0].number = 100;
console.log(array); //  [{number: 1}, { number: 2 }, { number: 3 }]
console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]

JSON.parse() 方法用于将一个 JSON 字符串转换为对象–(反序列化)

JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串–(序列化)

序列化的缺点:

  1. 不支持基本数据类型的undefined,序列化后将其省略
  2. 不支持函数
  3. Nan,Infinity序列化的结果是null

能用大招杀的就不要用q杀嘛!!

2.手动写递归

你说啥? 你说上面的那种方法太无脑, 一定要自己写一段递归才有做技术的感觉? OK成全你!

代码语言:javascript复制
let array = [
   { number: 1 },
   { number: 2 },
   { number: 3 }
];
function copy (obj) {
        //首先判断需要拷贝的“东西”是什么类型
        if(typeof obj !== 'object' || obj == null){
            return;
        }
        let newobj = obj.constructor === Array ? [] : {};
        //obj是数组类型,下面的i就是index;obj是对象,i就是key
        for(let i in obj){
           newobj[i] = typeof obj[i] === 'object' ? copy(obj[i]) : obj[i];
        }
        return newobj
}

let copyArray = copy(array)
copyArray[0].number = 100;
console.log(array); //  [{number: 1}, { number: 2 }, { number: 3 }]
console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]

【注意】上文的所有的示例都忽略了一些特殊的情况: 对对象/数组中的Function,正则表达式等特殊类型的拷贝

上述代码中还隐藏这两个知识点:

(1)obj == null 为何不是 obj === null?

obj==null =>obj为null 或者 obj 为undefined,因为null == undefined,这样写能默认处理两种情况,obj===null ,成立前提只有obj是null

(2)怎样判断一个对象是不是数组?

先上方法(确定的方法有两种)

  1. 根据对象的class属性来判断,跨原型链调用toString()方法。 Object. prototype.toString.call(obj)===[ object Array]
  2. Array.isArray直接判断 Array. isArray(obj)。

补充:

推荐文章:为什么用Object.prototype.toString.call(obj)检测对象类型

toString为Object的原型方法,返回一个用来描述该对象的字符串,所以可以调用对象原型方法toString()来探明对象的信息。 那么原型方法怎么调用呢,利用call,将其this指向需要判断的对象, 就可以用toString()方法了。

代码语言:javascript复制
let arr=[1,2,3];
console.log(Array.prototype.hasOwnProperty("toString"));//true
console.log(arr.toString());//1,2,3 arr作为对象Object的实例,重写了toString()方法。
delete Array.prototype.toString;//delete操作符可以删除实例属性
console.log(Array.prototype.hasOwnProperty("toString"));//false
console.log(arr.toString());//"[object Array]" 
// 由于删除了实例对象中的toString()方法,找不到,顺着原型链往上走,就调用了对象Object的方法,返回的结果就和Array.prototype.toString(arr)一样的。

根据构造函数来判断 instanceof 操作符可以来表示实例是否属于某个构造函数创建的。

这种方法有一个问题,就是验证不够严格。 即使对象创建时不是使用数组创建的,但是只要原型链上有数组类型,也认为是数组,亦或者,即便创建时是数组创建,但其原型上有对象类型,便不再被认为是数组。

推荐文章

(3)for…in 和for…of,forEach的区别,for…in用于对象复制时需要注意什么?

for… in特点

  • 遍历对象返回的对象的key值,遍历数组返回的数组的下标(key)。
  • for … in 会遍历原型上的属性值
  • 遍历返回数据是乱序

总结一句: for in 循环特别适合遍历对象。

for… of特点

  • for of遍历的只是数组内的元素,而不包括数组的原型属性method和索引name
  • for … in 会遍历原型上的属性值
  • 遍历返回数据是乱序
  • for of 不同与 forEach, 它可以与 break、continue和return 配合使用,也就是说 for of 循环可以随时退出循环。

总结一句: for of 比较适合遍历数组,及其他具有遍历器的集合

forEach特点

  • 使用foreach遍历数组的话,使用break不能中断循环,使用return也不能返回到外层函数。forEach与break和return 不搭
  • forEach()无法在所有元素都传递给调用的函数之前终止遍历

for…in循环可应用于对象的复制,不过其有一个缺点,就是会从原型属性里继承prototype()属性。

例如:

代码语言:javascript复制
let array = [1,2,3,4,5] 
Array.prototype.age = 13;
var result = []; 
for(let i in array){ 
    result.push(array[i]); 
} 
alert(result.join(“,”)); 
result返回结果【1,2,3,4,5,13】 

如何避免从原型属性里继承prototype()属性,这里使用hasOwnProperty(name),该函数指示一个对象自身(不包括原型链)是否具有指定名称的属性。如果有返回true,如果没有返回false。

代码语言:javascript复制
let array = [1,2,3,4,5] 
Array.prototype.age = 13; 
var result = []; 
for(let i in array){ 
if(array.hasOwnProperty(i)){ 
    result.push(array[i]); 
} 
alert(result.join(“,”)); 
} 
result返回结果【1,2,3,4,5】

所以上面的深拷贝代码应优化为如下:

代码语言:javascript复制
let array = [
   { number: 1 },
   { number: 2 },
   { number: 3 }
];
function copy (obj) {
        //首先判断需要拷贝的“东西”是什么类型
        if(typeof obj !== 'object' || obj == null){
            return;
        }
        let newobj = Array.isArray(obj) ? [] : {};
        //obj是数组类型,下面的i就是index;obj是对象,i就是key
        for(let i in obj){
           if(obj.hasOwnProperty(i)){
               newobj[i] = typeof obj[i] === 'object' ? copy(obj[i]) : obj[i];
            }
        }
        return newobj
}

let copyArray = copy(array)
copyArray[0].number = 100;
console.log(array); //  [{number: 1}, { number: 2 }, { number: 3 }]
console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]

存在大量深拷贝需求的代码——immutable提供的解决方案

实际上,即使我们知道了如何在各种情况下进行深拷贝,我们也仍然面临一些问题: 深拷贝实际上是很消耗性能的。(我们可能只是希望改变新数组里的其中一个元素的时候不影响原数组,但却被迫要把整个原数组都拷贝一遍,这不是一种浪费吗?)所以,当你的项目里有大量深拷贝需求的时候,性能就可能形成了一个制约的瓶颈了。

immutable的作用

通过immutable引入的一套API,实现:

1.在改变新的数组(对象)的时候,不改变原数组(对象)

2.在大量深拷贝操作中显著地减少性能消耗

先睹为快:

代码语言:javascript复制
const { Map } = require('immutable')
const map1 = Map({ a: 1, b: 2, c: 3 })
const map2 = map1.set('b', 50)
map1.get('b') // 2
map2.get('b') // 50

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/186989.html原文链接:https://javaforall.cn

0 人点赞