首页 > 解决方案 > 用对象方法深度克隆一个类对象?

问题描述

如何深度克隆用户定义类的对象并保留该类的对象方法?

例如,我有一个Schedule用成员days: number[]和函数调用的 Object 类getWeekdays()

因此,如果我想创建一个新Schedule对象,该对象将是具有克隆属性的现有对象的克隆,Schedule并且还具有该getWeekdays()功能,我该怎么做?我试过Object.assign()了,但只有浅拷贝days,我知道JSON.parse()不会工作,因为我不会得到对象方法。我尝试了 lodash _.cloneDeep(),但不幸的是,创建的对象缺少对象方法。

标签: javascripttypescriptlodash

解决方案


Object.assign()如果您使用以下方法之一将方法绑定到对象而不是其原型,则将保留该getWeekdays()方法:

⚠️ 将方法直接绑定到对象而不是其原型通常被认为是一种反模式——尤其是在性能优先级更高的情况下——因为 N Schedules 将引用 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到方法的值thiswith .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在.truetsconfig.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()thisObject.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]]目标对象(即{})。
  • 用于检修PropertyDescriptors访问器的s 不可枚举。@boundMethodgetWeekend()

如果我们要更改最后一点并使用可枚举访问器,我们可以开始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__: … , … }

推荐阅读