Как настроить цепочку прототипов Dynami c в TypeScript? - PullRequest
0 голосов
/ 17 апреля 2020

В JavaScript я могу написать «производный класс», чей «базовый класс» - Dynami c, с кодом, подобным следующему:

function NewBaseClass(sF) { 
  function DynamicBaseClass(iF) { this.instanceField = iF; }
  // EDIT: oops, this is not really static in the ES6 sense, but it's in the
  //       "base" prototype and, importantly, is NOT in the final object.
  DynamicBaseClass.prototype.staticField = sF;
  return DynamicBaseClass;
}
function NewDerivedClass(baseClass) {
  function DerivedClass(iF, dF) {
    baseClass.call(this, iF);
    this.derivedField = dF;
  }
  DerivedClass.prototype = Object.create(baseClass.prototype);
  Object.defineProperty(DerivedClass.prototype, 'constructor', { 
    value: DerivedClass,
    enumerable: false, // omit from 'for in' loop
    writable: true
  });
  DerivedClass.prototype.dump = function dump() {
    console.log("instanceField=" + this.instanceField +
        " derivedField=" + this.derivedField + 
        " staticField=" + this.staticField + 
        " base=" + this.__proto__.__proto__.constructor.name);
  }
  return DerivedClass;
}
var BaseClass1 = NewBaseClass("dynamic prototype #1");
var BaseClass2 = NewBaseClass("dynamic prototype #2");
new (NewDerivedClass(BaseClass1))(3, 33).dump();
new (NewDerivedClass(BaseClass1))(4, 44).dump();
new (NewDerivedClass(BaseClass2))(5, 55).dump();
new (NewDerivedClass(BaseClass2))(6, 66).dump();
// Output:
//   instanceField=3 derivedField=33 staticField=dynamic prototype #1 base=DynamicBaseClass
//   instanceField=4 derivedField=44 staticField=dynamic prototype #1 base=DynamicBaseClass
//   instanceField=5 derivedField=55 staticField=dynamic prototype #2 base=DynamicBaseClass
//   instanceField=6 derivedField=66 staticField=dynamic prototype #2 base=DynamicBaseClass

(Для этого есть различные причины; в моем случае это экономит память: если все большие группы объектов нуждаются в одинаковых значениях для нескольких свойств, они могут эффективно использовать эти общие значения в цепочке прототипов.)

Как добиться подобного эффекта в TypeScript ? Это нормально, если код для определения класса немного уродлив, если можно написать new (NewDerivedClass(BaseClass)) так, чтобы это выражение имело разумный тип.

Ответы [ 2 ]

1 голос
/ 17 апреля 2020

С классами ES2015

Поскольку ключевое слово extends может принимать переменную, вы можете преобразовываться из конструкторов функциональных классов в классы в стиле ES2015.

function NewBaseClass(sF: string) { 
    return class {
        staticField = sF;
        instanceField: number;

        constructor(iF: number) {
            this.instanceField = iF;
        }
    };
}

interface SuperclassType {
    new(iF: number): {
        instanceField: number;
        staticField: string;
    };
}

function NewDerivedClass(baseClass: SuperclassType) {
    return class extends baseClass {
        derivedField: number;
        constructor(iF: number, dF: number) {
            super(iF);
            this.derivedField = dF;
        }

        dump() {
            console.log("instanceField=" + this.instanceField +
                " derivedField=" + this.derivedField + 
                " staticField=" + this.staticField);
        }
    };
}

var BaseClass1 = NewBaseClass("dynamic prototype #1");
var BaseClass2 = NewBaseClass("dynamic prototype #2");
new (NewDerivedClass(BaseClass1))(3, 33).dump();
new (NewDerivedClass(BaseClass1))(4, 44).dump();
new (NewDerivedClass(BaseClass2))(5, 55).dump();
new (NewDerivedClass(BaseClass2))(6, 66).dump();
// Output:
//   instanceField=3 derivedField=33 staticField=dynamic prototype #1
//   instanceField=4 derivedField=44 staticField=dynamic prototype #1
//   instanceField=5 derivedField=55 staticField=dynamic prototype #2
//   instanceField=6 derivedField=66 staticField=dynamic prototype #2

детская площадка

Одна из опасностей этого подхода заключается в том, что вы не можете принять и изменить generic c как часть NewDerivedClass, что не позволяет вносить изменения в произвольный класс - отчасти из-за вероятность того, что вы введете столкновение имен. См. выпуск # 4890 .

С ES2015 static

Обратите внимание, что выше не используется Typescript static, так как вы фактически поместили данные c поле в анонимном объекте-прототипе NewBaseClass, а не в самой функции конструктора. Это легко исправить, обратившись к объекту базового класса в функции деривации, а не рассматривая его как необработанное свойство.

function NewBaseClass(sF: string) { 
    return class {
        static staticField = sF;  // <-- static
        instanceField: number;

        constructor(iF: number) {
            this.instanceField = iF;
        }
    };
}

interface SuperclassType {
    staticField: string;  // <-- static
    new(iF: number): {
        instanceField: number;
    };
}

function NewDerivedClass(baseClass: SuperclassType) {
    return class extends baseClass {
        derivedField: number;
        constructor(iF: number, dF: number) {
            super(iF);
            this.derivedField = dF;
        }

        dump() {
            console.log("instanceField=" + this.instanceField +
                " derivedField=" + this.derivedField + 
                " staticField=" + baseClass.staticField);  // <-- superclass reference
        }
    };
}

детская площадка

С функциональным классом Конструкторы

Это вряд ли приведет к очень читабельному коду, но это можно сделать с помощью строгой типизации (вне ваших производных классов) без изменения вашего кода. По смежным вопросам # 2299 и # 2310 не похоже, что будет хороший способ express безопасно использовать функциональный синтаксис. Я сохранил его, используя ваш синтаксис, и (как рекомендовано в темах) я перенаправляю приведение с помощью unknown.

function NewDerivedClass<T extends BaseClassConstructor>(baseClass: T):
    DerivedClassConstructor<T> {
  function DerivedClass(
      this: InstanceType<BaseClassConstructor> & { derivedField: number },
      iF: number,
      dF: number) {
    baseClass.call(this, iF);
    this.derivedField = dF;
  }
  DerivedClass.prototype = Object.create(baseClass.prototype);
  Object.defineProperty(DerivedClass.prototype, 'constructor', {
    value: DerivedClass,
    enumerable: false, // omit from 'for in' loop
    writable: true
  });
  DerivedClass.prototype.dump = function dump() {
    console.log("instanceField=" + this.instanceField +
      " derivedField=" + this.derivedField +
      " staticField=" + this.staticField +
      " base=" + this.__proto__.__proto__.constructor.name);
  }
  return DerivedClass as unknown as DerivedClassConstructor<T>;
}

. Как вы можете видеть по ссылке ниже, это позволяет всем дословно описанные случаи, а также позволяют строго типизированный доступ к SomeOtherClass.someOtherField.

игровая площадка

1 голос
/ 17 апреля 2020

Общее решение

Следующий код производит тот же эффект в TypeScript (staticField находится в цепочке прототипов, а не в производном объекте). Однако обратите внимание, что использовать действительно static поле в базовом классе проще: вам не нужно писать as BaseClass в NewBaseClass.

  • TypeScript 3.8.3 не делает полностью примите это: он жалуется на DerivedClass, говоря: «Класс mixin должен иметь конструктор с единственным параметром rest типа 'any []'". Однако эту ошибку можно устранить с помощью // @ts-ignore.
  • В TypeScript 3.6.5, похоже, не понимается, что baseClass не пусто и поэтому выдает несколько ошибок. В нем также говорится «Возвращаемый тип экспортированной функции имеет или использует закрытое имя DerivedClass», что странно, поскольку NewDerivedClass не экспортируется. Обходной путь для последней ошибки - определить соответствующий интерфейс и использовать его в качестве возвращаемого типа:

    interface DerivedClass_ {
      new (iF: number, dF: number): {
        derivedField: number;
        dump(): void;
      }
    }
    
interface BaseClass {
  new (iF: number): {
    instanceField: number;
    staticField: string;
  };
}
function NewBaseClass(sF: string): BaseClass {
  class DynamicBaseClass {
    instanceField: number;
    staticField?: string; // a value assigned here wouldn't be on the prototype
    constructor(iF: number) { this.instanceField = iF; }
  }
  DynamicBaseClass.prototype.staticField = sF;
  return DynamicBaseClass as BaseClass;
}
function NewDerivedClass<Base extends BaseClass>(baseClass: Base) {
  // @ts-ignore "A mixin class must have a constructor with a single rest parameter..."
  class DerivedClass extends baseClass {
    derivedField: number;
    constructor(iF: number, dF: number) {
      super(iF);
      this.derivedField = dF;
    }
    dump() {
      console.log("instanceField=" + this.instanceField +
          " derivedField=" + this.derivedField + 
          " staticField=" + this.staticField +
          " base=" + (this as any).__proto__.__proto__.constructor.name);
    }
  }
  return DerivedClass;
}
var BaseClass1 = NewBaseClass("dynamic prototype chain #1");
var BaseClass2 = NewBaseClass("dynamic prototype chain #2");
new (NewDerivedClass(BaseClass1))(3, 33).dump();
new (NewDerivedClass(BaseClass1))(4, 44).dump();
new (NewDerivedClass(BaseClass2))(5, 55).dump();
new (NewDerivedClass(BaseClass2))(6, 66).dump();

Вывод компилятора выглядит так в TS 3.6.3 и работает как ожидалось:

"use strict";
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
function NewBaseClass(sF) {
    var DynamicBaseClass = /** @class */ (function () {
        function DynamicBaseClass(iF) {
            this.instanceField = iF;
        }
        return DynamicBaseClass;
    }());
    DynamicBaseClass.prototype.staticField = sF;
    return DynamicBaseClass;
}
function NewDerivedClass(baseClass) {
    // @ts-ignore "A mixin class must have a constructor with a single rest parameter of type 'any[]'."
    var DerivedClass = /** @class */ (function (_super) {
        __extends(DerivedClass, _super);
        function DerivedClass(iF, dF) {
            var _this = _super.call(this, iF) || this;
            _this.derivedField = dF;
            return _this;
        }
        DerivedClass.prototype.dump = function () {
            console.log("instanceField=" + this.instanceField +
                " derivedField=" + this.derivedField +
                " staticField=" + this.staticField +
                " base=" + this.__proto__.__proto__.constructor.name);
        };
        return DerivedClass;
    }(baseClass));
    return DerivedClass;
}
var BaseClass1 = NewBaseClass("dynamic prototype chain #1");
var BaseClass2 = NewBaseClass("dynamic prototype chain #2");
new (NewDerivedClass(BaseClass1))(3, 33).dump();
new (NewDerivedClass(BaseClass1))(4, 44).dump();
new (NewDerivedClass(BaseClass2))(5, 55).dump();
new (NewDerivedClass(BaseClass2))(6, 66).dump();

Я вижу, он использует Object.setPrototypeOf, который MDN предупреждает нас не использовать по соображениям производительности. Я надеюсь, что люди из TypeScript знают, что делают!

Техника для «дешевого» обмена данными

Если цель состоит в том, чтобы просто делиться данными между многими экземплярами, не занимая при этом память в отдельных экземплярах, это это можно сделать намного проще, как это:

interface DynamicClass_ { // not needed in TypeScript 3.8
  new (iF: number, dF: number): {
    instanceField: number;
    derivedField: number;
  };
}
function NewClass(staticField: string, foo: any): DynamicClass_ {
  class DynamicClass {
    constructor(public instanceField: number, 
                public derivedField: number) { }
    dump() {
      console.log("instanceField=" + this.instanceField +
          " derivedField=" + this.derivedField + 
          " staticField=" + staticField + // <<<<<<<<<<<<<<<<<<<<<<<<<
          " foo=" + foo);                 // <<<<<<<<<<<<<<<<<<<<<<<<<
    }
  }
  return DynamicClass;
}

Обратите внимание, что dump() может ссылаться на параметры, не сохраняя их в классе где-либо! В общем, среда выполнения JS должна создавать некоторый объект кучи для функций класса, таких как dump(), для совместного использования. Логически, он не может сохранить параметры (staticField et c.) В экземпляре (this), потому что можно изменить this с помощью кода, подобного new (NewClass(...))(...).dump.bind(otherThis) - и все же отскок dump по-прежнему будет иметь доступ к параметрам NewClass.

Я почти уверен, что объекты, представляющие функции внутри DynamicClass, должны создаваться заново каждый раз, когда вызывается NewClass, потому что эти объекты доступны для JS программ. Таким образом, любой метод, который включает в себя возврат классов или функций из другой функции, повлечет за собой определенную стоимость памяти. В зависимости от обстоятельств эта стоимость может быть меньше или больше, чем хранение данных в экземплярах класса.

При использовании этого метода может быть полезно скопировать параметры в прототип для целей отладки:

function NewClass(staticField: string): DynamicClass_ {
  class DynamicClass {
    ...
  }

  let proto: any = DynamicClass.prototype;
  proto.staticField = staticField;

  return DynamicClass;
}
...