“浅”拷贝和“深”拷贝

前言

拷贝是指通过一种手段创建了一个新对象,并且新对象和原对象长得一模一样。拷贝通常只针对引用类型 ,有深拷贝和浅拷贝

浅拷贝 -- 引用类型直接复制引用地址

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

常见的浅拷贝方法

  1. Object.create(obj)
  2. Object.assign({}, a)
  3. [].concat(arr)
  4. 数组解构
  5. arr.slice(0)
  6. arr.toReversed().reverse()

实现原理:

  1. 借助for in 遍历原对象,将原对象的属性值增加在新对象中
  2. 因为 for in 会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的属性

深拷贝

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

- JSON.parse(JSON.stringify(obj))

缺陷:

  1. 不能识别BigInt 类型
  2. 不能拷贝 undefined Symbol function 类型的值
  3. 不能处理循环引用

- structuredClone()

实现原理:

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

示例

ini 复制代码
let a = 1
let b = a
a = 2
console.log(b); 

打印结果

分析

let声明的一个变量a的值为1,声明一个变量b将变量a赋给b,此时b的值为1,将a的值改为2对b没有影响。

ini 复制代码
let obj = {
    age: 18
}
let obj2 = obj
obj.age = 20

console.log(obj2);

打印结果

分析 let 声明了一个对象obj,里面放的值为 age: 18let obj2 = obj这一步是将obj的引用地址赋给了obj2(obj和obj2还是一个对象),因为对象可以无限大,所以js引擎在预编译的时候,对象会被存入堆中,而此时obj的值为一个引用地址,指向该对象,obj.age = 20改变对象里面的属性的值但是并没有改变该引用地址,所以obj2的引用地址也没变,因此,obj2里面的值也会随着obj改变。

Object.create(obj)

用于创建一个新对象,并且让新对象隐式继承原对象里面的属性。

分析

此时b是一个空对象,但是b的隐式原型里继承到了a的属性。但是如果把a的属性改变,b也会变化。这是浅拷贝。

Object.assign(obj, obj2)

用于将两个对象合并,将obj2拼到obj上去。

ini 复制代码
let obj = {
    age: 18
}
let obj2 = {
    name: 'lxp'
}
let c = Object.assign(obj, obj2)
console.log(c, obj);

打印结果

分析

a对象的属性也变了,说明此时a对象和c对象是一个对象。

换一种写法:let c = Object.assign({}, obj)相当于c拷贝了obj

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

let c = Object.assign({}, obj)
obj.age = 20
console.log(c, obj);

打印结果

分析 c对象里的age没变,说明是浅拷贝,对吗?

ini 复制代码
let obj = {
    age: 18,
    n: {
        like: 'movie'
    }
}

let c = Object.assign({}, obj)
obj.n.like = 'drink'
console.log(c, obj);

打印结果

分析 c和obj的属性n都改变了,说明属性n拷贝过来的还是n的引用地址。c对象里的属性还是会受原对象obj的影响,所以还是浅拷贝。

[].concat(arr)

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

ini 复制代码
let arr = [1,2,3, {a: 'movie'}]
let newArr = [].concat(arr)

arr[3].a = 'book'

console.log(newArr);

打印结果

分析

newArr还是会受arr的影响,说明还是浅拷贝。

解构

let newArr = []相当于创建了一个新的空数组, ...arr是数组的解构,将数组的元素解构出来,let newArr = [...arr]也相当于是创建了一个新数组和原数组长得一模一样。

ini 复制代码
let arr = [1,2,3, {a: 'movie'}]
let newArr = [...arr]

arr[3].a = 'book'
console.log(newArr);

打印结果

分析

newArr还是会受arr的影响,说明还是浅拷贝。

arr.slice()

截取数组原数组不受影响,接收两个参数,第一个参数是开始截的数组的下标,第二个参数是停止截取的数组的下标(第二参数不写默认是截到底)。左闭右开。

ini 复制代码
let arr = [1, 2, 3, 4, 5]
let arr2 = arr.slice(1, 3)

console.log(arr2, arr);

打印结果

分析

左闭右开,不包含第二个参数的下标。

ini 复制代码
let arr = [1, 2, 3, 4, {a: 'movie'}]
let arr2 = arr.slice(0)
arr[4].a = 'hello'

console.log(arr2, arr);

打印结果

分析

let arr2 = arr.slice(0)截取arr中所有的元素并返回一个新数组arr2,arr2和arr长得一样,arr2还是会受arr的影响,所以还是浅拷贝。

arr.toReversed().reverse()-- toReversed()是一个比较新的方法如果node版本过低会识别不出来

将一个数组反转再反转,arr.toReversed()会将一个数组反转得到一个新数组,将得到的新数组通过reverse()再反转一次,就得到了与arr长得一样的一个新数组。toReversed()方法会创建一个新数组。注:如果node版本过低可以放到谷歌浏览器上运行。

ini 复制代码
let arr = [1, 2, 3, 4, {a: 'movie'}]
let newArr = arr.toReversed().reverse()

arr[4].a = 'book'
console.log(newArr);

打印结果

分析

newArr还是受arr影响,还是浅拷贝。

手写一个浅拷贝方法shallow

实现原理:

  1. 借助for in 遍历原对象,将原对象的属性值增加在新对象中
  2. 因为 for in 会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的属性
vbnet 复制代码
let obj = {
    name: 'lxp',
    like: {
        n: 'coding'
    }
}
function shallow(obj) {
    let newObj = {}
    for(let key in obj){
        newObj[key] = obj[key]
    }
    return newObj
}
console.log(shallow(obj));

打印结果

分析 for循环遍历原对象,newObj[key] = obj[key]这是把obj的属性值赋给newObj相同键名的属性,不能写newObj.key = obj.key因为key是变量。

注:拷贝一般是指拷贝对象上的显示属性,隐式具有的属性是不拷贝的

插播:hasOwnProperty()方法

判断对象中的key是否是隐式具有的,是就返回false,不是就返回true。

javascript 复制代码
Object.prototype.d = 'a'
let obj = {
    a: 'lxp',
    b: {
        n: 'coding'
    }
}

console.log(obj.hasOwnProperty('d'));
console.log(obj.hasOwnProperty('a'));

打印结果

借助hasOwnProperty()方法判断for循环遍历的对象里面是否有隐式具有的属性,有就不赋给newObj。

vbnet 复制代码
let obj = {
    name: 'lxp',
    like: {
        a: 'food'
    }
}

function shallow(obj) {
    let newObj = {}
    for(let key in obj) {
        if(obj.hasOwnProperty(key)){
        newObj[key] = obj[key]
    }}
    return newObj
}
let obj2 = shallow(obj)
obj2.like.a = 'sport'
console.log(shallow(obj));

打印结果

JSON.parse(JSON.stringify(obj))

JSON.stringify(obj)方法是将obj对象转为JSON字符串,再通过JSON.parse()将JSON字符串转为对象。

javascript 复制代码
let obj = {
    name: 'lxp',
    age: 18,
    like: {
        n: 'music'
    },
    a: true,
    b: undefined,
    c: null,
    d: Symbol(1),
    f: function() {},
    e: 123n
}

let obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj2);

打印结果

分析 JSON.parse(JSON.stringify(obj))方法无法识别BigInt类型

javascript 复制代码
let obj = {
    name: 'lxp',
    age: 18,
    like: {
        n: 'music'
    },
    a: true,
    b: undefined,
    c: null,
    d: Symbol(1),
    f: function() {}
}

let obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj2);

打印结果

分析 不能拷贝 undefined Symbol function 类型的值

javascript 复制代码
let obj = {
    name: 'lxp',
    age: 18,
    like: {
        n: 'music'
    },
    a: true,
    b: undefined,
    c: null,
    d: Symbol(1),
    f: function() {},
    e: 123n
}
obj.c = obj.like
obj.like.n = obj.c
let obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj2);

打印结果

分析 obj.c = obj.like; obj.like.n = obj.c循环引用,JSON.parse(JSON.stringify(obj))不能处理循环引用。

javascript 复制代码
let obj = {
    name: 'lxp',
    age: 18,
    like: {
        n: 'music'
    },
    a: true,
    c: null
}
let obj2 = JSON.parse(JSON.stringify(obj))

obj.like.n = 'majiang'
console.log(obj2);

打印结果

分析 obj不能影响obj2,实现了深拷贝。

structuredClone()

由js官方打造的实现深拷贝的方法,很新,node版本过低也运行不出来,放到谷歌浏览器运行。

ini 复制代码
const user = {
    name: {
        firstname: 'L',
        lastname: 'xp'
    },
    age: 18
}

const newUser = structuredClone(user)
user.name.firstname = 'Lee'
console.log(newUser)

运行结果

假如面试官让你自己手搓一个深拷贝方法deep()

实现原理:

  1. 借助for in 遍历原对象,将原对象的属性值增加在新对象中
  2. 因为 for in 会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的属性
  3. 如果遍历到的属性值是原始值类型,直接往新对象中赋值,如果是引用类型,递归创建新的子对象
ini 复制代码
const user = {
    name: {
        firstname: 'L',
        lastname: 'xp'
    },
    age: 18
}

function deep(obj) {
    let newObj = {}
    for(let key in obj) {
        if (obj.hasOwnProperty(key)){
            if(obj[key] instanceof Object) {  // obj[key] == 'object' && obj[key] !== null  obj[key] 是不是对象
                newObj[key] = deep(obj[key]);
            } else{
                newObj[key] = obj[key]
            }
        }
    }
    return newObj
}
const newobj = deep(user)
user.name.firstname = 'zhangsan'
console.log(newobj);

打印结果

结语

内容比较多,自己梳理一下记得更牢。

相关推荐
NiNg_1_23415 分钟前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河17 分钟前
CSS总结
前端·css
BigYe程普39 分钟前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H1 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍1 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai1 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默1 小时前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_857297911 小时前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_2 小时前
meta标签作用/SEO优化
前端·javascript·html
Ink2 小时前
从底层看 path.resolve 实现
前端·node.js