1.Object.assign和扩展操作符实现对象浅拷贝
1.1 Object.assign拷贝效果
Object.assign(target, …sources)设target为目标对象,sources为源对象。
- 复制自有可枚举属性
- 浅复制
- 复制符号属性
- 同名属性覆盖
let obj = Object.create({ name: "Danny" }) // 1.复制自有属性
Object.defineProperty(obj, "grade", { // 1.复制可枚举属性
enumerable: false, // 设置成绩属性不可被枚举
value: 100
})
obj.school = { // 2.浅复制
name: "SDU"
}
obj[Symbol("Danny")] = "79707536" // 3.复制符号属性
obj.gender = "man" // 4.同名属性覆盖
let test = {
gender: "woman"
}
Object.assign(test, obj)
console.log(test) // { gender: 'man', school: { name: 'SDU' }, [Symbol(Danny)]: '79707536' }
obj.school.name = "PKU"
console.log(test) // { gender: 'man', school: { name: 'PKU' }, [Symbol(Danny)]: '79707536' }
-
访问器方法复制:
- 综述:源对象的访问器方法不会被复制到目标对象上
- 目标对象有访问器属性,源对象同名属性是普通属性,在复制时会把源对象同名属性值传给目标对象set方法调用
- 目标对象有访问器属性,源对象同名属性是访问器属性,在复制时会把源对象的get方法值传给目标对象set方法调用
- 目标对象没有访问器属性,源对象同名属性是访问器属性,在复制时会把源对象的get方法值赋给目标对象的属性进行覆盖
// 情况一:目标对象的属性是访问器属性,源对象的同名属性为普通属性
// 结果是复制时,把源对象的同名属性值传给目标对象的set方法调用。复制后目标对象的访问器属性未发生变化。
let obj = {
gender: "man",
genderFunc: null
}
let test = {
gender: "woman",
get genderFunc() {
console.log("调用了目标对象的get")
return this.gender
},
set genderFunc(val) {
console.log("调用了目标对象的set")
this.gender = val
}
}
Object.assign(test, obj)
console.log(test)
// 调用了目标对象的set
// { gender: null, genderFunc: [Getter/Setter] }
console.log(test.genderFunc)
// 调用了目标对象的get
// null
// 情况二:目标对象的属性是访问器属性,源对象的同名属性为访问器属性
// 结果是复制时,把源对象的同名属性的get方法的值传给目标对象的set方法调用。复制后目标对象的访问器属性未发生变化。
let obj = {
gender: "man",
get genderFunc() {
console.log("调用了源对象的get")
return this.gender
},
set genderFunc(val) {
this.gender = val
}
}
let test = {
gender: "woman",
get genderFunc() {
console.log("调用了目标对象的get")
return this.gender
},
set genderFunc(val) {
console.log("调用了目标对象的set")
this.gender = val
}
}
Object.assign(test, obj)
console.log(test)
// 调用了源对象的get
// 调用了目标对象的set
// { gender: 'man', genderFunc: [Getter/Setter] }
console.log(test.genderFunc)
// 调用了目标对象的get
// man
// 情况三:目标对象的属性是普通属性,源对象的同名属性为访问器属性
// 结果是复制时,把源对象的同名属性的get方法的值赋给目标对象的属性值。
let obj = {
gender: "man",
get genderFunc() {
console.log("调用了源对象的get")
return this.gender
},
set genderFunc(val) {
this.gender = val
}
}
let test = {
gender: "woman",
genderFunc: "test"
}
Object.assign(test, obj)
console.log(test)
// 调用了源对象的get
// { gender: 'man', genderFunc: 'man' }
1.2 扩展操作符拷贝效果
扩展操作符…复制时前四点和Object.assign相同
-
复制自有可枚举属性
-
浅复制
-
复制符号属性
-
同名属性覆盖
-
访问器属性赋值:
- 综述:源对象上的同名属性会以属性值的形式直接覆盖目标对象的访问器属性;源对象的访问器属性不会复制到目标对象上(同Object.assign)
- 目标对象有访问器属性,源对象同名属性是普通属性,在复制时会把源对象的同名属性直接覆盖访问器属性赋给目标对象
- 目标对象有访问器属性,源对象同名属性是访问器属性,在复制时会把源对象的get方法的值直接覆盖访问器属性赋给目标对象
- 目标对象有普通属性,源对象同名属性是访问器属性,在复制时会把源对象的get方法的值直接覆盖赋给目标对象
// 情况一:目标对象的属性是访问器属性,源对象的同名属性为普通属性
// 结果是复制时,把源对象的同名属性值直接赋给目标对象的属性,直接覆盖
let obj = {
gender: "man",
genderFunc: "Danny"
}
let test = {
gender: "woman",
get genderFunc() {
return this.gender
},
set genderFunc(val) {
console.log("调用了目标对象的set")
this.gender = val
}
}
test = {...test, ...obj}
console.log(test)
// { gender: 'man', genderFunc: 'Danny' }
// 情况二:目标对象的属性是访问器属性,源对象的同名属性为访问器属性
// 结果是复制时,把源对象的同名属性的get方法的值赋给目标对象的属性值,直接进行覆盖。
let obj = {
gender: "man",
get genderFunc() {
console.log("调用了源对象的get")
return this.gender
},
set genderFunc(val) {
this.gender = val
}
}
let test = {
gender: "woman",
get genderFunc() {
return this.gender
},
set genderFunc(val) {
console.log("调用了目标对象的set")
this.gender = val
}
}
test = {...test, ...obj}
console.log(test)
// 调用了源对象的get
// { gender: 'man', genderFunc: 'man' }
// 情况三:目标对象的属性是普通属性,源对象的同名属性为访问器属性
// 结果是复制时,把源对象的同名属性的get方法的值赋给目标对象的属性值。
let obj = {
gender: "man",
get genderFunc() {
console.log("调用了源对象的get")
return this.gender
},
set genderFunc(val) {
this.gender = val
}
}
let test = {
gender: "woman",
genderFunc: "test"
}
test = {...test, ...obj}
console.log(test)
// 调用了源对象的get
// { gender: 'man', genderFunc: 'man' }
2.实现对象浅拷贝
2.1 浅拷贝相关的对象方法
- Object.keys():枚举对象的自有可枚举属性,不包括符号属性
- Object.getOwnPropertyNames():枚举对象的自有属性,不包括符号属性
- Object.getOwnPropertySymbols():枚举对象的自有符号属性
- Reflect.ownKeys():枚举对象的所有属性,包括不可枚举属性,继承属性,符号属性
- Object.getOwnPropertyDescriptor():获取对象属性的特性,包括可枚举性,可配置等等
- Object.defineProperty(obj, property, {}):对象定义属性,并定义属性的特性
- obj.hasOwnProperty§:用于判断某个属性是否是对象的自有属性
☆ 下面用到的遍历对象自有可枚举属性的方法:先通过Object.entries()遍历自有可枚举非Symbol,再用Object.getOwnPropertySymbols()和描述符判断遍历自有可枚举Symbol。
2.2 实现单个对象浅拷贝
-
需求是需要一个浅拷贝函数,输入一个对象,输出它的浅拷贝值。
-
具体实现步骤如下:
- (1) 参数装箱
- (2) 参数类型判断
- (3) 复制非符号自有可枚举属性(访问器属性处理默认按照Object.assign规则)
- (4) 复制符号自有可枚举属性
function copy(obj) {
// 如果obj不是引用类型,那么进行装箱操作
obj = obj instanceof Object ? obj : Object(obj)
// 初始化浅拷贝目标对象和枚举键
let target = null, key
// 实现最基础的常见对象类型:”数组,函数和非JavaScript特殊内置对象“的浅拷贝
if(obj instanceof Array)
target = []
else if(obj instanceof Function)
// 单个函数对象的浅复制没有明确实际意义,这里返回一个相同的引用
return obj
else
target = new Object
// 将自有可枚举属性复制到浅拷贝目标对象,不包括Symbol
for([key, target[key]] of Object.entries(obj));
// 将自有可枚举的Symbol属性复制到浅拷贝目标对象
for(let p of Object.getOwnPropertySymbols(obj))
if(Object.getOwnPropertyDescriptor(obj, p).enumerable)
target[p] = obj[p]
return target
}
2.3 实现Object.assign()
有了单个对象浅复制的实现,其实Object.assign()的实现就是把多个对象的浅复制结合在一起。
具体实现步骤如下:
- (1) 所有参数装箱
- (2) 源对象的非符号自有可枚举属性复制到目标对象(访问器属性处理默认按照Object.assign规则)
- (3) 源对象的符号自有可枚举属性复制到目标对象
Object.$assign = function(target, ...sources) {
// 处理参数,如果参数不是引用类型,那么进行装箱
target = target instanceof Object ? target : Object(target)
sources = sources.map(el => el instanceof Object ? el : Object(el))
// 下面操作类似于单个对象的浅拷贝
let key
sources.forEach(obj => {
// 自有可枚举属性不包括符号属性复制到目标对象target
for([key, target[key]] of Object.entries(obj));
// 自有可枚举符号属性复制到目标对象target
for(let p of Object.getOwnPropertySymbols(obj))
if(Object.getOwnPropertyDescriptor(obj, p).enumerable)
target[p] = obj[p]
})
return target
}
2.4 实现扩展操作符合并对象
扩展操作符合并对象和Object.assign和并对象类似,只不过区别在于扩展操作符处理后返回一个新对象,扩展操作符会以值的形式覆盖目标对象的访问器属性。
具体实现步骤如下:
- (1) 所有参数装箱
- (2) 创建一个新对象
- (3) 目标对象的自有可枚举属性,复制时定义属性特性不能为访问器,自有可枚举符号属性复制到新对象中
- (4) 源对象的自有可枚举属性,复制时定义属性特性不能为访问器,自有可枚举符号属性复制到新对象中
Object.$expand = function(target, ...sources) {
// 处理参数,如果参数不是引用类型,那么进行装箱
target = target instanceof Object ? target : Object(target)
sources = sources.map(el => el instanceof Object ? el : Object(el))
// 扩展操作符会返回一个新对象
let res = {}
// 复制操作
function copy(obj) {
for(let [key, value] of Object.entries(obj))
// 扩展操作符复制后不存在访问器属性,在此设置属性特性,使之一定不存在
Object.defineProperty(res, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
})
// 自有可枚举符号属性复制到目标对象target
for(let p of Object.getOwnPropertySymbols(target))
if(Object.getOwnPropertyDescriptor(target, p).enumerable)
Object.defineProperty(obj, p, {
value: target[p],
enumerable: true,
configurable: true,
writable: true
})
}
// 复制源对象
sources.forEach(copy)
// 复制目标对象
copy(target)
return res
}
2.5 实现扩展操作符合并数组
合并数组不需要考虑“访问器方法”和“不可枚举”和“自有属性”,比较简单,不做举例。在此提到该情况,在复习时在脑子中过一遍即可。
3.实现对象深拷贝
3.1 实现单个对象深拷贝
-
需求是实现一个单个对象深拷贝函数,输入一个对象,输出该对象的深拷贝值
-
具体实现步骤如下:
- (1) 参数装箱
- (2) 类型检查并创建对象
- (3) 防止循环引用
- (4) 复制所有自有可枚举属性以及Symbol以及其特性
- (5) 引用类型递归复制
function deepClone(obj) {
let wm = new WeakMap()
// 深复制非函数属性
function copy(key, value, target, obj) {
// 深复制要复制属性的特性
if (value instanceof Object) {
let descriptor = Object.getOwnPropertyDescriptor(obj, key)
// 如果是引用类型,那么就递归继续复制
descriptor.value = clone(value)
Object.defineProperty(target, key, descriptor)
} else
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(obj, key))
}
function clone(obj) {
// 深复制函数参数必须是对象
obj = obj instanceof Object ? obj : Object(obj)
// JavaScript特殊内置对象处理
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
if (wm.has(obj))
return wm.get(obj)
let target
// JavaScript非特殊对象处理
if (typeof obj === "object")
// 保持继承链
target = new obj.constructor()
else if (typeof obj === "function")
return obj
else if (Array.isArray(obj))
target = new Array
// 解决循环引用
wm.set(obj, target)
// 复制所有非符号自有可枚举属性及其特性
Object.entries(obj).forEach(([key, value]) => copy(key, value, target, obj))
// 复制所有符号自有可枚举属性及其特性
Object.getOwnPropertySymbols(obj).forEach(key => {
if (Object.getOwnPropertyDescriptor(obj, key).enumerable)
copy(key, obj[key], target, obj)
})
return target
}
return clone(obj)
}
3.2 深拷贝细节讲解
-
如何复制所有自有可枚举属性以及其特性?
共分为三步
第一步:Object.keys()和Object.values()和Object.entries() 可以获取非符号自有可枚举属性的键值。
第二步:Object.getOwnPropertySymbols()获取自有Symbol属性,Object.getOwnPropertyDescriptor判断是否是可枚举。
第三步:Object.getOwnPropertyDescriptor获取属性的特性
-
如何递归深复制引用类型?
共分为三点
第一点:上面示例中的clone函数就是深拷贝函数,作为递归函数
第二点:上面示例中的递归边界共有5个,特殊内置对象Date,RegExp,函数,只有原始类型属性的对象,当前对象是Map中的某一个键
第三点:上面示例中的递归表达式是clone(value),表示碰到引用类型,就直接递归调用即可
-
如何解决循环引用?
共分为三步
第一步:上面示例中创建一个WeakMap
第二步:在clone函数中创建完新对象后,立即加入Map,当前对象作为键,新对象作为值。后续操作会陆续为新对象添加属性,但是堆内存地址不变。
第三步:在clone函数中创建新对象之前,在Map中询问,是否已经存在当前对象作为的键,如果有,那么就立即返回所对应的值。
-
是否需要深复制函数?
不需要深复制函数,深复制函数是没有意义的,在上述clone函数中已经将碰到函数作为递归边界。因为函数的修改往往是重写,引用类型在共享时最危险的操作时修改而不是重写。函数的属性都是只读的,不存在修改的情况。但是准确来说,如果在构造函数上定义静态方法,并想得到该函数的深拷贝,这时面临着修改的风险,需要写针对函数的深拷贝。但是我们为什么要使用这种操作呢?
3.3 函数深拷贝
虽然在3.2中提到函数的深拷贝是没有意义的,但是处于娱乐和探索目的,我们在这里还是实现一个函数的深拷贝。
具体实现步骤如下:
- (1) 通过prototype和函数转为字符串后起始函数名(ES6增强写法)来判断是箭头函数还是普通函数
- (2) 普通函数通过小括号和大括号匹配来获取参数和函数体(indexOf匹配)
- (3) 普通函数体中替换递归调用的函数名
- (4) 箭头函数先判断是否是参数简写和函数体简写再通过括号匹配获取参数和函数体
- (5) 依据参数和函数体新创建一个Function对象并返回
// 函数的属性都是只读的,在这里重新复制一个函数没什么意义
// 如果考虑构造函数上定义静态方法,还有的考虑,但下面不会考虑这一点
function copyFunction(target) {
let func = target.toString(), args, body
// 获取非箭头函数的参数,注意ES6增强写法的函数也没有prototype
if(target.prototype || func.startsWith(`${target.name}`)) {
args = func.slice(func.indexOf("(") + 1, func.indexOf(")")).split(",")
// 获取函数体
body = func.slice(func.indexOf("{") + 1, func.lastIndexOf("}"))
// 递归调用时函数名无法使用,将其改为arguments.callee调用递归
body = body.replaceAll("this." + target.name, "arguments.callee")
} else {
if(func.startsWith("("))
// 箭头函数参数非简写形式
args = func.slice(func.indexOf("(") + 1, func.indexOf(")")).split(",")
else
// 箭头函数单参数简写形式
args = func.slice(0, func.indexOf("=>")).trim().split(",")
if(func.endsWith("}"))
// 箭头函数函数体非简写形式
// 对象中的箭头函数方法中无法使用函数名调用自己来进行递归,这里不用做递归处理
body = func.slice(func.indexOf("{") + 1, func.lastIndexOf("}"))
else
// 箭头函数函数体简写形式
// 对象中的箭头函数方法中无法使用函数名调用自己来进行递归,这里不用做递归处理
body = "return " + func.slice(func.indexOf("=>") + 2).trim()
}
// Function只能创建匿名函数
return new Function(...args, body)
}
3.4 JavaScript中自带的深拷贝方法
注意:下面提到的几种深拷贝方法只介绍缺点,其它没介绍的地方默认和上面实现的深拷贝方法效果相同。
- JSON API:不会处理内置对象和循环引用。undefined值不会处理。NaN,Infinity,-Infinity值会被复制为null。
// 对比JSON深拷贝和自己写的深拷贝的区别,正如上几点所述。
let obj = {
name: undefined,
a: NaN,
b: Infinity,
c: -Infinity,
date: new Date
}
let test = deepClone(obj)
console.log(test)
let test2 = JSON.parse(JSON.stringify(obj))
let test = deepClone(obj)
console.log(test)
// { name: undefined, a: NaN, b: Infinity, c: -Infinity, date: 2021-12-23T12:32:52.855Z }
let test2 = JSON.parse(JSON.stringify(obj))
console.log(test2)
// { a: null, b: null, c: null, date: '2021-12-23T12:32:52.855Z' }
- MessageChannel:不能拷贝函数和符号类型
let channel = new MessageChannel()
let port1 = channel.port1
let port2 = channel.port2
let B = {}
let A = {
a: new RegExp(/123/g),
b: new Date,
[Symbol("C")]: {
name: "c"
},
d: B,
}
B.a = A
port1.postMessage(A)
port2.onmessage = function({data}) {
console.log(data)
// <ref *1> {
// a: /123/g,
// b: 2021-12-23T12:47:07.894Z,
// d: { a: [Circular *1] }
// }
}
- History API:不能拷贝函数和符号类型
<script>
let B = {}
let A = {
a: new RegExp(/123/g),
b: new Date,
[Symbol("C")]: {
name: "c"
},
d: B,
[Symbol("e")]: 123,
}
B.a = A
// 保存当前历史状态
let oldState = history.state
// 把历史信息栈指针当前位置替换成要结构化克隆的对象
history.replaceState(A, document.title)
// 获取结构化克隆结果
let copy = history.state
// 复原当前历史状态
history.replaceState(oldState, document.title)
console.log(copy)
// {a: /123/g, b: Thu Dec 23 2021 20:55:53 GMT+0800 (中国标准时间), d: {…}}
// a: /123/g
// b: Thu Dec 23 2021 20:55:53 GMT+0800 (中国标准时间) {}
// d: {a: {…}}
// [[Prototype]]: Object
</script>
- Notification API:不能拷贝函数和符号类型
注意:下面Notification的用法目前在mdn上还没有中文版,在中文版下不会显示下面示例中用到的Notification的一些属性。
<script>
let B = {}
let A = {
a: new RegExp(/123/g),
b: new Date,
[Symbol("C")]: {
name: "c"
},
d: B
}
B.a = A
console.log(new Notification('', {data: A, silent: true}).data)
// Object
// a: /123/g
// b: Thu Dec 23 2021 21:03:36 GMT+0800 (中国标准时间) {}
// d: {a: {…}}
// [[Prototype]]: Object
</script>
4.JSON的理解
4.1 JSON的理解
- JSON是严格的JavaScript子集,是有效的JavaScript源码
- JSON序列化对象时会调用对象的toJSON方法
4.2 JSON序列化和反序列化的扩展
JSON的序列化和反序列化经常用于简单的对象深拷贝,但是某些情况下不满足需求,再写一个完整的对象深拷贝太费时间了,JSON的API允许我们进行需求扩展。
- 反序列化parse()的第二个参数:
是一个函数,返回undefined则删除该键值对,返回非undefined值则修改这个键值对。
let obj = {
name: "Danny",
age: 20,
gender: "man",
school: {
name: "SDU"
}
}
JSON.parse(JSON.stringify(obj), function(key, value) {
// 会输出每一对键值,this表示当前键值所在的对象
console.log(key, value, this)
return value
})
- 序列化stringify()的第二个参数:
是一个数组,在序列化时,如果对象属性不在数组中将不会参与序列化过程,会被抛弃。是一个函数,用法同parse()的第二个参数。
// 第二个参数是数组的情况
let obj = {
name: "Danny",
age: 20,
gender: "man",
school: {
name: "SDU"
}
}
console.log(JSON.stringify(obj, ["name"]))
// {"name":"Danny"}
// 第二个参数是函数的情况
let B = {}
let A = {
name: "Danny",
age: 20,
gender: "man",
school: {
name: "SDU"
},
b: B
}
B.a = A
// JSON不能序列化循环引用,所以必须把存在循环引用的某个对象置空,破坏循环引用
// 一般情况下我们不会用到循环引用,所以直接破坏它就好了
let wm = new WeakMap()
console.log(JSON.stringify(A, function(key, value) {
if(wm.has(value))
return
if(value instanceof Object)
wm.set(value, value)
return value
}))
// {"name":"Danny","age":20,"gender":"man","school":{"name":"SDU"},"b":{}}
上面的第二个例子,就是JSON序列化时碰到循环引用的解决方案就是通过破坏循环引用来实现的,这一点不如自己实现的深拷贝函数。但是JSON的好处在于可以转为字符串。这也是之前在做项目时碰到的问题。