面试官:手写一个浅拷贝和一个深拷贝(拷贝详解)

两个拷贝都是JavaScript中应该必会的知识点,这两个东西基本上面试都会被问到,手写两个拷贝也理应是有手就会,如果你还不清楚拷贝是啥,或者不懂如何手写代码,不妨现在看下去

JavaScript中拷贝一般都是针对引用类型来说,先给大家看下深浅分别是什么样子的

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

这里a赋值到b后,b的值不会受a的变化而变化,传值而非址 ,这里其实也有底层逻辑在里面的,不妨看看我这篇预编译

像这样,一个数据拷贝另一个数据,原数据改变不会影响到另一个数据的拷贝,就是深拷贝,其实很好理解,就是拷贝得很彻底!不会再变了

ini 复制代码
let obj1 = {
	age: 18
}
let obj2 = obj1
obj1.age = 20
console.log(obj2.age) // 20

引用类型在调用栈中存放的是地址,传址而非值,引用类型真正存放在堆中,obj1赋值给obj2是地址,因此obj1和obj2共用一个地址,你改我也改

像这样,一个数据拷贝另一个数据,原数据改变会影响到另一个数据,就是浅拷贝,同理,就是拷贝得不彻底,后面还能变

既然拷贝都是针对引用类型来说,我们从这里就可以看出,普通对象的赋值是一个浅拷贝,那有没有其他手段,也能实现出浅拷贝,下面就开始介绍实现浅拷贝的方法

浅拷贝

下面两个是对象的浅拷贝常见手段

Object.create(x)

上次见这个方法还是创造对象的时候,这个方法创造的对象是一个空对象,并且会隐式继承原对象的属性

举个栗子🌰

css 复制代码
let a = {
	name: '小黑子'
}
let b = Object.create(a)
console.log(b.name) // 小黑子
console.log(b) // {} 

我们展开这个{}

现在看看这个方法的浅拷贝体现

css 复制代码
let a = {
	name: '小黑子'
}
let b = Object.create(a)
a.name = '大黑子'
console.log(b.name) // 大黑子

没有问题,你改我也改

Object.assign({}, x)

Object.assign是用来合并对象的,我们先认识下

举个栗子🌰

css 复制代码
let a = {
	name: '小黑子',
	hobby: {
		n: 'running'
	}
}
let b = {
	age: 18
}
let c = Object.assign(a, b)
console.log(c) // { name: '小黑子', hobby: { n: 'running' }, age: 18 }

我们现在来看下它的浅拷贝体现

css 复制代码
let a = {
	name: '小黑子',
	hobby: {
		n: 'running'
	}
}
let b = Object.assign({}, a)
a.name = '大黑子'
console.log(b.name) // 小黑子 --- WTF???

啊哈!笔者你逗我,说好的浅拷贝呢,别急,我们再来试试

css 复制代码
let a = {
	name: '小黑子',
	hobby: {
		n: 'running'
	}
}
let b = Object.assign({}, a)
a.hobby = {}
console.log(b.hobby) // running --- WTF?????? 

额......怎么回事,这两个栗子都是深拷贝啊!Hold on! 我们再来看一个

css 复制代码
let a = {
	name: '小黑子',
	hobby: {
		n: 'running'
	}
}
let b = Object.assign({}, a)
a.hobby.n = 'coding'
console.log(b.hobby) // coding

这里确实是浅拷贝,那这个方法怎么说,怎么既有浅拷贝又有深拷贝,我们不妨仔细分析下,a对象中有个hobby键,这个键的值又是一个对象,a.hobby.n = 'coding'而这个操作其实就是针对一个引用类型就行修改,一定会随之改变的,也就是说,assign是浅拷贝操作,如果对象的属性是引用类型,则就相当于拷贝了一个引用地址。当然,你也可以说深得不够彻底(前两个),还是浅!

数组也有浅拷贝的手段,下面四个是数组浅拷贝

concat

用于合并数组,返回一个新数组,不会修改原数组

浅拷贝体现🌰

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

没有问题

slice

提取数组的一部分,返回一个新数组,不会修改原数组,参数一是起始下标,参数二是终止下标,左闭右开

浅拷贝体现🌰

ini 复制代码
let arr = [1, 2, 3, {a: 10}]
let newArr = arr.slice(0)
// 只有一个参数0,也就是提取整个数组
arr[3].a = 1
console.log(newArr) // [ 1, 2, 3, { a: 1 } ]

没有问题

打消你的疑惑:

既然都上slice了,我splice为何不可浅拷贝?

splice可以返回值,确实是可以获得一个新的数组,但是你要清楚,我们是来拷贝的,splice会影响原数组,拷贝完了你还把人家给删了,怕不是......

数组解构

[...arr]数组解构是es6新增的方法,解构是从数组中提取元素进行赋值

浅拷贝体现🌰

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

没有问题

arr.toReversed().reverse()

arr.toReversed()方法对应着reverse()都是用来颠倒数组,这个方法比较新,可能大家的node还不认识,可以去浏览器试试。阮一峰老师书中也有介绍这个方法,这里放个链接,请点击。因为reverse会影响原数组,并返回一个新数组,所以我们不用两个reversetoReversed()不影响原数组,所以这个方法刚好可以进行一个拷贝

浅拷贝体现🌰

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

没有问题

for

在手写之前,我们需要认识下for infor of此前文章没有详细讲过

for有三种遍历,一种是普通的for循环,还有两种分别为for infor of

for of

for of是天生给具有迭代器(Iterator)属性的数据结构遍历的,普通对象是没有的,数组有,因此你也可以拿他来遍历数组,今天我们主要是针对对象,所以这个方法我们不用

这个方法我此前文章有讲到过,贴个链接(这个文章没啥流量(悲)

Iterator-Set-Map-WeakSet-弱引用详解 - 掘金 (juejin.cn)

用例🌰

scss 复制代码
let arr = ['a', 'b', 'c', 'd', 'e']
for(let item of arr){
    console.log(item);
} // abcde

for in

for in是专门用来遍历对象的,既然能遍历对象也就能遍历数组。但是对今天的拷贝来说有个缺陷,就是for in甚至可以遍历到隐式具有的属性,我们拷贝不会去拷贝人家隐式的属性

请看下面的栗子🌰

javascript 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    }
}
let newObj = Object.create(obj)
newObj.sex = 'boy'
for(let key in newObj){
    console.log(key);
} // sex name age hobby

如果我们想要手搓一个浅拷贝,必然是要遍历出对象的key的,但是我们不能要人家的隐式东西,解决这个问题,我们只需要判断一下,刚好有个属性就是判断隐式属性的

newObj.hasOwnProperty(key)

该方法返回布尔值,true代表显示,false代表不具有或者隐式

因此我们在for中添加一个判断就可以隔绝掉隐式属性

vbnet 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    }
}
let newObj = Object.create(obj)
newObj.sex = 'boy'
for(let key in newObj){
    if(newObj.hasOwnProperty(key)){
        console.log(key);
    }
} // key

面试官:手搓一个浅拷贝

思路:既然是拷贝,必然是对一个对象进行拷贝,拷贝一般就是拷贝对象和数组,其余不考虑,因此这里必然先对形参判断一下。如果接收的形参是对象就给一个空对象,是数组就先给一个空数组,然后再进行遍历赋值,赋值必然是复制人家的key和value,人家的value如果还是一个引用类型,那刚好就是一个浅拷贝,因为赋的是地址

开始手搓

javascript 复制代码
function shalldowCopy(obj){
    // 只拷贝引用类型
    if(typeof obj !== 'object' || obj == null) return 

    let objCopy = obj instanceof Array ? [] : {}

    for(let key in obj){
        // 不要隐式
        if(obj.hasOwnProperty(key)){
            // objCopy.key是个字符串,[]可以当成变量
            objCopy[key] = obj[key]
        }
    }
    return objCopy
}

试试看🌰

ini 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    }
}
let arr = ['a', {n: 1}, 1, undefined, null]
let newObj = shalldowCopy(obj)
let newArr = shalldowCopy(arr)
obj.age = 20
obj.hobby.type = 'swimming'
arr[0] = 'b'
arr[1].n = 2
console.log(newObj); // { name: '小黑子', age: 18, hobby: { type: 'swimming' } }
console.log(newArr); // [ 'a', { n: 2 }, 1, undefined, null ]

完美!

深拷贝

深拷贝只有一个自带的方法:JSON.parse(JSON.stringify(obj))

JSON.parse(JSON.stringify(obj))

JSON是前后端数据传输的一种数据交互格式,类似对象,它的key都是字符串

JSON.stringify(obj)

这个方法将对象转换成JSON字符串格式

举个栗子🌰

css 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    },
    a: undefined,
    b: null,
    c: function() {},
    d: {
        n: 100
    },
    e: Symbol('hello')
}
console.log(JSON.stringify(obj)); // {"name":"小黑子","age":18,"hobby":{"type":"coding"},"b":null,"d":{"n":100}}

key都变成了字符串

JSON.parse(JSON.stringify(obj))

将JSON格式转换成对象

上面那个栗子🌰

yaml 复制代码
console.log(JSON.parse(JSON.stringify(obj))); 
// 输出如下
{
  name: '小黑子',
  age: 18,
  hobby: { type: 'coding' },
  b: null,
  d: { n: 100 }
}

大家发现没有,这个方法把人家的undefinedfunctionsymbol都吞掉了,起始还有一个bigint也无法展示,bigint会报错

浅拷贝体现🌰

javascript 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    },
    b: null,
    d: {
        n: 100
    }
}
let newObj = JSON.parse(JSON.stringify(obj));
obj.hobby.type = 'running'
console.log(newObj.hobby.type); // coding

没有问题,是深拷贝

循环引用

对象之间存在相互引用的现象

举个栗子🌰

yaml 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    },
    b: null,
    c: function() {},
    d: {
        n: 100
    }
}
obj.c = obj.d
obj.d.n = obj.c
console.log(obj)
// 输出如下
{
  name: '小黑子',
  age: 18,
  hobby: { type: 'coding' },
  b: null,
  c: <ref *1> { n: [Circular *1] }, // 循环
  d: <ref *1> { n: [Circular *1] }  // 循环
}

如果是带有循环引用的对象,我能否对其用JSON.parse(JSON.stringify(obj))进行深拷贝呢?

答案是不行的,会报错

因此这个方法有两个缺陷

  • 无法拷贝undefined,function,symbol,BigInt这几种数据类型
  • 无法处理循环引用

面试官:手搓一个深拷贝

实际上,深拷贝被面试问到的概率更大
思路:既然你对象中的引用类型不能随之修改,碰到这种我们直接再创建一个新的对象就可以了!这里用递归的思想会很巧妙,刚好递归的时候就创建了一个新的引用对象,其余和浅拷贝一样的,当然如果key是原始类型,就是直接赋值

开始手搓

scss 复制代码
function deepCopy(obj){
    let objCopy = {}
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
                //obj[key]原始可以直接用,非原始就操作一下
            if(obj[key] instanceof Object){
                // 引用类型 递归就可以创建一个新的objCopy
                // 右边就是hobby这个对象
                objCopy[key] = deepCopy(obj[key])
            }else{
                // 原始类型
            objCopy[key] = obj[key] 
            }
            // 出口:if进不去就出来了
        }
    }
    return objCopy
}

试试看🌰

ini 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    }
}
let newObj = deepCopy(obj)
obj.hobby.type = 'running'
console.log(newObj.hobby.type); // coding

完美!

如果你传的是数组,你就直接前面进行一个判断就可以,实际上,上面的手搓代码足以让面试官认可你了,如果非要刁钻!非得给你所有的数据类型,我们就用下面的方法,当然JSON.parse(JSON.stringify(obj))深拷贝的两个缺陷你也可以去引用下面两个库去解决

underscore

lodash

上面两个库都可以作为很好的资源去学习源码,像什么拷贝,去重,扁平化各种API都有封装,我们要做的是去实现他的源码


如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]

相关推荐
万叶学编程2 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js
前端李易安4 小时前
Web常见的攻击方式及防御方法
前端
PythonFun4 小时前
Python技巧:如何避免数据输入类型错误
前端·python
Neituijunsir4 小时前
2024.09.22 校招 实习 内推 面经
大数据·人工智能·算法·面试·自动驾驶·汽车·求职招聘
知否技术4 小时前
为什么nodejs成为后端开发者的新宠?
前端·后端·node.js
hakesashou4 小时前
python交互式命令时如何清除
java·前端·python
天涯学馆4 小时前
Next.js与NextAuth:身份验证实践
前端·javascript·next.js
HEX9CF5 小时前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
ConardLi5 小时前
Chrome:新的滚动捕捉事件助你实现更丝滑的动画效果!
前端·javascript·浏览器