一、拷贝
- 浅拷贝和深拷贝都复制了值和地址,都是为了解决引用类型赋值后互相影响的问题。
- 但是浅拷贝只进行一层复制,深层次的引用类型还是共享内存地址,原对象和拷贝对象还是会互相影响。
- 深拷贝就是无限层级拷贝,深拷贝后的原对象不会和拷贝对象互相影响。
二、浅拷贝
可实现浅拷贝的方式如下:
1. Object.assign
const obj = {
name: 'lin'
}
const newObj = Object.assign({}, obj)
obj.name = 'xxx' // 改变原来的对象
console.log(newObj) // { name: 'lin' } 新对象不变
console.log(obj == newObj) // false 两者指向不同地址
2. 数组的slice和concat方法
const newArr = arr.slice(0)
arr[2] = 'rich' // 改变原来的数组
console.log(newArr) // ['lin', 'is', 'handsome']
console.log(arr == newArr) // false 两者指向不同地址
const arr = ['lin', 'is', 'handsome']
const newArr = [].concat(arr)
arr[2] = 'rich' // 改变原来的数组
console.log(newArr) // ['lin', 'is', 'handsome'] // 新数组不变
console.log(arr == newArr) // false 两者指向不同地址
3. 数组的静态方法 Array.from
const arr = ['lin', 'is', 'handsome']
const newArr = Array.from(arr)
arr[2] = 'rich' // 改变原来的数组
console.log(newArr) // ['lin', 'is', 'handsome']
console.log(arr == newArr) // false 两者指向不同地址
4.扩展运算符
const arr = ['lin', 'is', 'handsome']
const newArr = [...arr]
arr[2] = 'rich' // 改变原来的数组
console.log(newArr) // ['lin', 'is', 'handsome'] // 新数组不变
console.log(arr == newArr) // false 两者指向不同地址
const obj = {
name: 'lin'
}
const newObj = { ...obj }
obj.name = 'xxx' // 改变原来的对象
console.log(newObj) // { name: 'lin' } // 新对象不变
console.log(obj == newObj) // false 两者指向不同地址
5. 循环遍历赋值
function clone (obj) {
const cloneObj = {} // 创建一个新的对象
for (const key in obj) { // 遍历需克隆的对象
cloneObj[key] = obj[key] // 将需要克隆对象的属性依次添加到新对象上
}
return cloneObj
}
三、深拷贝
1. 序列化
JSON.parse(JSON.stringify(obj))
const obj = {
person: {
name: 'lin'
}
}
const newObj = JSON.parse(JSON.stringify(obj))
obj.person.name = 'xxx' // 改变原来的深层对象
console.log(newObj) // { person: { name: 'lin' } } 新的深层对象不变
使用序列化的方式来实现深度克隆有些许弊端;
- 会忽略
undefined,symbol和函数 NaNInfinity-Infinity会被序列化为nullMap序列化返回是空对象{}; 个人认为Map的结构类似键值对,然后value是个函数,因函数的缘故无法序列化
2. 递归实现
要求:
- 支持对象、数组、日期、正则的拷贝。
- 处理原始类型(原始类型直接返回,只有引用类型才有深拷贝这个概念)。
- 处理
Symbol作为键名的情况。 - 处理函数(函数直接返回,拷贝函数没有意义,两个对象使用内存中同一个地址的函数,问题不大)。
- 处理
DOM元素(DOM元素直接返回,拷贝DOM元素没有意义,都是指向页面中同一个)。 - 额外开辟一个储存空间
WeakMap,解决循环引用递归爆栈问题(引入WeakMap的另一个意义,配合垃圾回收机制,防止内存泄漏)。
Reflect.ownKeys()方法返回一个由目标对象(自身)的属性键组成的数组(包括Symbol);它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
function getType(target) {
return Object.prototype.toString.call(target).slice(8, -1)
}
function deepClone(target, hash = new WeakMap()) {
// 处理 原始值 null、undefined、number、string、symbol、bigInt、boolean
if (typeof target !== 'object' || target === null) {
return target
}
// 处理 array
if (Array.isArray(target)) {
return target.map((e) => deepClone(e))
}
// 处理 function
if (getType(target) === 'Function') {
return eval(`(${target.toString()})`).bind(this) // function 声明需要用"("、")"包裹
}
// 拷贝日期
if(getType(target) === 'Date') {
return new Date(target.valueOf())
}
// 拷贝正则
if(getType(target) === 'RegExp') {
return new RegExp(target)
}
// 处理 map
if (getType(target) === 'Map') {
let map = new Map()
target.forEach((v, k) => {
map.set(k, deepClone(v))
})
return map
}
// 处理 set
if (getType(target) === 'Set') {
let set = new Set()
for (let val of target.values()) {
set.add(deepClone(val))
}
return set
}
if (hash.get(target) return hash.get(target) // 当需要拷贝当前对象时,先去存储空间中找,如果有的话直接返回
const cloneTarget = new target.constructor() // 通过target的构造函数创建一个新的与之一样类型的对象,这样写的就不需要判断类型了
hash.set(target, cloneTarget) // 如果存储空间中没有就存进存储空间 hash 里
// 处理 object
if (getType(target) === 'Object') {
Reflect.ownKeys(target).forEach(key => {
cloneTarget[key] = deepClone(target[key], hash) // 递归拷贝每一层
})
return cloneTarget
}
return target
}
3. structuredClone
结构化克隆算法是由HTML5规范定义的用于复制复杂JavaScript对象的算法;H5定义的全局深度克隆方法;
const obj = {
person: {
name: 'lin'
}
}
const newObj = structuredClone(obj) //
obj.person.name = 'xxx' // 改变原来的对象
console.log('原来的对象', obj)
console.log('新的对象', newObj)
console.log('更深层的对象指向不同的地址', obj.person == newObj.person)
缺点:
-
Error以及Function对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出DATA_CLONE_ERR的异常。 -
企图去克隆
DOM节点同样会抛出DATA_CLONE_ERR异常。 -
对象的某些特定参数也不会被保留
RegExp对象的lastIndex字段不会被保留- 属性描述符,
setters以及getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为read-only,它将会被复制为read-write,因为这是默认的情况下。 - 原形链上的属性也不会被追踪以及复制。
-
Symbol除外可复制
最后说一局,推荐使用lodash,lodash的实现是很全面的
感谢您抽出宝贵的时间观看本文;本文是 JavaScript 系列的第 5 篇,后续会持续更新!欢迎关注~