拿捏~面试官——深入理解JavaScript中的拷贝:浅拷贝vs深拷贝

372 阅读6分钟

前言

JavaScript 中,拷贝是一项常见的操作。当我们需要复制对象或数组时,我们通常会遇到两种不同的拷贝方式:浅拷贝和深拷贝。这两者之间的区别对于开发人员来说至关重要,因为它们在处理数据时具有不同的行为。今天我就来带大家来深入理解浅拷贝和深拷贝!

js中的变量

 js中的原始类型包括数字、字符串、布尔值、nullundefined
 它们占的内存小 会直接存放在在词法环境的栈当中,
 而引用类型如对象、数组由于复杂性(体积大)在预编译时会被存储在堆中,而栈中存储的是它们在堆中的地址。
let obj={  
age:18  
}  
let obj2=obj  
obj.age=20  
console.log(obj2.age);
输出结果为20
输出结果为20是因为执行 let obj2=obj时候obj2在栈中得到的值为obj存储在堆中的地址,
当执行obj.age=20后会将堆中obj.age赋值为20。
执行console.log(obj2.age)后会通过obj2在栈中的地址在堆中找到对应地址下age的值

image.png

拷贝的概念

拷贝并不是简单的复制通常只针对引用类型
let a={
    name:'coder'
}
let b=a
这并不是拷贝,虽然a,和b是两个变量但至始至终都只创建出来了一个对象。
那怎样才算是真正的拷贝呢?咱们接着往下聊~

浅拷贝

可以使用Object.create()方法来创建新的对象,具体操作如下:

let a={
name:'coder'
}
let b=Object.create(a)
a.name='fu'
console.log(b.name)
 这段代码打印出的结果为'fu',
 由此可以看出浅拷贝的特点当原对象里边的内容变更了新对象里边的内容也会随之变更
 
 

也可以使用Object.assign()来实现浅拷贝

let a={
name='fu'
}
let b={
age=18
}
let c=Object.assign(a,b)
console.log(c,a)
 打印出的结果为{name:'fu',age:18} {name:'fu',age:18}
 说明assign()会将b里边的属性拼接到a里边(a会受影响)
 ——以上代码并没有创建出新对象不能称为拷贝
 
let a={
name='fu'
like:{
  n:'running'
  }
}
let c=Object.assign({},a)
a.name='xu'
a.like.n='swimming'
console.log(c,a)
console(c.name)

打印出结果为
 {name:'fu',like:{n:'swimming'}}{name:'fu',like:{n:'swimming'}} 'fu'上述代码创建出来了新的对象并且新对象和原对象一模一样,
 且当原对象里边的内容变更了时,新对象里边的内容也会变更了。由此为浅拷贝。

[].concat(arr)——常见的浅拷贝

let arr=[1,2,3,{a:10}]
let newArr=[].concat(arr)//将arr中的元素合并到[]中,并返回一个新的数组
arr.splice(1,1)
arr[3].a=100  
console.log(newArr);
输出结果为[1,2,3,{a:100}]故这也是一种常见的浅拷贝

数组的解构

 解构就是将数组中的元素一个个剖析出来
 
let arr=[1,2,3,{a:10}]
console.log(...arr);
 运行结果为 1 2 3 {a:10}   
 从运行的结果我们便可以很好的理解解构的概念

接下来用数组的解构来实现浅拷贝

let arr=[1,2,3,{a:10}]
let newArr=[...arr]
arr[3].a=100
console.log(newArr);
 输出结果为[1,2,3,{a:100}]
 由此可见解构这种行为可作为一种浅拷贝
 

arr.slice(0)

我们先聊聊 slice和splice的区别

slice(start,end)是 从已有数组中返回选定的元素,返回一个新数组,包含从 start 到 end(不包含该元素)的数组元素。不会改变原数组,而是返回一个子数组。
splice(start,end)是表示从索引i开始删除,一共会删除j个元素,注意!!!用splice截取数组时候原数组会发生改变的,这与slice不同

用arr.slice(0)来实现浅拷贝

let arr=[1,2,3,{a:10}]
let newArr=arr.slice(0)//第二位可以不写或写成length-1
console.log(newArr)
arr[3].a=100
console.log(newArr)
输出结果为[1,2,3,{a:10}]
         [1,2,3,{a:100}]由此可见为浅拷贝
         
         

arr.toReversed().reverse()

先简单了解下arr.toReversed和arr.reverse()

      toReversed()它会返回一个元素顺序相反的新数组,不会改变原数组。
      reverse()方法用于修改原始数组将数组中的元素顺序颠倒

用arr.toReversed().reverse()实现浅拷贝

let arr=[1,2,3,{a:10}]
let newArr=arr.toReversed().reverse()
arr[3].a=100
console.log(newArr)

输出结果为[1,2,3,{a:100}]

手搓浅拷贝

let obj={
    name:'fu',
    like:{
    a:'food'
    }
}
function shallow(obj){
  let newObj={}
  for(let key in obj){/*用for in来遍历对象属性但for in 语句也会遍历到对象隐式具有的属性
      if(obj.hasOwnProperty(key)){//判断是否为显示具有的属性,将显示具有的属性进行拷贝
      newObj[key]=obj[key]
      }
  }
return newObj
}
console.log(shallow(obj));//{xxx}

let obj2=shallow(obj)
obj.like.a='beer'

console.log(obj2);//{xxx}
输出结果为{name:'fu',like:{a:'food'}}
{name:'fu',like:{a:'beer'}}
手搓浅拷贝的实现完成啦
总结:借助for in 遍历原对象,将原对象的属性值增加在新对象中
  因为for in 会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝
  的属性是不是对象显示具有的

深拷贝

深拷贝的定义

基于原对象,拷贝得到了一个新的对象,原对象中的内容的修改不会影响到新对象

JSON.parse(JSON.stringify(obj))

let obj={
   name:'fu',
   age:18,
   like:{
   n:'coding'
},
a:true,
b:undefined,
c:null,
d:Symbol(1),
f:function(){}
}
   let obj2=JSON.parse(JSON.stringify(obj))
   obj.like.n='running'
   console.log(obj2)

 输出结果:{name:'萍萍',age:18,like:{n:'coding'},a:true,c:null}
由上可看出JSON.parse(JSON.stringify(obj))为深拷贝,但有严重缺陷
不能识别BigInt类型
不能拷贝undefined Symbol function 类型的值
 注意 不能处理循环引用

structuredClone()

const user={
    name:{
    firstName:'xu'
    lastName:'yu'
    },
    age:18
}
const newUser=structuredClone(user)
user.name.firstName='xx'
console.log(newUser);
 输出结果:{name:{firstname:'xu' lastName:'yu'},age:18}由此可看出为深拷贝
 注意!!!这个方法是最新打造出了,可能在某些浏览器上跑不了

实现深拷贝——手搓

const user={
    name:{
    firstName:'xu'
    lastName:'yu'
    },
    age:18
}
function deep(obj){
  let newObj={}
  for(let key in obj){
  if(obj.hasOwnProperty(key)){//只拷贝显示具有的属性
     if(obj[key] instanceof Object){
        newObj[key]=deep(obj[key]);//递归
     }else{
       newObj[key]=obj[key]
     }
   }
  }
return newObj
}
const newUser=deep(user)
user.name.firstName='xuxu'
console.log(newUser)

 输出结果{ name:{firstName:'xu' lastName:'yu'},age:18}
 深拷贝完成啦

实现原理

 借助for in 遍历原对象,将原对象的属性增加在新对象中
因为for in 会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的
如果遍历到的属性值是原始值类型,直接往新对象中赋值,如果是引用类型,递归创建新的子对象

结语在实际中用到浅拷贝更多,两种拷贝的运用要看实际开发需求