对象赋值关系
| 分类 | 说明 |
|---|---|
| 引用赋值 | 指向同一个对象,相互之间会影响 |
| 浅拷贝 | 只是浅层的拷贝,内部引入对象时,依然会相互影响 |
| 深拷贝 | 两个对象不再有任何关系,不会相互影响 |
引用赋值
const user1 = {
name: 'Klaus',
friend: {
name: 'Alex'
}
}
const user2 = user1
user1.name = 'Steven'
console.log(user2.name) // => Steven
浅拷贝
浅拷贝实现主要有两种方式:
- 展开运算符
- Object.assign方法
const user1 = {
name: 'Klaus',
friend: {
name: 'Alex'
}
}
const user2 = { ...user1 }
user1.name = 'Steven'
console.log(user2.name) // => Klaus
user1.friend.name = 'Jhon'
console.log(user2.friend.name) // => Jhon
const user1 = {
name: 'Klaus',
friend: {
name: 'Alex'
}
}
// Object.assign将参数2 和 参数1 进行合并
// 如果属性在参数1和参数2中 同时存在,则参数2的属性值会覆盖参数1中的属性值
// 如果属性在参数1中存在,参数2中不存在,那么依旧会存在于返回的新对象中
// 如果属性在参数2中存在,参数1中不存在,那么会被添加到新返回的对象中
// 所以在Object.assign方法中,如果参数1的值为空对象的时候
// 返回的新对象 其实 就是参数2的 浅拷贝对象
const user2 = Object.assign({}, user1)
console.log(user2)
user1.name = 'Steven'
console.log(user2.name) // => Klaus
user1.friend.name = 'Jhon'
console.log(user2.friend.name) // => Jhon
深拷贝
const user1 = {
name: 'Klaus',
friend: {
name: 'Alex'
}
}
// 可以使用JSON.parse和JSON.stringify方法实现对象的深拷贝
const user2 = JSON.parse(JSON.stringify(user1))
user1.name = 'Peter'
user1.friend.nam = 'Alice'
console.log(user2) // => { name: 'Klaus', friend: { name: 'Alex' } }
但是默认情况下,JS并没有实现对象深拷贝方法,使用JSON来实现深拷贝只是一种尽可能的模拟实现
如果对象中存在方法,Symbol值,或 undefined,使用JSON来进行深拷贝的时候,就会显得无能为力
因为JSON不支持undefined,Symbol值 和 方法
const user1 = {
name: 'Klaus',
friend: {
name: 'Alex'
},
running() {
console.log('running')
},
[Symbol()]: 'symbol',
address: undefined
}
const user2 = JSON.parse(JSON.stringify(user1))
// JSON不支持方法,undefined 和 symbol类型值
console.log(user2) // => { name: 'Klaus', friend: { name: 'Alex' } }
此外, 如果对象中存在循环引用,例如对象自身有一个属性的属性值指向自己,那么JSON就无法将其转换为对应的字符串
自然也就没有办法使用JSON来实现对象的深拷贝了
const user1 = {
name: 'Klaus',
friend: {
name: 'Alex'
}
}
user1.user = user1
const user2 = JSON.stringify(user1) // error
console.log(user2)
所以,如果我们需要实现真正意义上的深拷贝,我们需要自己手动进行编写,或使用lodash/underscore等第三方库
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>深拷贝</title>
</head>
<body>
<input type="text" id="foo">
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<script src="./index.js">
const user1 = {
name: 'Klaus',
friend: {
name: 'Alex'
},
running() {
console.log('running')
},
[Symbol()]: 'symbol',
address: undefined
}
user1.user = user1
const user2 = _.cloneDeep(user1)
console.log(user2)
</script>
</body>
</html>
手动实现深拷贝
基本实现
const user1 = {
name: 'Klaus',
friend: {
name: 'Alex'
}
}
// 判断参数是基本数据类型 还是 复杂数据类型
function isObject(param) {
return (param !== null) && ['function', 'object'].includes(typeof param)
}
function deepClone(origin) {
// 如果是基本数据类型,那么就直接返回基本数据类型
if (!isObject(origin)) return origin
// 判断对象是否是数组,并进行对应的初始化操作
const obj = Array.isArray(origin) ? [] : {}
// 使用递归依次进行拷贝
for (const key in origin) {
obj[key] = deepClone(origin[key])
}
return obj
}
const user2 = deepClone(user1)
console.log(user2)
其它数据类型处理
在实际深拷贝的过程中,可能会遇到set, map,symbol等多种其它数据类型值,此时就需要对于他们进行特殊处理
const user1 = {
name: 'Klaus',
friend: {
name: 'Alex'
},
running() {
console.log('running')
},
set: new Set([1, 2, 3]),
map: new Map([['key1', 'value1'], ['key2', 'value2']]),
symbol: Symbol('symbol属性值'),
[Symbol('s1')]: 'key值为symbol的属性值'
}
function isObject(property) {
return (property !== null) && ['function', 'object'].includes(typeof property)
}
function deepClone(origin) {
// 如果属性值为Symbol的时候,单独新建一个Symbol值
if (typeof origin === 'symbol') {
// 如果origin.description不存在,origin.description获取的值即为undefined
// 以undefined作为Symbol的描述符去创建Symbol的时候,描述符会自动忽略,最终输出值为 Symbol()
return Symbol(origin.description)
}
// 基本数据类型 直接返回
if (!isObject(origin)) return origin
// 函数类型的主要功能是用于执行,对于函数而言没有必要因为深拷贝而在内存中创建一个新的函数
// 所以如果属性值是函数,直接返回对应的函数对象即可
if (typeof origin === 'function') {
return origin
}
// 如果是Set类型 单独处理
if (origin instanceof Set) {
const set = new Set()
// set是可迭代对象,只能通过for-of来获取其中的值
// set是不可以使用for-in遍历的
for (const value of origin) {
set.add(value)
}
return set
}
// 如果是Map类型 单独处理
if (origin instanceof Map) {
const map = new Map()
for(const [key, value] of origin) {
map.set(key, value)
}
return map
}
const obj = Array.isArray(origin) ? [] : {}
// 默认情况下for-in只能迭代自身和自身原型链上可迭代的非Symbol属性
for (const key in origin) {
obj[key] = deepClone(origin[key])
}
// 对Symbol属性值单独迭代
for (const sym of Object.getOwnPropertySymbols(origin)) {
obj[Symbol(sym.description)] = deepClone(origin[sym])
}
return obj
}
const user2 = deepClone(user1)
console.log(user2)
循环引用
上述的示例代码是没有解决循环引用问题的,如果在上述示例代码中存在循环引用,那么就会出现无限递归的情况
所以我们需要对上述代码进行如下改进:
const user1 = {
name: 'Klaus',
friend: {
name: 'Alex'
}
}
// 产生循环引用
user1.self = user1
function isObject(property) {
return (property !== null) && ['function', 'object'].includes(typeof property)
}
// 在deepClone的时候传入一个WeakMap实例
// 调用者调用deepClone方法的时候, 不需要传递第二个参数, 使用默认生成的Weakmap实例即可
// 在deepClone内部调用的时候,手动传入之前初始化生成的那个WeakMap实例,以确保deepClone方法
// 内部使用的WeakMap实例都是同一个
function deepClone(origin, map = new WeakMap()) {
if (typeof origin === 'symbol') {
return Symbol(origin.description)
}
if (!isObject(origin)) return origin
if (typeof origin === 'function') {
return origin
}
if (origin instanceof Set) {
const set = new Set()
for (const value of origin) {
set.add(value)
}
return set
}
if (origin instanceof Map) {
const map = new Map()
for(const [key, value] of origin) {
map.set(key, value)
}
return map
}
// 在创建新对象的时候,先去map上查找
// 如果存在对应的对象就直接返回之前所创建的那个对象
// 而不用自己再新建一个新的对象
if (map.has(origin)) {
return map.get(origin)
}
const obj = Array.isArray(origin) ? [] : {}
// 每创建一个新对象,就在map上挂载一下
map.set(origin, obj)
for (const key in origin) {
obj[key] = deepClone(origin[key], map)
}
for (const sym of Object.getOwnPropertySymbols(origin)) {
obj[Symbol(sym.description)] = deepClone(origin[sym], map)
}
return obj
}
const user2 = deepClone(user1)
console.log(user2)
console.log(user2.self === user2) // => true