javascript - 用对象方法深度克隆一个类对象?
问题描述
如何深度克隆用户定义类的对象并保留该类的对象方法?
例如,我有一个Schedule
用成员days: number[]
和函数调用的 Object 类getWeekdays()
因此,如果我想创建一个新Schedule
对象,该对象将是具有克隆属性的现有对象的克隆,Schedule
并且还具有该getWeekdays()
功能,我该怎么做?我试过Object.assign()
了,但只有浅拷贝days
,我知道JSON.parse()
不会工作,因为我不会得到对象方法。我尝试了 lodash _.cloneDeep()
,但不幸的是,创建的对象缺少对象方法。
解决方案
Object.assign()
如果您使用以下方法之一将方法绑定到对象而不是其原型,则将保留该getWeekdays()
方法:
⚠️ 将方法直接绑定到对象而不是其原型通常被认为是一种反模式——尤其是在性能优先级更高的情况下——因为 N
Schedule
s 将引用 N个单独getWeekend()
的函数,而不是引用getWeekend()
原本由原型共享的单个函数。
箭头函数方法
第一种方法是class
使用箭头函数在定义中声明您的方法,如下所示:
class Schedule {
public days: Array<number> = [];
public getWeekdays = (): Array<number> => {
return this.days;
}
}
const clone = Object.assign({}, new Schedule());
...但为什么?
这样做的原因有两个:
- 因为箭头函数语法将方法绑定到结果对象而不是其原型。
- 因为
Object.assign()
复制对象自己的属性,但不复制其继承的属性。
如果你运行console.log(new Schedule());
,你可以看到第一点:
// with arrow function:
▼ Schedule {days: Array(0), getWeekdays: } ⓘ
▷ days: Array(0) []
▷ getWeekdays: () => { … }
▷ __proto__: Object { constructor: … }
// without arrow function:
▼ Schedule { days: Array(0) } ⓘ
▷ days: Array(0) []
▼ __proto__: Object { constructor: , getWeekdays: }
▷ constructor: class Schedule { … }
▷ getWeekdays: getWeekdays() { … }
▷ __proto__: Object { constructor: , __defineGetter__: , __defineSetter__: , … }
这与static
方法有何不同?
方法static
不是绑定对象的原型,而是绑定到class
自身,即原型的构造函数:
class Schedule {
public static days: Array<number> = [];
public static getWeekdays(): Array<number> {
return this.days;
}
}
const clone = Object.assign({}, new Schedule());
console.log(new Schedule());
// console
▼ Schedule {} ⓘ
▼ __proto__: Object { constructor: … }
▼ constructor: class Schedule { … }
[[FunctionLocation]]: internal#location
▷ [[Scopes]]: Scopes[1]
arguments: …
caller: …
▷ days: Array(0) []
▷ getWeekdays: getWeekdays() { … }
length: 0
name: "Schedule"
▷ prototype: Object { constructor: … }
▷ __proto__: function () { … }
▷ __proto__: Object { constructor: … , __defineGetter__: … , __defineSetter__: … , … }
这意味着static
方法可能不会直接绑定到对象。如果你尝试,你会得到这个 TSError:
~/dev/tmp/node_modules/ts-node/src/index.ts:261
return new TSError(diagnosticText, diagnosticCodes)
^
TSError: ⨯ Unable to compile TypeScript:
index.ts(14,14): error TS2334: 'this' cannot be referenced in a static property initializer.
at createTSError (~/dev/tmp/node_modules/ts-node/src/index.ts:261:12)
at getOutput (~/dev/tmp/node_modules/ts-node/src/index.ts:367:40)
at Object.compile (~/dev/tmp/node_modules/ts-node/src/index.ts:558:11)
at Module._compile (~/dev/tmp/node_modules/ts-node/src/index.ts:439:43)
at internal/modules/cjs/loader.js:733:10
at Object..ts (~/dev/tmp/node_modules/ts-node/src/index.ts:442:12)
at Module.load (internal/modules/cjs/loader.js:620:32)
at tryModuleLoad (internal/modules/cjs/loader.js:560:12)
at Function._load (internal/modules/cjs/loader.js:552:3)
at Function.runMain (internal/modules/cjs/loader.js:775:12)
.bind()
在构造函数中
箭头函数(包括那些在class
方法定义中使用的)是 ES6 的一个特性,它为函数声明表达式提供了关于this
关键字行为的更简洁的语法。与常规函数不同,箭头函数使用其封闭词法范围的值,而不是根据调用的上下文this
建立自己的值。this
他们也没有收到自己的arguments
对象(或super
,或new.target
)。
在 ES6 之前,如果您需要this
在用作回调的方法中使用,则必须将宿主对象的值绑定this
到方法的值this
with .bind()
,这将返回一个更新后的函数,其this
值设置为提供的值,像这样:
var clone;
function Schedule() {
this.days = [];
this.setWeekdays = function(days) {
this.days = days;
}
this.setWeekdays = this.setWeekdays.bind(this);
}
clone = Object.assign({}, new Schedule());
console.log(clone);
// console
▼ Object {days: Array(0), setWeekdays: }
▷ days:Array(0) []
▷ setWeekdays:function () { … }
▷ __proto__:Object {constructor: , __defineGetter__: , __defineSetter__: , …}
在 ES6class
中,您可以通过调用.bind()
构造函数中的方法来获得相同的结果:
class Schedule {
public days: Array<number> = [];
constructor() {
this.getWeekdays = this.getWeekdays.bind(this);
}
public getWeekdays(): Array<number> {
return this.days;
}
}
const clone = Object.assign({}, new Schedule());
console.log(clone);
// console
▼ Object {days: Array(0), setWeekdays: … } ⓘ
▷ days: Array(0) []
▷ setWeekdays: function () { … }
▷ __proto__: Object { constructor: , __defineGetter__: , __defineSetter__: , … }
未来奖励:自动绑定装饰器
⚠️也不一定推荐,因为您最终分配了通常从不调用的函数,如下所述。
装饰器被认为是 TypeScript 中的一项实验性功能,需要您experimentalDecorators
在.true
tsconfig.json
使用自动绑定装饰器将允许您getWeekdays()
“按需”重新绑定方法 - 就像.bind()
在构造函数中使用键一样,但绑定发生在getWeekdays()
调用时而不是调用时 -new Schedule()
仅以更紧凑的方式:
class Schedule {
public days: Array<number> = [];
@bound
public getWeekdays(): Array<number> {
return this.days;
}
}
但是,由于装饰器仍处于第 2 阶段,因此在 TypeScript 中启用装饰器仅公开 4 种类型的装饰器函数的接口(即ClassDecorator
, PropertyDecorator
, MethodDecorator
, ParameterDecorator
.)。第 2 阶段提出的内置装饰器,包括@bound
,并未开箱即用.
为了使用@bound
,您必须让 Babel 处理您的 TypeScript 转换@babel/preset-typescript
以及@babel/preset-stage-2
.
或者,这个功能可以(在某种程度上)用这个 NPM 包填充:
这个包@boundMethod
将把方法绑定到除了它的原型之外getWeekdays()
的结果对象,但不会被复制:new Schedule()
Object.assign()
// console.log(new Schedule());
▼ Schedule { days: Array(0) } ⓘ
▷ days: Array(0) []
▷ getWeekdays: function () { … }
▼ __proto__: Object { constructor: , getWeekdays: <accessor> }
▷ constructor: class Schedule { … }
▷ getWeekdays: getWeekdays() { … }
▷ __proto__: Object { constructor: … , __defineGetter__: … , __defineSetter__: … , … }
// console.log(clone);
▼ Object { days: Array(0) } ⓘ
▷ days: Array(0) []
▷ __proto__: Object { constructor: … , __defineGetter__: … , __defineSetter__: … , … }
这是因为@boundMethod
装饰器覆盖了要调用的方法get
和访问器(因为这些访问器中的值设置为分配属性的对象),将其附加到对象上,然后返回绑定方法,该方法具有一些有趣的效果:set
.bind()
this
Object.defineProperty()
PropertyDescriptor
const instance = new Schedule();
console.log('instance:', instance);
console.log('\ninstance.hasOwnProperty(\'getWeekdays\'):', instance.hasOwnProperty('getWeekdays'));
console.log('\ninstance.getWeekdays():', instance.getWeekdays());
console.log('\ninstance.hasOwnProperty(\'getWeekdays\'):', instance.hasOwnProperty('getWeekdays'));
// console
instance:
▼ Schedule { days: Array(0) } ⓘ
▷ days: Array(0) []
▷ getWeekdays: function () { … }
▷ __proto__: Object { constructor: , getWeekdays: <accessor> }
instance.hasOwnProperty('getWeekdays'): false
instance.getWeekdays():
▷ Array(0) []
instance.hasOwnProperty('getWeekdays'): true
Object.assign()
不起作用的原因实际上是双重的:
- 它实际上调用
[[Get]]
源对象(即new Schedule()
)和[[Set]]
目标对象(即{}
)。 - 用于检修
PropertyDescriptor
s访问器的s 不可枚举。@boundMethod
getWeekend()
如果我们要更改最后一点并使用可枚举访问器,我们可以开始Object.assign()
工作,但只有在 getWeekdays()
至少被调用一次之后:
const instance = new Schedule();
const clone1 = Object.assign({}, instance);
void instance.getWeekdays();
const clone2 = Object.assign({}, instance);
console.log('clone1:', clone1);
console.log('clone2:', clone2);
// console
clone1:
▼ Object { days: Array(0) } ⓘ
▷ days: Array(0) []
▷ __proto__: Object { constructor: … , __defineGetter__: … , __defineSetter__: … , … }
clone2:
▼ Object { days: Array(0) } ⓘ
▷ days: Array(0) []
▷ getWeekdays: function () { … }
▷ __proto__: Object { constructor: … , __defineGetter__: … , __defineSetter__: … , … }
推荐阅读
- c++ - 标准超出范围大小终止
- typescript - TypeScript - 具有满足一个接口和其他两个接口之一的值
- python - pyqt6 QThread,threading.Lock() 等效?
- javascript - 如何在按钮元素之外监听鼠标点击?
- android - Android Jetpack Compose:OutlinedTextField 上的“EditorInfoCompat 类中没有静态方法 setInitialSurroundingText”
- angular - Ionic 使模型中的字段类型在其他两个模型之间进行选择
- xml - 线性布局/相对布局中的第一个视图上方/重叠第二个视图
- python - Flask Migrate (Alembic) 不创建迁移
- nginx - 如何在 nginx certbot 中将非 www 重定向到 www
- amazon-web-services - 在 EC2 中部署的 Tomcat Webapp 无法与 Postgres RDS 实例通信