Беларусь, рогачёв |
Эмулируем множественное наследование
Как это будет работать
Теперь мы приступим к описанию функции multipleInherit, которая сможет решить все вышеописанные проблемы. Эта функция будет принимать следующие аргументы.
- Функцию, которая будет "прообразом" конструктора для создаваемого класса.
- Массив базовых классов (причем массив двумерный, о чем речь пойдет далее).
- Третий аргумент - необязательный. Он представляет собой массив, в котором хранится системный базовый класс (его нужно прикрепить к самой последней субцепочке - без копирования), а также функция вызова его конструктора, передающая туда необходимые аргументы.
Возвращать функция multipleInherit станет сгенерированный в ней объект-функцию, который и будет готовым конструктором нового класса. В поле prototype этого объекта-функции нужно будет заводить новые методы производного класса.
Поговорим подробнее об аргументах multipleInherit. Первый из них (функцию) мы назвали прообразом конструктора, поскольку из настоящего конструктора (его генерирует multipleInherit ) для того, чтобы произвести инициализацию, будет вызвана именно эта функция. Поэтому в рассматриваемой функции вы должны предусмотреть все необходимые инициализационные действия (за исключением вызова конструкторов базовых классов - для этого мы будем применять отдельный механизм).
Второй аргумент - массив базовых классов. Однако, массив этот двумерный, поскольку для каждого базового класса нужно указать еще некоторую дополнительную информацию. Таким образом, каждый базовый класс описывается своим массивом, в котором может быть до четырех элементов. Набор таких массивов, объединенных в один большой массив - это и есть второй аргумент. О смысле элементов маленьких массивов мы скажем чуть позже.
Наконец, третий аргумент имеет смысл использовать, если скажем, некие два класса (из всей массы классов, от которых вы наследуетесь) являются наследниками одного и того же системного класса. Тогда лучшее, что вы можете сделать - прицепить этот системный класс в конце цепочки __proto__, причем исходный класс, а не его копию. (В результате получается нечто вроде виртуального базового класса.) Поскольку в этом случае непонятно, с какими аргументами вызывать конструктор системного класса - с теми, что передаются через цепочку первого базового класса, или второго - необходимо предусмотреть функцию для генерации правильного набора аргументов конструктора. Эта функция имеет еще одну особенность, о которой мы скажем далее. Сам системный класс и эту функцию при вызове multipleInherit надо поместить в массив (в нем, таким образом, будет всего два элемента), и этот массив и передать в качестве третьего аргумента в multipleInherit.
Чтобы окончательно понять, как именно будет вызываться функция multipleInherit, нам остается проанализировать, из чего состоят маленькие массивы, которые являются составными частями второго аргумента - списка базовых классов. Итак, первым элементом такого массива является сам базовый класс. Вторым - функция, которая подготавливает аргументы для его конструктора. Ей будет передан тот же список аргументов, что и конструктору "множественного наследника" при создании экземпляра этого класса. А вернуть такая функция должна массив аргументов, которые будут переданы конструктору базового класса, к коему "приписана" эта функция. (И хотя эта последняя похожа на аналогичную функцию, которую мы делаем для системного базового класса, они все же существенно отличаются - дальше мы поговорим об этом подробнее). Третий - необязательный - аргумент - это класс, на котором нужно остановиться при копировании субцепочки предков данного класса. Скажем, если мы хотим эмулировать виртуальный базовый класс, субцепочки всех его наследников надо оборвать в тот момент, когда мы доберемся до класса, который хотим сделать виртуальным базовым (копируем мы цепочки, естественно, начиная "снизу" - с производных классов). И, кроме того, нужно будет в списке базовых классов указать тот, который мы хотим сделать виртуальным базовым (причем указать после его производных). Мы увидим, как все это работает, разбирая примеры.
По поводу третьего элемента маленького массива может возникнуть вопрос: где мы, собственно останавливаемся при копировании? То есть включаем в цепочку указанный класс, или же нет? Оказывается, полезными в разных случаях будут оба варианта. Поэтому четвертый элемент маленького массива и будет указывать, как нам поступать: если он равен true, то указанный класс в цепочку включаем, если false - нет.
Проблемы
По ходу дела нам придется преодолеть ряд специфических трудностей. Трудность первая: мы не можем вызывать конструкторы базовых классов напрямую, поскольку это не позволит оборвать цепочку вызовов super() в нужном месте. Поэтому, копируя цепочки __proto__, мы заменяем конструкторы, вызывая из них старые при помощи apply. (И если вам кажется, что это может не сработать, вы правы: с этим связана вторая проблема, более серьезная. Но пойдем по порядку). Так вот, нам нужно сообразить, где хранить ссылки на эти новые конструкторы. Конечно, для этого есть стандартное поле constructor ; но если речь идет об "основных" базовых классах, из которых мы и делаем нашего "множественного наследника", то к их новым конструкторам необходимо иметь моментальный доступ из готового конструктора "множественного наследника" (поскольку именно оттуда мы будем их вызывать). Так что для них придется создать отдельный массив.
Трудность вторая, по сравнению с которой первая кажется пустячной: вызывая конструкторы базовых классов как функции, мы делаем это с помощью кода наподобие baseConstr.apply(this, baseArguments). С одной стороны, никакого выбора у нас нет: действительно, базовый конструктор должен подействовать на this и никак иначе. С другой стороны, при вызове super() в базовом конструкторе мы оказываемся в двусмысленной ситуации: ведь super вызывает функцию-конструктор, записанную в __constructor__ у прототипа текущего класса; а текущим классом мы сами указали this, а вовсе не тот базовый, конструктор которого вызывали. Поначалу возникает впечатление, будто бы трудность эта (вызванная тем, что мы пренебрегли системными средствами и вызываем конструкторы "вручную") непреодолима. Затем появляется слабая надежда: записать в this.__proto__. __constructor__ ссылку на ту функцию-конструктор, которая нам нужна, перед вызовом super(). Как ни удивительно, это срабатывает, и мы можем двигаться дальше.
Трудность третья: вызвать конструктор системного базового класса аналогично остальным конструкторам нам не удастся. Это связано именно с тем, что конструкторы большинства системных классов (вроде Array или String ) работают немного по-другому, чем обычные функции-конструкторы. В частности, они одновременно являются и функциями-операторами для преобразования типа. Видимо, потому-то сии функции определяют, вызваны ли они через new или непосредственно (в принципе, определить это довольно просто: при вызове из new значение arguments.caller равно null ). Так или иначе, вызов конструкторов системных классов через apply не срабатывает корректно. Трудность кажется непреодолимой, однако нас снова выручит искусственный прием. Нужно лишь вспомнить, что наследоваться от системного класса обыкновенным образом вполне возможно. А это значит, что вызов его конструктора через super должен работать (несмотря на то, что arguments.caller при вызове через super выдает уже не null ; так что здесь вступают в игру какие-то дополнительные встроенные механизмы). Итак, каким бы образом ни работал вызов конструктора системного базового класса через super, нам нужно воспользоваться именно этим методом. Но ведь настраивать ссылку на "текущий базовый конструктор" мы уже умеем! Так что необходимо сделать следующее. Функция, формирующая аргументы для системного базового класса, должна не возвращать массив с ними, а выполнять вызов наподобие super(arg1, arg2). А перед вызовом этой функции мы установим ссылку this.__proto__. __constructor__ указывающей на системный базовый класс. Вот пример, который подтвердит, что такой пр ием сработает. Выполним следующий код:
class1 = function(arg){this.a = 100; this.b = arg;} class2 = function(argClass, otherArgsArray){ argClass.apply(this, otherArgsArray); } func = function(someArgsArray){ super(someArgsArray[0], someArgsArray[1]); } class3 = function(argClass, otherArgsArray){ this.__proto__.__constructor__ = argClass; func.apply(this, [otherArgsArray]); } b = new class2(class1, [1000]); c = new class2(Array, [111, 222]); b1 = new class3(class1, [1000]); c1 = new class3(Array, [111, 222]);
Здесь "базовым классом" являются по очереди class1 и Array, а "производными" - class2 и class3. Мы пишем термины "базовый" и "производный" в кавычках, поскольку цепочку __proto__ мы здесь не создаем, а лишь проверяем, как работают разные нестандартные способы вызова базовых конструкторов. При этом конструктор "базового класса" в class2 вызывается при помощи apply, а в class3 - при помощи вспомогательной функции, внутри которой стоит вызов super(). К вспомогательной функции мы здесь прибегаем, чтобы создать "условия, приближенные к боевым", то есть сделать код максимально похожим на тот, что будет использоваться при реализации множественного наследования. Итак, запустив этот код на исполнение, а затем нажав Ctrl+Alt+V (вывод переменных), получим:
Level #0: Variable _level0.$version = "WIN 6,0,21,0" Variable _level0.class1 = [function '__constructor__'] Variable _level0.class2 = [function] Variable _level0.func = [function] Variable _level0.class3 = [function] { prototype:[object #5, class 'Object'] { __constructor__:[function '__constructor__'] } } Variable _level0.b = [object #7] { a:100, b:1000 } Variable _level0.c = [object #8] {} Variable _level0.b1 = [object #9] { a:100, b:1000 } Variable _level0.c1 = [object #10] [ 0:111, 1:222 ]
Отсюда видно, что при использовании в качестве базового класса созданного нами class1 работают оба способа (вызов и через apply, и через super ). А вот для системного класса Array подходит только последний способ.
Наконец, при написании функции, реализующей множественное наследование, будет полезно сделать некоторые приготовления на будущее. Далее мы поговорим о том, какие могут возникнуть у пользователя пожелания, когда он соберется наследоваться от сделанного при помощи multipleInherit класса. Особенно если это наследование в свою очередь будет множественным. Оказывается, чтобы этот процесс был наиболее удобным, полезно сохранить ссылку на массив базовых классов. Что мы и сделаем, прикрепив ссылку в качестве поля к возвращаемому из multipleInherit готовому конструктору.