前面我写了一篇文章: 实现一个深拷贝函数,如果要拷贝的对象存在循环引用,怎么处理? ,讲述了如何深拷贝一个对象,拷贝函数如下:
function deepClone(obj,map=new Map()) {
if (typeof obj !== 'object' || obj === null) return obj
// 如果已经拷贝过,直接返回即可
if (map.has(obj)) return map.get(obj)
let newObj = Object.prototype.toString.call(obj) === '[object Array]' ? [] : {}
// 以当前对象为键,新对象为值
map.set(obj,newObj)
for (let key in obj) {
// 如果属性值是对象,则需要递归去调用拷贝函数
if (typeof obj[key] === 'object' && obj[key] !== null && obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key],map)
} else {
newObj[key] = obj[key]
}
}
return newObj
}
对于普通对象和数组,我们的深拷贝函数是可以正常工作的,但是对于日期对象和正则表达式对象,我们发现拷贝出来的新对象就不是预期的结果了,我们用如下代码测试一下:
let obj = {
date: new Date('2023-08-05 15:21:21'),
regExp: /^[0-9]+$/
}
let newObj = deepClone(obj)
console.log(newObj)
新对象打印结果如下:
我们发现新对象的中 date 和 regExp 都变成了空对象,原因是 deepClone 函数在执行如下代码时,由于 date 和 regExp 都是对象,所以 newObj 会初始化为空对象 {}
let newObj = Object.prototype.toString.call(obj) === '[object Array]' ? [] : {}
由于 date 和 regExp 是特殊的对象,它们不像普通对象那样拥有自己的 key,所以不会进入 for in 循环来拷贝 key,那么最后只会执行 return newObj 逻辑,直接返回了空对象 {}。所以,对于这两种特殊的对象,我们需要特殊处理。
对于日期对象,我们可以通过获取其时间戳,然后使用新的日期对象来创建一个拷贝,如下:
if (obj instanceof Date) {
const newObj = new Date(obj.getTime())
return newObj
}
上述代码中,我们判断 obj 是否是日期对象,是的话,我们使用 getTime() 方法获取其时间戳,然后创建一个新的日期对象,obj 的时间戳作为参数,最后返回新的日期对象,newObj 就是 obj 的拷贝。
对于正则表达式,我们可以使用构造函数的方式 new RegExp() 来创建一个新的正则表达式对象,把原表达式的 source 属性(正则表达式的文本,即匹配规则,如 /[1-9][a-z]$/) 和 flags 属性(正则表达式的标志位,如 i ,g) 作为参数,代码如下:
if (obj instanceof RegExp) {
const newObj = new RegExp(obj.source, obj.flags)
return newObj
}
如果对 source 和 flags 属性不熟悉的朋友可以到 MDN 上查询 正则表达式
下面我们把之前写的深拷贝函数改造一下:
function initNewObj(obj) {
const objTag = {
objectTag: '[object Object]',
arrayTag: '[object Array]',
dateTag: '[object Date]',
regexpRag: '[object RegExp]'
}
let newObj
const objType = Object.prototype.toString.call(obj)
switch(objType) {
case objTag.objectTag:
newObj = {}
break
case objTag.arrayTag:
newObj = []
break
case objTag.dateTag:
// 创建新的日期对象
newObj = new Date(obj.getTime())
break
case objTag.regexpRag:
// 创建新的正则表达式对象
newObj = new RegExp(obj.source,obj.flags)
break
default:
break
}
return newObj || {}
}
function deepClone(obj,map = new Map()) {
if (typeof obj !== 'object' || obj === null) return obj
if (map.has(obj)) return map.get(obj)
let newObj = initNewObj(obj)
map.set(obj,newObj)
for (let prop in obj) {
if (typeof obj[prop] === 'object' && obj[prop] !== null && obj.hasOwnProperty(prop)) {
newObj[prop] = mydeepClone(obj[prop],map)
} else {
newObj[prop] = obj[prop]
}
}
return newObj
}
上述代码中,我们把之前 deepClone 函数中 newObj 的初始化逻辑抽离到了 initNewObj 函数中,该函数内声明变量 objTag,枚举了几种对象类型,即通过 Object.prototype.toString.call() 方法来判断数据类型的返回值。之后创建新对象 newObj,调用 Object.prototype.toString.call(obj) 得到原对象 obj 的类型,然后在 switch 语句中根据 obj 的类型,把 newObj 初始化为不同的值。
使用如下测试用例测试一下改造后的深拷贝函数:
let obj = {
profile: {
name: 'xiaoming',
age: 25,
hobbies: ['篮球','摄影'],
},
date: new Date('2023-08-05 15:21:21'),
regExp1: /^[0-9]+$/,
regExp2: /^[a-z]+$/i
}
let newObj = deepClone(obj)
console.log(newObj)
打印结果如下,我们看到新对象里的日期对象和正则表达式对象也能正确复制过来。
上述改造过后的深拷贝函数已经可以满足普通对象、数组、日期对象、正则表达式对象的拷贝,但是我们知道,在 javascript 中,万物皆可为对象,比如:new String(‘我是字符串对象’)、new Boolean(true) 等等,那么对于这些对象,我们的拷贝函数就无法正常工作了,我们测试一下:
let obj = {
str: new String('我是字符串对象'),
}
let newObj = deepClone(obj)
console.log(newObj)
上述测试代码结果如下:
上图中,我们拷贝出来的结果是和原对象是不一样的,原因是字符串对象:“我是一个字符串对象”,在进入拷贝函数的 for in 循环的时候,会遍历每个字符,把每个字符的索引作为 key,当执行赋值语句 newObj[prop] = obj[prop],就会得到上述结果。
所以针对上述场景,我们需要在 initNewObj 函数中加上对这种情况的逻辑处理:
function initNewObj(obj) {
const objTag = {
...,
// String 对象标识
stringTag: '[object String]'
}
// 创建该实例对象(obj)的构造函数的引用
const Ctor = obj.constructor
let newObj
const objType = Object.prototype.toString.call(obj)
switch(objType) {
...,
case objTag.stringTag:
// 直接调用 obj 的构造函数创建新对象返回
return new Ctor(obj)
default:
break
}
return newObj || {}
}
在上述代码中,我们添加 String 对象的 tag,然后定义变量 Ctor,把创建对象 obj 的构造函数(obj.constructor)赋值给 Ctor,随后在 switch 语句中添加处理 String 对象的分支,直接调用构造函数,把 obj 作为参数,最后直接返回。这样就是实现了 String 对象的拷贝,拷贝结果如下:
有的朋友可能疑惑 obj.constructor 是什么?我们知道 obj 是一个对象,在 javascript 中,对象的 constructor 属性就是指向创建这个对象的构造函数,我们举几个例子:
const str = new String('test')
const bool = new Boolean(true)
const obj1 = {}
const obj2 = new Object({})
在上述例子中,str 是一个 String 对象,它是通过 new String() 创建的,所以 String() 函数就是 str 的构造函数,同理 Boolean() 函数是 bool 的构造函数。obj1 是通过字面量的形式创建的对象,obj2 是通过 new Object() 创建的,但它们的构造函数都是 Object() 函数。
所以代码 const Ctor = obj.constructor 很关键,它获取了原对象的构造函数,然后通过构造函数创建一个新的对象返回来达到原对象的拷贝目的。
其实我们前面对于日期对象和正则表达式对象的拷贝也是使用了它们对应的构造函数。除了本文提到的几种对象,还有很多对象,如下图是 lodash _deepClone 函数的实现中枚举对象,感兴趣的朋友可以去看看 _deepClone 的源码,看看各种类型的对象是怎么拷贝的。