Глубоко клонировать объект класса с помощью методов объекта? - PullRequest
0 голосов
/ 11 июня 2018

Как глубоко клонировать объект пользовательского класса, а также сохранить методы объекта этого класса?

Например, у меня есть класс Object с именем Schedule с членом days: number[] иfunction getWeekdays()

Так что, если я хочу создать новый объект Schedule, который будет клоном существующего Schedule с клонированными свойствами, а также иметь функцию getWeekdays(), как бы я это сделал??Я попытался Object.assign(), но это только мелкие копии days, и я знаю, JSON.parse() не будет работать, потому что я не получу методы объекта.Я попробовал lodash _.cloneDeep(), но, к сожалению, в создаваемом объекте отсутствуют методы объекта.

Ответы [ 3 ]

0 голосов
/ 12 июня 2018

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 с помощью .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__: , …}

В ES6 class вы можете достичь тех же результатов, вызвав .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__: , … }

Будущий бонус: декораторы автосвязки

⚠️ Также не обязательно рекомендуется, поскольку в конечном итоге вы выделяете функции, которые обычно никогда не вызываются, как описано ниже.

Декораторы считаются экспериментальной функцией вНапечатайте и потребуйте, чтобы вы явно установили 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]] на целевом объекте (т.е. {}).
  • thePropertyDescriptor s, которые @boundMethod использует для капитального ремонта * Средства доступа 1158 * не перечисляются.

Если бы мы изменили эту последнюю точку и использовали перечислимые средства доступа, мы могли бы заставить 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__: … , … }
0 голосов
/ 31 июля 2019

Вам необходимо сначала сериализовать объект в JSON, сделать глубокий клон результата, а затем десериализовать его обратно в объект класса.Вы можете использовать библиотеку, например, такую: https://github.com/typestack/class-transformer

В итоге это будет выглядеть так:

import { classToPlain, plainToClass } from "class-transformer";

let a = new Schedule();
let aSerialized = classToPlain(a);
let b = plainToClass(Schedule, aSerialized);

Или вы можете использовать метод classToClass:

import { classToClass } from "class-transformer";
let b = classToClass(a);

Суть в том, что вы должны аннотировать класс некоторыми аннотациями из вышеуказанной библиотеки, но я не думаю, что есть лучший способ сделать это.

0 голосов
/ 11 июня 2018

Попробуйте функцию copy из здесь

// from https://www.codementor.io/avijitgupta/deep-copying-in-js-7x6q8vh5d
function copy(o) {
   var output, v, key;
   output = Array.isArray(o) ? [] : {};
   for (key in o) {
       v = o[key];
       output[key] = (typeof v === "object") ? copy(v) : v;
   }
   return output;
}


var Event = /** @class */ (function () {
    function Event(name) {
        this.name = name;
    }
    Event.prototype.getName = function () {
        return "Event " + this.name;
    };
    return Event;
}());
var Schedule = /** @class */ (function () {
    function Schedule() {
    }
    Schedule.prototype.getWeekdays = function () {
        return this.weekDays;
    };
    return Schedule;
}());
var schedule = new Schedule();
schedule.days = [3, 11, 19];
schedule.weekDays = [1, 2, 3];
schedule.event = new Event("Event");

var clone = copy(schedule);
console.log(clone);
...