Как я могу расширить класс, определенный за закрытием в JavaScript?

У меня есть набор классов JavaScript, где базовый класс определяет функции, которые затем разделяются унаследованным классом. Он работает, и он настраивается следующим образом:

var ThingA = function(name) {
    this.name = name;
};

ThingA.prototype = {
    sayHi: function() {
        alert('Hi, ' + this.name + '!');
    }
};


var ThingB = function() {
    ThingA.call(this, 'Charlie');
};

ThingB.prototype = new ThingA();
ThingB.prototype.constructor = ThingB;

var instanceOfB = new ThingB();
instanceOfB.sayHi();   // alerts 'Hi, Charlie!'

По причинам, которые находятся вне моего контроля, моя компания предпочитает следовать этому шаблону при написании JavaScript:

SomeClass = function() {

    // "Private" functions go here

    function somePrivateMethod() { 
        ...
    }

    return {

        // "Public" methods go here
        somePublicMethod: function() { ... }

    };
}();

Теперь это хорошо, насколько все идет, и хорошо работает для многих ситуаций. Но это скорее функциональный стиль. Существует только один экземпляр класса, и все статично.

Меня попросили изменить мой рабочий код, чтобы более точно соответствовать стилю, который предпочитает моя компания. Итак, мой вопрос в том, есть способ наследовать от класса, который обернут внутри класса factory? Он будет выглядеть примерно так:

FactoryClassA = function() {

    var ThingA = function(name) {
        this.name = name;
    };

    ThingA.prototype = {
        sayHi: function() {
            alert('Hi, ' + this.name + '!');
        }
    };

    return {
         createThingA: function(name) {
             return new ThingA(name);
         }
    };
}();


FactoryClassB = function() {

    // Define a ThingB class that inherits from ThingA somehow

    return {
         createThingB: function() {
             return new ThingB();
         }
    };
}();


var instanceOfB = FactoryClassB.createThingB();
instanceOfB.sayHi();   // should alert 'Hi, Charlie!'

Есть ли способ определить ThingB, завернутый в FactoryClassB, который наследует от ThingA завернутый в FactoryClassA? Благодаря этому вопросу, я знаю, что я не смогу сделать это именно так. Я думаю об использовании метода для расширения данного класса... как-то?

Этот ответ кажется близким, но мне трудно понять, как изменить этот пример, чтобы он соответствовал специфике моей ситуации. Я готов немного сгладить свою модель компании, но могу ли я хотя бы приблизиться к ней?

ОБНОВЛЕНИЕ 1

В ответ на комментарий Адама просто добавьте параметр в класс factory, здесь, где я застрял:

ThingB.prototype = new ThingA();
ThingB.prototype.constructor = ThingB;

Я не могу понять, как адаптировать эти строки, чтобы заставить его работать, если я просто передаю параметр методу класса factory.

Ответ 1

Ниже приведено то, что (я считаю), которое вы ищете:

FactoryClassA = function() {
    var ThingA = function(name) {
       this.name = name;
    };
    ThingA.prototype = {
        sayHi: function() {
            console.log('Hi, ' + this.name + '!');
        }
    };
    // Add the constructor back to the prototype
    // (see explanation below)
    ThingA.prototype.constructor = ThingA;

    return {
        createThingA: function(name) {
            return new ThingA(name);
        }
    };
}();

FactoryClassB = function() {
    // Bootstrapping:
    // Capture the instance, as we'll need it to set up the prototype
    var baseInstance = new FactoryClassA.createThingA();
    // Capture the constructor
    var baseConstructor = baseInstance.constructor;
    // Keep a reference to the base prototype
    var baseProto = baseConstructor.prototype;

    function ThingB(name) {
        // Call base constructor, along with our args
        baseConstructor.call(this, name);
    };
    ThingB.prototype = baseInstance;
    ThingB.prototype.constructor = ThingB;

    ThingB.prototype.sayHi = function() {
        console.log('here I am');
        // call the base class `sayHi`
        baseProto.sayHi.call(this);
    };

    return {
        createThingB: function(name) {
            return new ThingB(name);
        }
    };
}();

// Testing
var foo = FactoryClassB.createThingB("Indeed");
foo.sayHi();

// Output:
//   here I am 
//   hi indeed

Объяснение

в FactoryClassA, эта строка необходима:

ThingA.prototype.constructor = ThingA;

Обратите внимание, что каждый прототип в JS автоматически создается со ссылкой на его конструктор. Например, когда вы выполните:

function T(){}

T.prototype уже имеет свойство constructor, которое указывает на T.

Однако в вашей реализации ThingA вы reset весь прототип, выполнив ThingA.prototype = { ... }. Поэтому вы потеряли ссылку на свой конструктор. В 99% случаев это нормально и не будет иметь никаких отрицательных побочных эффектов (вероятно, именно поэтому большинство разработчиков склонны его забывать). Однако в случае наследования это может быть необходимо.

Теперь, в FactoryClassB, нам нужно выполнить некоторую загрузку:

var baseInstance = new FactoryClassA.createThingA();
var baseConstructor = baseInstance.constructor;
var baseProto = baseConstructor.prototype;

Наблюдайте последние две строки, поскольку они имеют решающее значение для достижения наследования в этом шаблоне проектирования. Во-первых, поскольку конструктор ThingA доступен через прототип (ThingA.prototype.constructor = ThingA), то это означает, что с учетом экземпляра ThingA мы можем напрямую получить его конструктор. Поскольку конструктор является самой функцией, и поскольку каждая функция имеет ссылку на свой прототип, мы можем сохранить ссылку ThingA.prototype с помощью baseConstructor.prototype.

Далее - критическая часть, где мы установили цепочку наследования:

function ThingB(name) {
    // Call the base constructor
    baseConstructor.call(this, name);
};
ThingB.prototype = baseInstance;
ThingB.prototype.constructor = ThingB;

Последняя строка выше очень важна, так как она сообщает прототипу, что такое его конструктор, иначе он все равно будет указывать на ThingA.

Там у вас это - прототипное наследование.

Боковое примечание:

Вероятно, вы можете увидеть, как это может стать довольно утомительным, немного гротескным и повторяющимся. Ergo, вы можете рассмотреть библиотеку наследования типа Fiber.js, которая следует по шаблону инкапсуляции, который вам нужен (наряду с некоторыми бонусами, такими как миксины и декораторы), Отказ от ответственности: я создал библиотеку.