js深浅拷贝(官方新老方法及手写版)

前言

今天聊一聊js中的深浅拷贝,我们先来看一份代码:

js 复制代码
let obj  = {
    age:18
}

let obj2 = obj
obj.age = 20
console.log(obj2.age);

思考一下,这份代码的执行结果?

分析:

  1. 在调用栈中,创建全局上下文执行对象,有变量环境和词法环境
  2. let声明的变量存放于词法环境中。
  3. obj,obj2首先值为undefined,然后赋值,发现是一个对象,引用类型数据存放于堆中,由地址指向这个堆里的值,然后把obj的地址赋予obj2
  4. 因此他们的地址一样,更改了里面的内容,另一份打印出来也跟着变了。

拷贝

通常只针对引用类型,原始类型直接赋值,永远都是深拷贝,没有什么好讨论的。

浅拷贝

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

常见的浅拷贝方法:

  1. Object.create(x)
js 复制代码
let a = {
    name: '毛毛'
}
let b = Object.create(a)
a.name = '123'
console.log(b.name);

原来的a改变了,b也跟着改变,这种拷贝叫做浅拷贝。

  1. Object.assign({},a)
js 复制代码
let a = {
    name: '毛毛'
}

let b = {
    age: 18
}
// 数组中合并可以用concat,对象怎么合并?
let c = Object.assign(a, b)
console.log(c, a);
// 发现原来的a也变了(将b拼接进了a)

在这份代码中,我们发现assign方法可以将b拼接给a,我们接着往下看。

js 复制代码
let a = {
    name: '毛毛'
}


let c = Object.assign({}, a)
a.name = 'aaa'
console.log(c, a);

我们拷贝了一份a,更改a中的name属性,那么c中的name会改变吗?

我们发现拷贝的这一份并没有跟着改变,难道这是深拷贝?接着往下看

js 复制代码
let a = {
    name: '毛毛',
    like: {
        n: 'running'
    }
}
let c = Object.assign({}, a)
// a.name = '123'
a.like.n = 'swimming'
console.log(c, a);

我们发现两份都改变了,回过头看我们的前言,事实上,这还是由于引用类型的数据存放于堆中,这种拷贝还是不够深,因此属于浅拷贝的范畴。

  1. [].concat(arr)
js 复制代码
let arr = [1, 2, 3, { a: 10 }]
let newArr = [].concat(arr)
arr[3].a = 100
console.log(newArr, arr);

原数组和新数组都跟着改变了,这还是属于浅拷贝。

  1. 数组解构
js 复制代码
let arr = [1, 2, 3, { a: 10 }]

let newArr = [...arr]
arr[3].a = 100
console.log(newArr, arr);

原数组和新数组都跟着改变了,这还是属于浅拷贝。

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

slice()类似python中的切片,左闭右开,传两个参数,切下标哪里到哪里,第一位0第二位不写默认从下标0到最后。

  1. arr.toReversed().reverse()
js 复制代码
let arr = [1, 2, 3, { a: 10 }]
let newArr = arr.toReversed().reverse()
arr[3].a = 100
console.log(newArr, arr);

这里由于我node的版本老了,就不给展示结果了,但是它依然是浅拷贝。

手写浅拷贝

至此我们都在讲一些js内置方法做浅拷贝,那么如果面试官问你能不能自己手写一个浅拷贝呢?

js 复制代码
let obj = {
    name:'张三',
    like:{
        a:'food'
    }
}

// 手写一个浅拷贝,希望传一个对象进来,能够拷贝一个
function shallowCopy(obj){
    let newObj = {};
    
    return newObj;
}

中间的代码应该如何填充?做到,丢一个obj进去,拷贝一份(newObj),最终返回newObj。

js 复制代码
let obj = {
    name: '张三',
    like: {
        a: 'food'
    }
}

// 手写一个浅拷贝,希望传一个对象进来,能够拷贝一个
function shallowCopy(obj) {
    let newObj = {};
    for (let key in obj) {
        newObj[key] = obj[key];
    }
    return newObj;
}
let obj2 = shallowCopy(obj);
obj.like.a = 'drink';
console.log(obj2);

但是这种方法会把原对象中隐式属性也给遍历到,在实际开发中浅拷贝一般不需要将原型上具有的属性一块拷贝。因此我们需要优化一下代码,规避这个问题

实现原理

  • 借助for in 遍历原对象,将原对象的属性增加在新对象中
  • 因为for in 会遍历到原对象中隐式具有的属性,通常需要使用obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显式具有的
js 复制代码
let obj = {
    name: '张三',
    like: {
        a: 'food'
    }
}

// 手写一个浅拷贝,希望传一个对象进来,能够拷贝一个
function shallowCopy(obj) {
    let newObj = {};
    for (let key in obj) {
        if(obj.hasOwnProperty(key)){
            newObj[key] = obj[key];
        }
        
    }
    return newObj;
}
let obj2 = shallowCopy(obj);
obj.like.a = 'drink';
console.log(obj2);

归根结底:引用类型复制的是引用地址,因此原来的东西变更了,你也得变更,引用地址都是一样的,但是内容改变了。

深拷贝

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

深拷贝手段

  1. JSON.parse(JSON.stringify(obj)) 先将对象序列化为 JSON 字符串,再将其解析回对象,会得到一个与原始对象结构相同但在内存中独立的新对象。
js 复制代码
let obj = {
    name: '张三',
    age: 18,
    like: {
        a: 'coding'
    },
    a: true,
    b: undefined,
    c: null,
    d: Symbol('123'),
    // e: 123n,
    f: function () { }
}

let obj2 = JSON.parse(JSON.stringify(obj))
obj.like.n = 'running'
console.log(obj2);

缺陷:

  • 不能识别BigInt类型
  • 不能拷贝undefined、symbol、function类型的值
  • 不能处理循环引用
  1. structuredClone()
js 复制代码
const user = {
    name: {
        firstName: '牛',
        lastName: '蜗'
    },
    age: 19
}

const newUser = structuredClone(user)
user.name.firstName = 'kk'
console.log(newUser);

手写深拷贝

面试官经典一问:如果js不提供这些方法,你能不能手搓一个方法,实现深拷贝对象?

思考:我们还能像刚刚手写浅拷贝一样那么做吗?

手写浅拷贝,我们会把一个对象的引用地址给复制过去,但是既然是深拷贝,那我们就不能用原来的地址了,不然不就成浅拷贝了?我们需要开辟一块新的地址。

js 复制代码
const user = {
    name: {
        firstName: '牛',
        lastName: '蜗'
    },
    age: 19
}

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 = 'kk'
console.log(newUser);

在这里,我们通过自定义的 deep 函数实现了对复杂对象的深度拷贝。当我们修改原始对象中的属性值时,新拷贝出来的对象 newUser 并不会受到影响。

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

小结

小小总结一下这篇文章,介绍了深浅拷贝的内置方法,以及手写深浅拷贝,理解一下深浅拷贝底层到底为什么一个是浅一个是深。

通过对具体代码示例的分析,我们了解到可以通过自定义函数来处理对象的拷贝,对于属性值为对象的情况进行递归操作,从而确保了数据的独立性。

相关推荐
前端李易安36 分钟前
Web常见的攻击方式及防御方法
前端
PythonFun1 小时前
Python技巧:如何避免数据输入类型错误
前端·python
Neituijunsir1 小时前
2024.09.22 校招 实习 内推 面经
大数据·人工智能·算法·面试·自动驾驶·汽车·求职招聘
hakesashou1 小时前
python交互式命令时如何清除
java·前端·python
天涯学馆1 小时前
Next.js与NextAuth:身份验证实践
前端·javascript·next.js
HEX9CF1 小时前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
ConardLi1 小时前
Chrome:新的滚动捕捉事件助你实现更丝滑的动画效果!
前端·javascript·浏览器
ConardLi2 小时前
安全赋值运算符,新的 JavaScript 提案让你告别 trycatch !
前端·javascript
凌云行者2 小时前
使用rust写一个Web服务器——单线程版本
服务器·前端·rust
华农第一蒟蒻2 小时前
Java中JWT(JSON Web Token)的运用
java·前端·spring boot·json·token