• 作者:老汪软件技巧
  • 发表时间:2024-10-14 15:03
  • 浏览量:

引言

在处理对象和数组这类复合数据类型时,一个常见的挑战就是如何正确地复制这些数据结构,以避免不必要的副作用。这就是“深浅拷贝”概念登场的地方。本文旨在通过深入浅出的方式,带你全面了解JavaScript中深浅拷贝的概念、方法以及实现原理。

let a = {
    name: '今天一定晴q',
    age: 18,
    like: {
        n: 'sleep'
    }
}

现在我有一个对象 a ,如果我创造出了一个新对象并让它长得跟 a 一样,那么这就是拷贝。但是注意,let b = a 这不叫拷贝,因为在堆中还是只有一个对象。

拷贝又分为深拷贝和浅拷贝。

浅拷贝

基于原对象拷贝得到一个新的对象,原对象中数据的修改会影响新对象。有以下六种方法:

Object.create(x)

let b = Object.create(a) 
a.age = 20
console.log(b.age)  // 20   ba的修改影响了

Object.create(a)创建了一个新对象b,并且能够让新对象隐式继承到原对象 a 的属性,这些属性存在 b 的原型上

Object.assign({}, x)

Object.assign(a, b)可以把 b 里面的属性拼接到 a 上,这样 a 对象就拥有了 a 和 b 所有的属性:

let b = {
    sex: 'female'
}
let c = Object.assign(a, b)
console.log(c)  // { name: '今天一定晴q', age: 18, sex: 'female' }
console.log(a)  // { name: '今天一定晴q', age: 18, sex: 'female' } c 和 a 就是一个东西

所以可以利用Object.assign({}, a)拷贝出一个和 a 相同的新对象

需要注意的是,如果 a 和 b 中存在引用类型(如对象或数组),那么 Object.assign 只会复制引用,因此原对象中数据的修改会影响新对象

let c = Object.assign({}, a)
a.age = 20
a.like.n = 'food'
console.log(c)  // { name: '今天一定晴q', age: 18, like: { n: 'food' } }

[].concat(arr)

将 arr 中的元素和 [] 中的元素合并,返回一个新数组 newArr

let arr = [1, 2, 3, {a: 10}]
let newArr = [].concat(arr)
arr[1] = 20
arr[3].a = 100
console.log(newArr);  // [ 1, 2, 3, { a: 100 } ]  引用类型的元素被改变了,拷贝还是不够彻底

数组解构

将原数组的元素解构到一个新数组中

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)

arr.slice(a, b)可以接收两个参数,分别是开始的下标和结束的下标(左闭右开),返回一个数组,包含我们需要取下的那一节数据,原数组不受影响

_深浅拷贝面试_深拷贝浅拷贝面试题

let arr = [1, 2, 3, {a: 10}]
let s = arr.slice(1, 2) 
console.log(s);  // [ 2 ]

如果不写第二个参数就可以一截到底,直接取下原数组中所有元素返回一个新数组。但改变原数组中的引用类型元素,新数组还是会受到影响

let arr = [1, 2, 3, {a: 10}]
let newArr = arr.slice(0) 
console.log(newArr);   // [ 1, 2, 3, { a: 10 } ]
arr[3].a = 100
console.log(newArr); // [ 1, 2, 3, { a: 100 } ]

arr.toReversed().reverse()

toReversed()会翻转数组并返回出一个新数组,而reversed()会将原数组的元素翻转

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

实现原理创建新对象借助for in 遍历原对象如果key是obj显式具有的属性,就将该属性及其值增添至新对象中返回这个新对象

function shallowCopy(obj) {
    let newObj = {}
    for(let key in obj) {   
        if(obj.hasOwnProperty(key)) {   
            newObj[key] = obj[key]
        } 
    }
    return newObj
}

当遍历对象的所有键名时,会遍历到对象隐式具有的属性(即对象原型上的属性),但是开发过程中我们一般只需要拷贝对象显式具有的属性,所以使用if(obj.hasOwnProperty(key)) {...}来判断要拷贝的属性是否为对象显式具有的

深拷贝

基于原对象拷贝得到一个新的对象,原对象中内容的修改不会影响新对象。有以下两种方法:

JSON.parse(JSON.stringify(obj))

把对象变成JSON字符串,再把JSON字符串变成对象

let obj = {
    name: '今天一定晴q',
    age: 18,
    like: {
        n: 'money'
    },
    a: true,
    b: null,
    c: undefined,
    d: Symbol('f'),
    f: function() {}
}
let newObj = JSON.parse(JSON.stringify(obj))  
obj.like.n = 'coding'
console.log(newObj);  // { name: '今天一定晴q', age: 18, like: { n: 'money' }, a: true, b: null }

正如代码所示,这种方法有几个缺点:

structuredClone(obj)

js新的内置的深拷贝方法,可能目前还有很多浏览器没跟上

const user = {
    name: {
        firstname: '今天一定晴',
        lastname: 'q'
    },
    age: 18
}
const newUser = structuredClone(user)
user.name.firstname = 'hello'
console.log(newUser)  // { name: { firstname: '今天一定晴', lastname: 'q' }, age: 18 }

实现原理创建新对象借助for in 遍历原对象如果key是obj显式具有的属性,就将该属性及其值增添至新对象中如果遍历到的属性值是原始类型,直接往新对象中赋值,如果是引用类型,递归创建新的子对象返回这个新对象

function deepClone(obj) {
    let newObj = {}
    for(let key in obj) {
        if(obj.hasOwnProperty(key)) {  // 只拷贝显式具有的属性
            if(typeof obj[key] === 'object' && obj[key] !== null) {  // 如果是对象,递归拷贝
                newObj[key] = deepClone(obj[key])
            } else {
                newObj[key] = obj[key]
            }
        }
    }
    return newObj
}

这种深拷贝写法属于是乞丐版,但是面试中会写这种就够了,让面试官明白你懂里面的核心思想

结语

以上带大家全面了解了拷贝的所有方法,并手搓了实现原理。两种拷贝没有优劣之分,我们只需认清二者的区别,拎清使用场景。