你不知道的js中的拷贝方法

204 阅读9分钟

前言

我们在web的开发过程中都会面对一个必不可少的一部分数据的拷贝,而拷贝被分为深拷贝和浅拷贝,那当我们数据结构越来越复杂时,我们在js中能够实现精确的拷贝尤为的重要,下面就让我们由浅入深探索拷贝的奥秘吧。

拷贝

我们再来看一下我们老生常谈的一份代码

let a = 1
b = a
a = 2
console.log(b);
//输出1

我们又一次看到了这份代码,我们在前面的文章中提过原始类型是放在栈里面的,原始类型的赋值是值得复制,所以在第一段代码虽然后面对a的值进行修改,但b之仍不变为1,这其实就叫深拷贝

let obj = {
    a:1
}
let obj2 = obj
obj2.a = 2

console.log(obj2);

那同样对于引用类型,我们提到过对于引用类型里面的值是存放在堆里面的,而在栈中存放的是引用地址,引用类型的赋值是引用地址的复制,所以在第二段代码中当我们对对象obj中的内容进行更改相应的obj2的内容也会与之更改,这其实就是浅拷贝

而其实在我们再提及拷贝的概念时,上面的两个示例是很不贴切的,因为我们在聊拷贝时一定是只去引用类型中谈,因为在原始类型中的赋值就是值得复制一定是深拷贝0,上面两份代码其实都是赋值而我们提及上面的代码是为了让我们理解深拷贝和浅拷贝,下面就让我们深入地去理解深拷贝和浅拷贝

-只针对引用类型

-基于原对象,拷贝得到一个新对象

浅拷贝

我们上面通过一份代码浅层的理解浅拷贝的含义,其实在js中我们会通过很多方式来实现浅拷贝,现在让我们来深入理解一下浅拷贝

浅拷贝:新对象会受原对象的影响(只拷贝了对象的第一层,而里面的的子对象只拷贝的是引用地址)

Object.create(x)

我们在讲解js原型链的时候提到了除了Object.creat(null)所构建出来的对象没有原型,其他的对象在使用中都有自身的原型,那我们就可以根据Object.creat(null)的特性来作为我们实现浅拷贝的一种方式:

let obj  = {
    a:1
}

let newObj = Object.create(obj)
obj.a = 2

console.log(newObj.a);

我们通过通过Object.create(obj)来创建一个空对象,并在这个对象的隐式原型指向obj,即在将obj的地址放在了创建的新对象的隐式原型中,那当我们对原对象进行修改,那新对象调用时内容也会发生更改。其实这个也可以不算是一种浅拷贝的方法,仁者见仁智者见智吧,因为我们刚刚再提及浅拷贝时说过只拷贝了对象的第一层,而里面的的子对象只拷贝的是引用地址而上面的方法并没有拷贝对象中的子对象,那要具体将该方法归不归为浅拷贝这一类当中去就看读者大大自己了

Object.assign({},obj)

那我们就来看看浅拷贝是如何真正实现对对象中的子对象进行拷贝,那就让我们来看一下第二种方法对象的合并Object.assign({},obj)通过方法的使用形式我们不难看出这个方法放在了Object原型上,那就让我们来看一下这个个方法是如何实行浅拷贝的吧

let obj = {
    a: 1,
}

let obj2 = {
    b: 2
}
let obj3 = Object.assign(obj,obj2)//将后面对象的内容拼到前面
console.log(obj3);
//输出
//{ a: 1, b: 2 }

这个方法我们把obj2拼到了obj里面去,导致obj中的内容发生改变后再返回拼完后的obj,那我们看完了方法的使用效果,那为什么我们会说这是一个浅拷贝,并且还是一个标准的浅拷贝那让我们对代码进行改一改来进一步去理解

let obj = {
    a: 1,
    b:{
        n : 2
    }
}
let obj3 = Object.assign({},obj)//将后面对象的内容拼到前面
obj.a = 10
obj3.b.n = 20
console.log(obj3); 
//输出:{ a: 1, b: { n: 20 } }

image.png 通过输出我们可以看到如果修改obj中的属性obj3同样也会随之修改,所以用这种方法实现的是浅拷贝

[].concat(arr)

我们来看一下数组这个引用类型是怎么合并的,为了实现数组合并数组自身有一个[].concat(arr)方法,先让我们来看一下这个个方法是如何使用的

let arr = [1, 2]
let arr2 = [3, 4]

let arr3 = arr.concat(arr2)
//concat()可以用来合并数组,不会影响调用的数组,因为它会开一个新的数组来储存,concat()返回一个新数组。
console.log(arr3);
console.log(arr, arr2);

在上面代码中我们通过concat方法将arr和arr2数组的内容进行合并放入新构建的数组arr3中存放,那其中arr和arr2数组中的内容没有改变。那如果我们将数组中的原始类型换成引用类型使用该方法那不就实现了浅拷贝了吗?

let arr = [{a: 1}]
let arr2 = [2]
let arr3 = arr.concat(arr2)
console.log(arr3)
arr[0].a = 2
console.log(arr3)

image.png 我们可以看到输出内容当我们数组中放入的是引用类型使用concat方法将内容拷贝到新的数组中,新数组中的引用类型内容会随着原始数组数据的更改而更改

数组解构

其实在数组中,我们也能通过展开运算符使用结构的方法实现浅拷贝

let arr = [1, {a: 1}]
let arr2 = [...arr]
console.log(arr2)
arr[1].a = 2
console.log(arr2)

// 输出:
// [ 1, { a: 1 } ]
// [ 1, { a: 2 } ]

我们我们通过结构的方法将数组arr内容拷贝到arr2中,当我们对arr中的内容进行更改时arr2的内容也会随之改变,那这个个方法不就是我们所谈及的浅拷贝。

arr.slice()

其实在数组的分割方法中也会出现浅拷贝,当我们提及数组的分割时就要着重区分slice()splice()两种分割方式,如果用splice()这个方法会对原数组进行修改,而slice()并不会

let arr = [1, 2, 3]
let newArr = arr.slice(1)
console.log(newArr);
console.log(arr)

console.log('--------------------------')

let arr2 = [1, 2, 3]
let newArr2 = arr2.splice(1)
console.log(newArr2);
console.log(arr2)

image.png 我们同过输出的结果更呢个清晰地看出这个了两种方法的区别,那现在让我们来看一下slice()是如何实现浅拷贝的

let arr = [1,2,{n:3}]
let newArr = arr.slice()      
arr[2].n = 30

console.log(newArr);

image.png

arr.toReversed().reverse()

toReversed()这个是ES6新构建的反转一个方法,它会凭空创建一个新数组来存放反转后的数组,那就让我们来看一下它是如何实现浅拷贝的,同时在该方法创建之前也有一个反转的方法reverse()他们之间区别就是reverse()这个方法在使用时会直接在原数组上进行更改

let arr = ['a', 'b', 'c', 'd', 'e']
console.log(arr.toReversed().reverse());
console.log(arr);

// 输出:
// [ 'a', 'b', 'c', 'd', 'e' ]
// [ 'a', 'b', 'c', 'd', 'e' ]

我们可以看到我们我们先通过toReversed()方法将arr反转并存入新数组中,在对新数组自身使用reverse()方法再反转回去实现拷贝

深拷贝

上面我们把浅拷贝介绍完了,我们发现浅拷贝拷起来很肤浅它只能去拷贝底层的数据那当我们想要将所有内容进行拷贝并实现内容不会随言数据的更改而更改那我们就要使用深拷贝,现在让我们来聊一聊深拷贝常见的几种方法

JSON.parse(JSON.stringify(obj))

在js这门语言中官方并没有为我们打造一个方法来实现深拷贝,所以我们在使用时往往将两个方法结合一起使用的

JSON.stringify():将对象转化为字符串

JSON.parse():将字符串转化为对象

现在让我们来看一下这两个方法是如何使用的:

let obj = {
    name: '钟总',
    age: 18,
    like: {
      n: '金铲铲'
    },
    a: true,
    b: null,
    c: undefined,
    d: Infinity,
    e: -Infinity,
    f: NaN,
    g: Symbol(1),
    // h: 123n,
    i: function() {}
  }

  obj.c = obj.like
  obj.like.n = obj.c

//   let str = JSON.stringify(obj)//将对象变成字符串
  let res = JSON.parse(JSON.stringify(obj))//将字符串变成对象
  obj.like.n = '王者'
  console.log(res);
  console.log(obj);

image.png 我们根据输出结果发现这个方法在使用时部分数据实现了深拷贝但同时会存在一些属性不能够被识别,这其实就是这个方法在使用时的缺点

  1. 不能识别bigint
  2. 不能拷贝 undefined、Symbol、function, NaN Ifinity
  3. 无法处理循环引用

structuredClone()

那在js艰难又漫长的编程旅程中官方终于打造一个structuredClone()方法让我们可以实现深拷贝,让我们来看一下这个方法是如何实现的

const user = {
    name:'zyx',
    like:{
        n:'泡脚',
        m:'吃鸡'
    }
}


const newUser = structuredClone(user)

user.like.n = '喝茶'

console.log(user);
console.log(newUser);

image.png 我们通过这个方法实现了对user对象的深度拷贝

重点

而我们往往会在日后的面试中出现面试官让我们手搓一个深度拷贝的方法,代码如下,代码如何实现的我们会在后面介绍递归是来讲解,读者大大们可以先思考一下代码是如何实现的

let person ={
    name:'zyx',
    age:18,
    like:{
        n:'running'
    }
}



function shallow(obj){
    let res = {}
    for(let key in obj){
        //key是不是obj显示具有的属性
        if(obj.hasOwnProperty(key)){
            res[key] = obj[key]
        }
       
    }

    return res
}
console.log(shallow(person));

结语

拷贝

-只针对引用类型 -基于原对象,拷贝得到一个新对象

1.浅拷贝:新对象会受原对象的影响(只拷贝了对象的第一层,而里面的的子对象只拷贝的是引用地址)

  • Object.create(x)

  • Object.assign({},obj)

  • [].concat(arr)

  • 数组解构

  • arr.slice()

  • arr.toReversed().reverse()

实现原理: 创建一个新的对象,将原对象的属性通过for(let key in obj)复制到新对象上

2.深拷贝:新对象不受原对象的影响 JSON.parse(JSON.stringify(obj))

  • 1.不能识别bigint

  • 2.不能拷贝 undefined、Symbol、function, NaN Ifinity

  • 3.无法处理循环引用

structuredClone()