js-如何实现深浅拷贝?

前言

深浅拷贝在实战开发中被经常使用,同时也是许多大厂常年面试的考点。尤其是深拷贝,因为目前javascript还没有一个api能完美地实现深拷贝,所以面试时经常会以此作为一道手写题实现深拷贝,考验面试者的实际能力。为此我总结了关于深浅拷贝的知识点,希望可以帮助到大家。

引:在js中,基本数据类型是保存在栈当中,而引用类型是存储在堆当中(把指向堆的指针放在栈中),所以深拷贝和浅拷贝通常只针对引用数据类型

浅拷贝

浅拷贝,即对原始对象的属性进行拷贝,如果是原始类型就直接拷贝原始类型的值,如果是引用类型,则拷贝的是该引用类型存放在堆中的引用地址,所以拷贝得到的对象会受原对象的影响

实现方法

浅拷贝是一种比较常见的拷贝方式,包括平时对象的直接赋值也是浅拷贝,我总结了引用类型常用的几种浅拷贝的方法:

scss 复制代码
1. 引用类型的赋值也可以称之为浅拷贝
2. Object.create()
3. slice()
4. concat()

1> 直接赋值

在平时的开发中,我们通常会用直接赋值的方法对对象或者数组进行拷贝,这样做是把原始对象的引用地址直接赋值给新的对象,那么新的和旧的对象都指向同一个存放在堆内的对象,他们各自对对象内的属性进行修改时,另一个对象也会受到影响,所以是浅拷贝

css 复制代码
let a = {name:'张三',age:18}
let b = a
console.log(b); //{ name: '张三', age: 18 }
a.name = '李四'
console.log(b); //{ name: '李四', age: 18 }

2> Object.create()

Object.create()是创建一个新对象的方法,它可以接收一个对象,让新的对象继承到这个参数对象内的全部属性,这些继承的属性只在新对象的隐式原型中,新的对象本身是一个空对象,但是依然能够访问到,且新的对象也会受到原对象修改的影响,所以是浅拷贝

css 复制代码
let a = {name:'张三',age:18}
let b = Object.create(a)
console.log(b); //{}
console.log(b.age); //18
a.age = 20
console.log(b.age); //20

3> slice()

slice()本身是数组的一个切片方法,可以接收两个参数,第一个参数表示从第几位开始切,第二个参数表示切到第几位,返回包含切割出来的元素的数组,不影响原数组。当不接受参数时,表示完整切割原数组,返回一个新的数组,也可以作为拷贝方法。当原数组中原始类型元素改变时,新数组中的该元素不会改变,但是当数组中引用类型元素的属性改变时,新数组中的该元素的该属性也会被改变,所以这个方法仍然是浅拷贝

sql 复制代码
let arr = [{n:'old'},1,true,null,undefined] 
let newArr = arr.slice()
console.log(newArr); //[ { n: 'old' }, 1, true, null, undefined ]
arr[0].n = 'new'
arr[1] = 2 //修改的是原数组的原始类型元素,所以不影响新数组中该元素
console.log(arr); //[ { n: 'new' }, 2, true, null, undefined ]
console.log(newArr); //[ { n: 'new' }, 1, true, null, undefined ]

4> concat()

concat()本身是数组的拼接方法,可以将两个数组拼接在一起并返回新的数组,不影响原数组。当一个数组调用concat()并传入另一个数组作为参数,那么返回这两个数组拼接后的新数组。若没有传入另一个数组,那么就是创建一个新的数组等于调用该方法的数组,达到拷贝的效果。同样,当原数组中原始类型元素改变时,新数组中的该元素不会改变,但是当数组中引用类型元素的属性改变时,新数组中的该元素的该属性也会被改变,所以这个方法也是浅拷贝

sql 复制代码
let arr = [{n:'old'},1,true,null,undefined] 
let newArr = arr.concat()
console.log(newArr); //[ { n: 'old' }, 1, true, null, undefined ]
arr[0].n = 'new'
arr[1] = 2 //修改的是原数组的原始类型元素,所以不影响新数组中该元素
console.log(arr); //[ { n: 'new' }, 2, true, null, undefined ]
console.log(newArr); //[ { n: 'new' }, 1, true, null, undefined ]

手写实现

通常情况下,面试官不会要求我们手写浅拷贝,因为包括赋值等很多简单的方法就可以实现浅拷贝,但是我们依然应该知道如何手写完成浅拷贝:

实现思路:

  1. 首先我们要判断拷贝的对象是否为引用类型,若不是则直接返回该原对象
  2. 然后需要创建一个新的引用类型,这需要根据拷贝的对象是数组还是对象来定义新的空数组或者空对象
  3. 之后就是遍历原对象,并将原对象的每个属性都赋值给新对象的该属性,因为是浅拷贝,所以若原对象属性值是引用类型,也是直接赋值拷贝
  4. 最后返回拷贝后的新对象

源代码:

javascript 复制代码
function shallowCopy(obj){
    //只考虑对象
    if(typeof obj !== 'object' || obj === null) return obj
    let newObj = obj instanceof Array ? []:{}

    for(let key in obj){
        newObj[key] = obj[key]
    }

    return newObj
}

let obj = {
    name:'lin',
    age:18,
    like:{
        type:'coding'
    }
}
let newObj = shallowCopy(obj)
console.log(newObj); //{ name: 'lin', age: 18, like: { type: 'coding' } }
obj.age = 19
obj.like.type = 'running'
console.log(obj); //{ name: 'lin', age: 19, like: { type: 'running' } }
console.log(newObj); //{ name: 'lin', age: 18, like: { type: 'running' } }

深拷贝

深拷贝同样对原对象的每个属性都进行拷贝,如果原对象属性值有引用类型,那么仍然需要拷贝该引用类型中的所有属性,而不是只拷贝其引用地址,这样拷贝得到的新对象不受原对象属性值修改的影响,所以是深拷贝

实现方法:

JSON.parse(JSON.stringify())

这是js自带方法,将引用类型转换成字符串,再将字符串转换成json格式赋值给新对象,这样得到的新的对象的属性不会受到原对象属性修改的影响,所以是深拷贝

css 复制代码
let foo = 'hello'
let obj = {//对象里面,只能用字符串作为key
    1:1 ,  //1被转成字符串作为key
    [foo]:'world',
    a:undefined,
    b:function(){},
    2:[1,2,3],
    c:{},
    d:{
        n:100
    },
    e:Symbol('hello')
}
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj); //{ '1': 1, '2': [ 1, 2, 3 ], hello: 'world', c: {}, d: { n: 100 } }

obj[1] = 2
obj.d.n = 99
console.log(obj); //{'1': 2,'2': [ 1, 2, 3 ],hello: 'world',a: undefined,b: [Function: b],c: {},d: { n: 99 },e: Symbol(hello)}
console.log(newObj); //{ '1': 1, '2': [ 1, 2, 3 ], hello: 'world', c: {}, d: { n: 100 } }

通过上面,我们知道JSON.parse(JSON.stringify())确实实现了深拷贝,拷贝后修改原对象引用类型的属性时,新的对象没有受到影响,但是也不难发现,JSON.parse(JSON.stringify())实现深拷贝有几个缺点

  • 不能拷贝undefined
  • 不能拷贝函数
  • 不能拷贝Symbol类型
  • 无法处理循环地情况 (上面代码没有体现这点,因为写入循环引用执行会报错,下面会详细解释)

手写实现

在面试中,经常遇到被要求手写实现深拷贝,其实和浅拷贝的实现思路差不多。需要注意的是,对于原对象中属性为引用类型的,要继续拷贝该引用类型中的所有属性,那么我们就可以使用递归,遇到引用类型就再次调用深拷贝方法拷贝该引用类型。同时,这样也避免了使用上一种方法不能拷贝一些数据类型的缺点

css 复制代码
//深拷贝
function deepCopy(obj){
    if(typeof obj !== 'object' || obj ===null) return obj
    let newObj = obj instanceof Array ? [] : {}
    for(let key in obj){
        //obj[key]是原始类型才赋值
        if(typeof obj[key] === 'object' && obj[key] !== null){
            //再创建一个新的对象
            newObj[key] = deepCopy(obj[key])
        }else{
            newObj[key] = obj[key]
        }
    }
    return newObj
}

let obj = {//对象里面,只能用字符串作为key
    1:1 ,  //1被转成字符串作为key
    a:undefined,
    b:function(){},
    2:[1,2,3],
    c:{},
    d:{
        n:100
    },
    e:Symbol('hello')
}
// obj.c = obj.d
// obj.d.m = obj.c


let newObj = deepCopy(obj)
console.log(newObj); //{'1': 1,'2': [ 1, 2, 3 ],a: undefined,b: [Function: b],c: {},d: { n: 100 },e: Symbol(hello)}
obj.d.n = 99
console.log(obj); //{'1': 1,'2': [ 1, 2, 3 ],a: undefined,b: [Function: b],c: {},d: { n: 99 },e: Symbol(hello)}
console.log(newObj); //{'1': 1,'2': [ 1, 2, 3 ],a: undefined,b: [Function: b],c: {},d: { n: 100 },e: Symbol(hello)}

循环引用

循环引用,就是对象的属性引用已经存在的属性,包括引用自身

ini 复制代码
let foo = 'hello'
let obj = {//对象里面,只能用字符串作为key
    1:1 ,//1被转成字符串作为key
    [foo]:'world',
    a:undefined,
    b:function(){},
    2:[1,2,3],
    c:{},
    d:{
        n:100
    },
    e:Symbol('hello')
}
obj.c = obj.d
obj.d.m = obj.c
console.log(obj);

遇到循环引用引用类型时,上面的手写方法仍然不能处理,那么我们可以对上面的代码进行优化。可以使用 Map() 对象,利用它的get()和set(),进行循环引用的拷贝。

实现过程:

  1. 刚开始深拷贝时,创建一个map = new Map()对象
  2. 判断深拷贝的对象是否为引用类型,不是则直接返回该对象
  3. 通过深拷贝对象是数组或者对象,创建一个空数组或空对象,作为深拷贝得到的新对象
  4. 再判断map对象中是否存在要深拷贝的对象,若存在,则直接将map中的该对象属性直接返回,若不存在,则将该对象设置为map对象的属性(存入map)
  5. 然后开始遍历深拷贝的对象,若属性不是引用类型,则直接将该属性拷贝给新对象,若是引用类型,再次调用自身深拷贝方法,将该属性以及map对象作为参数,形成递归
  6. 这样在每次遇到引用类型就进行深拷贝,判断map中是否之前出现了该引用类型,若出现,就直接返回该引用类型,并将深拷贝结果返回,赋值给新对象对应的属性
  7. 最后返回深拷贝的结果
javascript 复制代码
//深拷贝
function deepCopy(obj,map = new Map()){
    if(typeof obj !== 'object' || obj ===null) return obj
    let newObj = obj instanceof Array ? [] : {}
    if(map.get(obj)){
        return map.get(obj)
    }
    map.set(obj,newObj)
    for(let key in obj){
        //obj[key]是原始类型才赋值
        if(typeof obj[key] === 'object' && obj[key] !== null){
            //再创建一个新的对象
            newObj[key] = deepCopy(obj[key],map)
        }else{
            newObj[key] = obj[key]
        }
    }
    return newObj
}

let obj = {//对象里面,只能用字符串作为key
    1:1 ,//1被转成字符串作为key
    a:undefined,
    b:function(){},
    2:[1,2,3],
    c:{},
    d:{
        n:100
    },
    e:Symbol('hello')
}
obj.c = obj.d
obj.d.m = obj.c

let newObj = deepCopy(obj)
console.log(newObj); 
obj.d.n = 99
console.log(obj); 
console.log(newObj); 

总结

我对深浅拷贝的总结就是这些,希望对大家有所帮助,在面试中深浅拷贝还是比较重要的,只要理解了,那就不是问题

相关推荐
hackeroink1 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
LCG元2 小时前
【面试问题】JIT 是什么?和 JVM 什么关系?
面试·职场和发展
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235245 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240256 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar6 小时前
纯前端实现更新检测
开发语言·前端·javascript