引言
"能手写一个深拷贝吗?"------这几乎成了前端面试中必问的保留节目。大多数人能写出基础的递归,一部分人能考虑到各种数据类型的兼容,但当面试官微笑着抛出"如果对象循环引用了怎么办?"时,场面往往瞬间安静。深拷贝的深水区,远比想象中更暗流涌动。今天,我们就来彻底揭开 JS 深拷贝的底裤,从递归的底层逻辑讲起,深入探讨各种棘手边界情况,最终破解循环引用的死局,帮你打造一个连面试官都无法挑剔的终极解法。
递归
在聊 JS 中拷贝前我们先来聊聊递归是啥?
递归: 递归就是程序自己调用自己,把大问题拆成相同的小问题,直到遇到终止条件再逐层返回结果。
知道递归是什么之后我们来用代码实现数学中如何求取一个值的阶乘如求取一个 5!。
第一种方法:
ini
function mul(n) {
let res = 1
for(let i = n ; i > 0; i--){
res = res * 5
}
return res
}
我们可以直接进行一个循环,得到 1 * 2 * 3 * 4 * 5 的结果,这样我们就可以算出 5!的值了这个是用循环的方法得到的,但我们要使用递归的方法。
在使用递归之前我们可以先把我们所需要的结果进行拆分一下,我们的目标是 5!,所以就是得到 mul(5)
scss
mul(5)
mul(5)= 5 * mul(4)
mul(4)= 4 * mul(3)
mul(3)= 3 * mul(2)
mul(2)= 2 * mul(1)
mul(1)= 1
知道了这个过程之后我们就可以动动小手敲代码了
第二种方法:
scss
function mul (n) {
if(n == 1) return 1
return n * mul(n-1)
}
递归的过程 :
1. 找公式
2. 找出口
知道了这个过程之后我们就来运用到数学的斐波那契数列中即求第n位数是什么
scss
// 1 1 2 3 5 8 13 ... 斐波那契数列
// 1.找公式: f(n)=f(n-1)+f(n-2)
// 2.找出口: n=2 fb=1 n=1 fb=1
function fb(n){
if(n<3) return 1
return n = fb(n-1)+fb(n-2)
}
拷贝
拷贝:克隆一份原对象,得到一份新对象
浅拷贝
浅拷贝: 克隆得到的新对象,其中的引用类型和原对象指向同一个内存地址。
说到浅拷贝,我们可以直接使用的有几个,如 Object.assign({},obj) , \[\].slice(0) ,Array.from()
接下来我们自己通过写出一个代码来充分了解浅拷贝是什么。
scss
let obj ={
name:'猪猪侠',
age:18,
like:['洗脚','按摩']
}
function shallowCopy(obj) {
let res = Array.isArray(obj) ? [] : {}
for(let key in obj){
if (obj.hasOwnProperty(key)){
res[key]=obj[key]
}
}
return res
}
let oo = shallowCopy(obj)
obj.like[0]='品茶'
console.log(oo);
我们直接看一下输出结果:

输出当中的结果中 like 里面第一个被更改为了 '品茶' ,其实这就是浅拷贝,它能将原始值直接拷贝,但是遇到引用类型的值时只会把引用类型的引用地址拷贝过去,和原对象用的都是同一个引用地址,当你要更改新对象里面包含的引用类型时,原对象里面的值也会被更改,这就是浅拷贝。
深拷贝
深拷贝: 将原对象中的所有的子对象,层层拷贝,引用地址全是全新的。
我们依旧通过代码来实现解释:
vbnet
let obj ={
name:'猪猪侠',
age:18,
like:{
n:'打瓦',
m:'三角洲',
o:{
a:'洗脚'
}
}
}
function deepCopy(obj){
let res = {}
for(let key in obj){
if (obj.hasOwnProperty(key)){
if(typeof obj[key] == 'object' && obj[key] !== null ){
res[key] = deepCopy(obj[key])
}else{ //原始值
res[key]=obj[key]
}
}
}
return res
}
let oo = deepCopy(obj)
obj.like.o.a='按摩'
console.log(oo);
我们还是先看一下输出结果:

我们更改了原对象里面的引用类型里面的值,但是新对象还是跟原对象刚开始的值是一样的,这是因为新对象里面的引用地址全是新的不是和原对象的应用地址是同一个。
注意: 在创建这个函数的时候我们需要用到递归方法,因为这种都是一层套一层的。
在代码中我们用到了 for (key in obj) {} 这个方法是用来遍历引用类型里面的内容 。
最后在补充一下我们现在可以用到的深拷贝的方法:
- JSON.parse(JSON.stringify(obj)) --> 不能处理 BigInt undefined function NaN Infinity symbol
- structuredClone() --> 不能处理 function
总结
从一行简陋的递归,到能从容应对各种奇葩边界与循环引用的终极解法,我们一步步揭开了 JS 深拷贝的神秘底裤。深拷贝的演进过程,其实就是对 JS 数据类型和内存机制不断加深理解的过程。技术没有捷径,唯有死磕底层细节,才能写出真正无懈可击的代码。愿你在未来的开发中,不再被引用的坑绊倒,每一份拷贝都清清楚楚、明明白白。