Ответ 1
Добро пожаловать в цепочку прототипов!
Посмотрите, как он выглядит в вашем примере.
Проблема
Когда вы вызываете new Thing()
, вы создаете новый объект со свойством relatedThings
, который ссылается на массив. Поэтому мы можем сказать, что у нас есть это:
+--------------+
|Thing instance|
| |
| relatedThings|----> Array
+--------------+
Затем вы назначаете этот экземпляр ThingA.prototype
:
+--------------+
| ThingA | +--------------+
| | |Thing instance|
| prototype |----> | |
+--------------+ | relatedThings|----> Array
+--------------+
Таким образом, каждый экземпляр ThingA
будет наследоваться от экземпляра Thing
. Теперь вы создадите ThingA1
и ThingA2
и назначьте новый экземпляр ThingA
каждому из своих прототипов, а затем создадите экземпляры ThingA1
и ThingA2
(и ThingA
и Thing
, но не показано здесь).
Теперь отношение это (__proto__
является внутренним свойством, соединяющим объект с его прототипом):
+-------------+
| ThingA |
| |
+-------------+ | prototype |----+
| ThingA1 | +-------------+ |
| | |
| prototype |---> +--------------+ |
+-------------+ | ThingA | |
| instance (1) | |
| | |
+-------------+ | __proto__ |--------------+
| ThingA1 | +--------------+ |
| instance | ^ |
| | | v
| __proto__ |-----------+ +--------------+
+-------------+ |Thing instance|
| |
| relatedThings|---> Array
+-------------+ +--------------+ +--------------+
| ThingA2 | | ThingA | ^
| | | instance (2) | |
| prototype |---> | | |
+-------------+ | __proto__ |--------------+
+--------------+
+-------------+ ^
| ThingA2 | |
| instance | |
| | |
| __proto__ |-----------+
+-------------+
И из-за этого каждый экземпляр ThingA
, ThingA1
или ThingA2
относится к одному и тому же экземпляру массива.
Это не, что вы хотите!
Решение
Чтобы решить эту проблему, каждый экземпляр любого "подкласса" должен иметь свое собственное свойство relatedThings
. Вы можете добиться этого, вызвав родительский конструктор в каждом дочернем конструкторе, похожий на вызов super()
на других языках:
function ThingA() {
Thing.call(this);
}
function ThingA1() {
ThingA.call(this);
}
// ...
Это вызывает Thing
и ThingA
и устанавливает this
внутри этой функции на первый аргумент, который вы передаете на .call
. Подробнее о .call
[MDN] и this
[MDN].
Только это изменит изображение выше:
+-------------+
| ThingA |
| |
+-------------+ | prototype |----+
| ThingA1 | +-------------+ |
| | |
| prototype |---> +--------------+ |
+-------------+ | ThingA | |
| instance (1) | |
| | |
| relatedThings|---> Array |
+-------------+ | __proto__ |--------------+
| ThingA1 | +--------------+ |
| instance | ^ |
| | | |
|relatedThings|---> Array | v
| __proto__ |-----------+ +--------------+
+-------------+ |Thing instance|
| |
| relatedThings|---> Array
+-------------+ +--------------+ +--------------+
| ThingA2 | | ThingA | ^
| | | instance (2) | |
| prototype |---> | | |
+-------------+ | relatedThings|---> Array |
| __proto__ |--------------+
+--------------+
+-------------+ ^
| ThingA2 | |
| instance | |
| | |
|relatedThings|---> Array |
| __proto__ |-----------+
+-------------+
Как вы можете видеть, каждый экземпляр имеет свое собственное свойство relatedThings
, которое ссылается на другой экземпляр массива. В цепочке прототипов все еще есть свойства relatedThings
, но все они затенены свойством экземпляра.
Лучшее наследование
Кроме того, не устанавливайте прототип с помощью:
ThingA.prototype = new Thing();
На самом деле вы не хотите создавать новый экземпляр Thing
здесь. Что произойдет, если Thing
ожидаемые аргументы? Какой из них вы пройдете? Что делать, если вызов конструктора Thing
имеет побочные эффекты?
На самом деле вы хотите подключить Thing.prototype
к цепочке прототипов. Вы можете сделать это с помощью Object.create
[MDN]:
ThingA.prototype = Object.create(Thing.prototype);
Все, что происходит при выполнении конструктора (Thing
), произойдет позже, когда мы фактически создадим новый экземпляр ThingA
(вызывая Thing.call(this)
, как показано выше).