Comments 120
Довольно логичное поведение (до инициализации инстанса класса-наследника должен отработать конструктор базового класса — а значит, инициализация полей наследника ещё не выполнена) и ьросающийся в глаза smelly code (в конструкторе дёргаете методы, опирающиеся на то, что объект уже готов...)
Жаль, что нынче учат программированию, начиная не с паскаля: в Objective Pascal многие нюансы были бы видны из кода, и при смене языка привычки бы остались.
А решение простое. Убрать из конструктора любую логику, кроме создания объекта. Конструктор должен выполнять ровно одну задачу: заполнить все поля так, чтобы соблюдались инварианты (в случае наследования — те поля, что отличаются от базового класса). Всё прочее выносите в функцию Init или ещё какую и вызывайте её явно.
Нет, не достаточно. Порядок инициализации зависит от того, как записана переменная. Например getFoo() { return 'foo' }
и getFoo = () => { return 'foo '; }
будут инициализированы в разном порядке в классе. Было бы хорошо найти где это прописано в спецификации языка, но пока что все тут говорят что это "очевидно".
Они не просто инициальзируются в разном порядке, первый вариант — это объявление метода. Этот метод попадает в прототип. Второй вариант — объявление поля, в которое записывается фунция. Это поле инициализируется в каждом экземпляре класса. Из чего следует еще один забавный момент: если попытаться унаследоваться от такого класса, то через super
, такая функция доступна не будет.
Вообще я имел в виду код на js. Но тот же мысленный эксперимент на java или c# (вызвать конструктор базового класса в середине конструктора класса-потомка, после инициализации его полей) даст ту же ошибку: присваивания полей в конструкторе базового класса выполнятся позже и перкроют значения, присвоенные потомком.
Тоесть этот код вызовет ошибку или не будет использовать значение свойства наследника, ибо в конструкторе оно еще не должно быть инициализировано?
qwe
asd
1. инициализируется проперти родителя
2. инициализируется проперти наследника
3. выполняется конструктор родителя
4. выполняется конструктор наследника
Странно что в отличии от жс, оба проперти инициализируются до первого конструктора. В этом то и проблема, и именно на нее автор и обратил внимания.
1. инициализация проперти родителя
2. вызов конструктора родителя
3. инициализация проперти наследника
4. вызов конструктора наследника
Вроде мелочь, но все же могут быть моменты которые ударят по пальцам, как в примере с кодом автора. Так что фраза «логичное поведение» не совсем подходит, и что что писали что «надо делать по другому ибо так правильно» тоже не катит, не все не всегда пишут как надо, жс позволяет писать неправильно, закрывает глаза на такое и ведет себя не так как другие языки.
И я еще раз повторю, жс ведет себя не очевидно для людей которые не изучали как работает жс специально.
ibb.co/WWLhcLb — ссылочка на скрин выполнения
gist.github.com/0xff00ff/bf9010cc4ac2e57c8edd5cf87925c1c9 — а это ссылочка на гист который вы можете запустить и проверить что я не написал на вскидку.
А ведь для кого-то это поведение очевидно ;)
Но есть и хорошие новости, я знаю язык в котором именно так и работает, в базовом конструкторе используется this наследника, если работаете с наследником и у него нет конструктора. В *язык который нельзя называть*
Возьмите ваш пример, поменяйте конструктор B и запустите в dotnetfiddle.net:
public B(): base() {
Console.WriteLine(prop);
Console.WriteLine(base.prop);
}
или в Main:
B b = new B();
Console.WriteLine(b.prop);
Console.WriteLine(((A)b).prop);
То, что вы назвали его prop в B, не меняет ровным счетом ничего, вы могли бы назвать его любым другим именем с тем же успехом. В этом принципиальное отличие .NET от JS — вы не можете «переопределить» поле.
В жс по сути тоже ведь не переопределение идет а создание нового поля.
Я изначально говорил о другом, порядок инициализации в c# и жс разные, но в конечном итоге staticlab правильно заметил что таким образом я отошел от реалий и полез не в те дебри.
А за сервис dotnetfiddle.net спасибо, я как-то и не знал про него.
1. инициализируется проперти родителя
3. выполняется конструктор родителя
2. инициализируется проперти наследника
4. выполняется конструктор наследника
пруфы все дела, тока свойства местами перепутал, но это не сильно меняет смысл
using System;
class Program
{
static void Main()
{
new B();
}
class A
{
private const string ClassName = nameof(A);
private const string FieldName = nameof(_value1);
string _value1 = SetHelper.SetField("Value A-1", ClassName, FieldName);
public A()
{
Console.WriteLine($"Entered {ClassName} constructor, current {FieldName} is \"{_value1}\".");
_value1 = SetHelper.SetField("Value A-2", ClassName, FieldName);
Console.WriteLine($"Exiting {ClassName} constructor, current {FieldName} is \"{_value1}\".");
}
}
class B : A
{
private const string ClassName = nameof(B);
private const string FieldName = nameof(_value2);
string _value2 = SetHelper.SetField("Value B-1", ClassName, FieldName);
public B()
{
Console.WriteLine($"Entered {ClassName} constructor, current {FieldName} is \"{_value2}\".");
_value2 = SetHelper.SetField("Value B-2", ClassName, FieldName);
Console.WriteLine($"Exiting {ClassName} constructor, current {FieldName} is \"{_value2}\".");
}
}
static class SetHelper
{
public static TМalue SetField<TМalue>(TМalue value, string className, string fieldName)
{
Console.WriteLine($"The field {fieldName} of {className} is set to \"{value}\".");
return value;
}
}
}
//The field _value2 of B is set to "Value B-1".
//The field _value1 of A is set to "Value A-1".
//Entered A constructor, current _value1 is "Value A-1".
//The field _value1 of A is set to "Value A-2".
//Exiting A constructor, current _value1 is "Value A-2".
//Entered B constructor, current _value2 is "Value B-1".
//The field _value2 of B is set to "Value B-2".
//Exiting B constructor, current _value2 is "Value B-2".
Оба неправы, но это не имеет значения, учитывая, что базовый класс ни при каких условиях не имеет доступа к полям наследника без вызова виртуальных методов.
Все правильно, как я и писал, сперва поля, затем конструкторы, в жс шаг за шагом это происходит, базовое поле, базовый конструктор, наследуемое поле, наследуемый конструктор итд по цепочке, что собственно тоже логично исходя их того что это прототипная штука, и оно динамически проходит всю цепочку шаг за шагом
Верно, и в отличии от жс, дотнет ведет себя немного по другому:
Разница между шарпом и js тут не из-за порядка инициализации, а из-за того, что в js поле предка и поле потомка — это одно и то же поле, а в шарпе — это два разных поля. По-этому в шарпе (в отличии от js) результат вообще не зависит от порядка инициализации (он мог бы зависеть если бы вы инициализировали поле вызовом метода базового класса, но в field initializers нельзя использовать this/base и, с-но, это одна из двух основных причин по которым их там использовать запрещено).
У вас есть первое поле — с одним значением и второе поле — с другим значением. И не важно, какое из них инициализировано первым, а какое вторым. Значение полей не меняется ни в какой момент.
В случае же js поле сперва инициализируется (в конструкторе базового класса) а потом перезаписывается (в конструкторе производного).
class a
{
public string x = "x";
public a(string s)
{
x = s;
}
public override string ToString()
{
return x;
}
}
class b : a
{
public b() : base("b") { }
}
class Program
{
static void Main(string[] args)
{
var x = new b();
Console.WriteLine(x); //print b
Console.ReadLine();
}
}
public string x;
public a(string s)
{
x = "x"
x = s;
}
Единственный способ вызвать конструктор без рефлекшена — использовать ключевое слов new.Что же тогда делает ключевое слово base?
Что же тогда делает ключевое слово base?
Конструктор через base вызвать нельзя.
т.е. вот такое вот:
class A
{
public string x;
public A(string s)
{
x = s;
}
}
class B: A
{
public B() {
base("b");
}
}
не скомпилируется
В моём примере есть правильный вызов конструктора базового класса.
public b() : base("b")
Иначе нельзя, но это именно явный вызов конструктора.
В моём примере есть правильный вызов конструктора базового класса.
Здесь нету вызова конструктора, т.к. указанный вами кусок кода с base не является statement, т.е. не исполняется. Вам же KostaArnorsky выше объяснил все. Указать конструктор базового класса дял конструктора производного — можно. Вызвать конструктор базового класса — нельзя. Он вызывается сам, неявно.
- Call a method on the base class that has been overridden by another method.
- Specify which base-class constructor should be called when creating instances of the derived class.
И да, вызывается он до исполнения конструктора наследника.
Разве нет?
Еще раз, он неявно вызывается. Вызвать его сами вы не можете.
В строчке 'public b(): base("b")' нет вызова конструктора. Это вообще не исполняемый код, это объявление.
Точно так же и конструктор конкретного класса вызывается «неявно» при создании экземпляра, хотя его исполнение происходит вполне явно в конкретном месте кода.
Да нет, вполне явно. И код вполне исполняемый.
Нет не явно, нет не исполняемый. Это часть объявления конструктора, еще раз. Что вам в этом непонятно? Этот код не исполняется ни в какой момент работы программы. base('b') не является валидным statement/expression в c#.
Просто нет другого штатного способа вызвать.
Его ВООБЩЕ нет способа вызвать. Приведенный вами код — это не вызов конструктора базового класса. Это часть объявления конструктора производного.
Точно так же и конструктор конкретного класса вызывается «неявно» при создании экземпляра
При создании экземпляра вы как раз вызываете конструктор явно. Вы пишите expression, который вызывает конструктор.
Этот код не исполняется ни в какой момент работы программы.Не исполняется, а в отладчик попасть можно прямо перед вызовом именно этого кода?
Это часть объявления конструктора производного.одно не отменяет другого. Вызов происходит при входе в конструктор наследника.
При создании экземпляра вы как раз вызываете конструктор явно— Нет, вы пишете new, который приводит к вызову конструктора наследника, который приводит к вызову конструктора базового класса…
Этот код не исполняется ни в какой момент работы программы.Посмотрите, во что превращается этот код через ildasm, тут видно, что он именно вызывается и именно там, где он написан:
Я ни в коей мере не считаю себя специалистом высокого класса, но я изучил достаточно много книг и статей, связанных с фронтенд-разработкой, и я не разу не встречал информации по описанной мной теме
Надо будет поискать. Думаю, в спецификации всё есть, но наверняка найдётся и что-то написанное простым языком.
Но я бы сформулировал вопрос иначе: можете ли найти ссылку на документацию, где описано другое поведение?
Очевидно, нет. Значит, закладываться на другое поведение причин не было.
Не знать конкретное поведение в такой ситуации – не проблема. Не знаешь, как будет работать такой код? Просто напиши другой, про который знаешь.
Не знать конкретное поведение в такой ситуации – не проблема. Не знаешь, как будет работать такой код? Просто напиши другой, про который знаешь.
Я вот не знал, как будет работать такой код. Разобрался и поделился с сообществом тем, что на мой взгляд может быть кому-то полезно. А мог бы написать другой код, про который знаю, и кто-то мне бы потом рассказывал, что, мол, если бы я начинал с Паскаля, мне не пришлось бы изобретать велосипеды.
Хотелось бы напомнить, что бОльшую часть истории языка Javascript в нём в принципе не было такой сущности, как class
, и разработчикам принципиально не нужно было знать поведение системы в таких ситуациях
Напомню также, что традиционная реализация классов для js (определение методов prototype конструктора) придумана очень давно, и позволяет предположить, какое поведение тут будет.
Но, тем не менее, использовать тонкости языка не стоит. Вы можете их знать, а вот тот, кто будет читать ваш код – нет. Старое правило: "пиши код так, будто сопровождать его будет склонный к насилию психопат, знающий, где ты живёшь".
Т.е. в вашем случае разобраться в поведении – хорошая идея (вы повысили свою и не только свою квалификацию), а вот использовать это в коде – нежелательно (лучше оставить конструктор простым)
А можете дать ссылку на то, где это прописано в спецификации? В https://tc39.es/ecma262/#sec-class-definitions я только вижу грамматики класса, но не то, как он должен инициализироваться.
Согласен. Наткнулся я на эту проблему именно в связи с тем, что в Marionette по сути конструктор ты не объявляешь, а исполнялся и перерабатывался в конструктор как раз метод initialize
Вот только про "не используйте глобальные переменные" только что на заборе не пишут, а вот с проблемой излишней логики в конструкторах я, например, столкнулся впервые
автор, а Вас не смущает, что в классе-наследнике нельзя обращаться к this
до тех пор пока не проинициализируется базовый класс (с помощью super
)? Вполне логично, что переменные наследников еще не будут проинициализированы.
Я не вижу причины, кроме выбора авторов языка, почему объявленные поля класса-наследника не могут быть инициализированы до вызова конструктора. Поля родительского класса определяются именно до вызова конструктора, и ожидать такое поведение от класса-наследника мне не кажется какой-то несусветной глупостью
Довольно очевидно, на самом деле. Попробуйте по шагам убрать синтаксический сахар:
- Заполнение полей вносим внутрь конструктора (в его начало).
1.1. То же самое для конструктора базового класса. - Вызов конструктора базового класса вносим в начало конструктора производного класса. Как раз попадёт перед заполнением полей (после – нельзя, в этот момент мы уже ожидаем, что объект базового класса готов)
- Читаем полученный код.
Вопрос по второму пункту. Почему нельзя? Почему мы в этот момент ожидаем, что объект базового класса уже готов? В памяти нигде нет ОТДЕЛЬНОГО объекта базового класса, есть один объект, в который добавляются свойства и методы.
На мой взгляд, это вполне могло быть реализовано внутри как
this.prop = Subclass.prototype.prop || Baseclass.prototype.prop, а потом уже вызов конструктора с проинициализированными полями.
Да, был выбран другой способ, но это не значит что он очевиден
Да, был выбран другой способ, но это не значит что он очевиден
Просмотрел внимательно (и вы тоже можете) заголовки тикетов; никому не приходило в голову, что текущий способ неочевидный. Никто не заводил такой тикет. Очевидно, что неочевидно это только для вас :)
И вы по-прежнему можете завести такой тикет, несмотря на то, что stage-3 означает, что все основные вопросы уже решены.
https://isocpp.org/wiki/faq/strange-inheritance
https://www.codeproject.com/Tips/641610/Be-Careful-with-Virtual-Method
https://lustforge.com/2014/02/08/dont-call-non-final-methods-from-your-constructor-please/
По итогу написания этой статьи, я смог найти довольно немало статей разного возраста про другие ЯП на английском, в которых описывается эта проблема. Это значит, что кому-то ещё это приходило в голову, а значит не так уж это и очевидно
И да, по приведённой Вами ссылке есть тикет на довольно близкую тему
https://github.com/tc39/proposal-class-fields/issues/151
Если бы дело конструктора было только в инициализации состояния, любые другие действия вообще запрещалось бы делать на уровне языка. Да и понятие инициализации состояния — довольно размытое, конкретно в моём случае, например, метод this.render просто неудачно назван, он не создаёт никаких dom элементов и не изменяет сторонние объекты, он просто компилирует в себя шаблон.
Это вполне подходит под понятие инициализации состояния, как я считаю
А в приведённых выше ссылках например написано, что для языка C# инициализация переменных класса-наследника происходит ДО вызова конструктора родителя, что уже делает наш спор не таким однозначным.
И само наличие десятков (я привёл маленькую выборку) статей на подобные темы значит именно то, что у других наших коллег тоже возникают вопросы по этому поводу, разве нет?
И само наличие десятков (я привёл маленькую выборку) статей на подобные темы значит именно то, что у других наших коллег тоже возникают вопросы по этому поводу, разве нет?Да темы не такие уж похожие. И по большей части разъясняют неучам правильные подходы к программированию.
Большая часть вопросов возникает из желания писать код абы как.
Вот в вашем пример есть одно концептуальное недоразумение: у вас template это свойство, а не поле. Но это историческое наследие JS — там просто не было пропертей. И для обращения к полю базового типа надо было к нему явно обращаться. Сейчас добавили синтаксис классов, но способ обращения к элементам и последовательность инициализации никуда не делись.
Попробуйте ответить на свой вопрос сами. Для вашего примера выполните руками пункт 1 (и 1.1) и проведите эксперимент – вставьте вызов конструктора базового класса не в начало. Какую ошибку получим?
Почему мы в этот момент ожидаем, что объект базового класса уже готов?Да хотя бы потому, что вы пытаетесь обратиться к полю базового класса для изменения значения — для этого класс уже должен быть проинициализирован, а это происходит после вызова конструктора.
Попробуйте в наследника добавить конструктор и там обратиться к полю — станет яснее.
В JS наследование реализовано с помощью прототипов.
Поэтому в конструкторе наследника сначала полностью конструируется объект базового класса, а потом этот объект дополняется до объекта класса наследника.
Убириание синтаксического сахара не поможет. Напримеh:
class A {
getOne() { return 'One'; }
getTwo = () => { return 'two'; }
}
Я ожидал, что getOne
— это синтактический сахар, который эквивалентен getTwo
. Но это не так, они будут инициализированы в разном порядке:
class A {
getOne() { return 'one from A'; }
getTwo = () => { return 'two from A'; }
constructor() {
console.log('getOne=' + this.getOne());
console.log('getTwo=' + this.getTwo());
}
}
class B extends A {
getOne() { return 'one from B'; }
getTwo = () => { return 'two from B'; }
}
new B();
Выведет
getOne=one from B
getTwo=two from A
Зря ожидали, потому что эквивалентный код — вот такой:
function A() {
this.getTwo = () => { return 'two'; }
}
A.prototype.getOne = function () {
return 'One';
}
Да, я уже понял, что я зря этого ожидал исходя из результатов, которые я получил. Написание правильного эквивалентного кода далеко не самая простая задача, как многие в комментариях тут рассуждают. Мне еще интересно исходя из какой документации/спецификации я могу понять, что правильные код именно ваш. Я конечно могу написать оба и сравнить что они делают, но хочется увидеть официальную спецификацию.
class BaseTooltip {
template = 'baseTemplate'
constructor(options) {
this.options = options
}
render(content) {
console.log('render:', content, this.template, this.options)
}
}
const tooltip = new BaseTooltip(options)
tooltip.render('content')
Просто кто-то не знает как работает прототипное наследование. Не буду оригинальным — RFM.
А при чём тут прототипное наследование?
Если бы свойство template попало в прототип — то и никакой проблемы бы как раз не было.
Да, виноват, по диагонали код автора прочитал. Там впринципе тупо сделано.
Пожалуй надо было так:
class BaseTooltip {
type = 'baseTooltip';
content;
constructor(someField) {
this.content = someField;
}
render() {
console.log(this.type, this.content);
}
}
class SpecialTooltip extends BaseTooltip {
type = 'specialTooltip';
constructor(content) {
super(content);
}
}
const newChild = new SpecialTooltip('child');
newChild.render();
П.С. Однако свойство template все же берется из родителя. В обычном наследовании, однако, результат будет аналогичным.
del
На самом деле поведение очень даже логичное и понятное. Разберу пример:
class A {
name = 'A'
constructor() {
this.log()
}
log() {
console.log(this.name)
}
}
class B extends A {
name = 'B'
}
new B
// A
Почему так происходит?
Всё на самом деле очень просто — в классе A
не указан конструктор, соответственно используется конструктор "по умолчанию" т.е. класс выглядит так:
class B extends A {
name = 'B'
constructor() {
super()
}
}
Любой наследующий класс должен в своём конструкторе сначала вызывать конструктор родительского класса (делается это через super()
) и только потом производить необходимые манипуляции с инстансом.
Соответственно поле name
указанное в классе B
будет установлено только после выполнения конструктора класса A
.
Поэтапное создание инстанса класса B
будет выглядить так:
1. Object.constructor() -> Object {}
2. A.name = 'A' -> A { name: 'A', log() {...} }
3. A.constructor() -> A { name: 'A', log() {...} }
4. A.log() -> A { name: 'A', log() {...} }
5. B.name = 'B' -> B { name: 'B', log() {...} }
6. B.constructor() -> B { name: 'B', log() {...} }
Т.е. на момент вызова метода log()
инстанс класса B
содержит в поле name
значение A
, .
Как добиться ожидаемого в данном примере поведения?
- Есть к примеру способ, который указал vvadzim, но он требует переноса инициализации в отдельный метод.
- Но есть и более простой способ, не требующий переноса инициализации в отдельный метод.
Что это за способ? Ответ: Вызов метода log()
в микротаске, но как? Есть опять таки два равносильных варианта:
// 1 вариант - Микротаск через Promise
class A {
name = 'A'
constructor() {
Promise.resolve().then(() => this.log())
}
log() {
console.log(this.name)
}
}
// 2 вариант - Микротаск по запросу
class A {
name = 'A'
constructor() {
queueMicrotask(() => this.log())
}
log() {
console.log(this.name)
}
}
Команда Polymer Project в проекте lit-element по сути так же использует микротаски (через метод _enqueueUpdate).
class B extends A {
name = 'B'
constructor() {
console.log('B constructor start');
super();
console.log('B constructor finish');
}
}
new B();
То лог будет:
B constructor start
A
B constructor finish
И я согласен с автором, что это далеко не очевидно, что инициализация name в B происходит после вызова super().
К тому же, если переписать код и вместо переменной name использовать метод getName тогда работает так, как автор и ожидал:
class A {
getName() {
return 'A';
}
constructor() {
this.log()
}
log() {
console.log(this.getName());
}
}
class B extends A {
getName() {
return 'B';
}
}
new B();
<source>
выведет "B". По моему личному мнению это не очевидно, что инициализация переменных и методов в классе происходит в разном порядке. Особенно учитывая что в JS разница между переменной и методом небольшая и в до ES6 классов методы и были переменными функциями.
Поэтапное создание класса выглядит не совсем так. B.constructor() будет вызван первым.
Совершенно верно, но я указал именно данный порядок, чтобы было яснее, какое значение поля name
будет во время вызова метода log()
. Если проще я просто обратную цепочку (из вложения) описал.
Правильнее (подробнее) ваш вариант тогда написать так:
class A {
name = 'A'
constructor() {
console.log('A constructor start')
this.log()
console.log('A constructor finish')
}
log() {
console.log(this.name)
}
}
class B extends A {
name = 'B'
constructor() {
console.log('B constructor start')
super()
console.log('B constructor finish')
}
}
new B()
B constructor start
A constructor start
A
A constructor finish
B constructor finish
И я согласен с автором, что это далеко не очевидно, что инициализация name в B происходит после вызова super().
И всё же на это указывает как спецификация ECMAScript, так и рантайм, если вы попробуете любым образом обратиться к любому полю/методу или установить поле наследующего класса до вызова super()
.
class B extends A {
constructor() {
this.name = 'B'
super()
}
}
new B()
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
at new B (<anonymous>:15:5)
at <anonymous>:20:1
Т.е. следующие три варианта эквивалентны за исключением того, что в первом и втором случаях поле устанавливается по семантике [[define]], а в третьем по семантике [[set]].
// Первый
class B extends A {
name = 'B'
}
// Второй (эквивалент первого с точностью до принципа установки поля)
class B extends A {
constructor() {
super()
Object.defineProperty(this, 'name', { value: 'B' })
}
}
// Третий
class B extends A {
constructor() {
super()
this.name = 'B'
}
}
К тому же, если переписать код и вместо переменной name использовать метод getName тогда работает так, как автор и ожидал:
В данном случае работает как ожидается только ввиду того, что метод эта та же функция и объявляется он в блоке (скоупе) класса. Как и обычная функция в глобальной области она просто всплывёт вверх и уже к вызову в конструкторе имеет необходимую сигнатуру.
К слову в моем описании поэтапной инициализации инстанса метод log() появился уже на этапе присвоения значения полю name
.
Т.е. поля и методы класса при объявлении ведут себя как function, var, let, const в любой области видимости.
LEXA_JA правильно указал, что методы попадают в prototype, а поля непосредственно в объект (по сути таким образом и соблюдается стандартный порядок объявления функций и переменных в областях видимости, но с поправкой на специфику классов). Прототип класса уже полностью известен и сформирован на момент создания инстанса.
И ответ на ваш комментарий в соседней ветке: https://github.com/tc39/proposal-class-fields.
Опять таки я указал способы достижения ожидаемого результата, используя микротаск — всё, что нам нужно это вызвать условную функцию log()
в следующем такте исполнения.
И я согласен с автором, что это далеко не очевидно, что инициализация name в B происходит после вызова super().
Это полностью очевидно. Во время инициализации полей наследника предок должен быть инициализирован, т.к. вы имеете право при инициализации вызывать через super методы предка. Если бы предок не был инициализирован, вы бы их вызывать не могли (т.е. методы бы, вообще говоря, вызывались, но возвращали бы дичь, т.к. отрабатывали бы на неинициализированом классе).
Половина комментаторов нам тут говорят, что при инициализации методы предка дёргать нельзя, а вы утверждаете, что предок инициализируется полностью именно для того, чтобы дёргать его методы.
Но мой основной вопрос всё равно не про это — почему инстанс класса предка сразу бы не инициализировать со значениями полей, переопределёнными в классе-потомке?
Все методы попадают в прототип т.е. все методы уже определены ещё до инициализации любого инстанса. А вот поля в инстансе устанавливаются именно в момент инициализации и именно в порядке из глубины (от самого первого предка).
Но мой основной вопрос всё равно не про это — почему инстанс класса предка сразу бы не инициализировать со значениями полей, переопределёнными в классе-потомке?
По тому, что во первых в ECMAScript классы это синтаксический сахар, во вторых вложенность не известна и процесс по своей сути итеративный. А методы так же как и функции получают ровно те значения полей и переменных своих областей видимости, которые имеются на момент вызова функции.
Пример ниже по своей сути ведёт себя совершенно так же как и ваш случай с классом.
function greet() {
console.log(`Hello, ${name}!`)
}
function constructorA() {
Object() // В случае с классами на данном месте неявно вызывается конструктор объекта (super()).
name = 'ninja cat' // Установка условного поля `name` в родительском классе.
greet()
}
function constructorB() {
constructorA() // Это место явного вызова super()
name = 'world' // Установка условного поля `name` в дочернем классе.
}
constructorB()
// Hello, ninja cat!
Данный пример показывает именно то, как представляется синтаксический сахар класса в рантайме (но на обычных функциях).
А это упрощенный пример, показывающий, что любая функция/метод использует значение переменной скоупа на момент вызова. Обращаю внимание, что на момент объявления функции greet() в скоупе о переменной name вообще ничего не известно.
function greet() {
console.log(`Hello, ${name}!`)
}
let name = 'ninja cat'
greet()
// Hello, ninja cat!
name = 'world'
greet()
// Hello, world!
Половина комментаторов нам тут говорят, что при инициализации методы предка дёргать нельзя
Комментаторы вам говорят, что методы дёргать нежелательно, потому что сайд-эффекты в конструкторе всегда были скользким моментом в любой реализации ООП.
Половина комментаторов нам тут говорят, что при инициализации методы предка дёргать нельзя
Методы предка? Конечно можно, с чего бы нет. После вызова super() (а предполагается что он всегда вызывается первым) базовый класс считается инициализированным. А раз он инициализирован — вы можете на нем вызывать что хотите, с чего бы это было проблемой? Вы когда, как в примере, значение поля базового класса в конструкторе меняете — то тоже, формально, вызываете метод предка. Сеттер данного поля.
Но мой основной вопрос всё равно не про это — почему инстанс класса предка сразу бы не инициализировать со значениями полей, переопределёнными в классе-потомке?
Потому что поле не переопределяется. Это одно и то же поле. Оно определено только один раз в базовом классе. В производном классе вы только меняете значение данного поля, причем меняете его в конструкторе производного класса. Запись field = something — это часть конструктора, а не определение.
А раз конструктор производного класса вызывается после конструктора базового — то в конструкторе базового класса поле имеет старое значение.
Это все абсолютно стандартное поведение для ООП-языков.
Как обычно набежала куча любителей самоутверждения чтения документации. Хабру явно не хватает возможности отключать комментарии.
v1vendi на многих ЯП ты бы получил ожидаемый результат:
class BaseTooltip:
template = 'baseTemplate'
def __init__(self, content):
self.render(content)
def render(self, content):
print('render:', content, self.template)
BaseTooltip('content')
class SpecialTooltip(BaseTooltip):
template = 'otherTemplate'
SpecialTooltip('otherContent')
# render: content baseTemplate
# render: otherContent otherTemplate
, плюс все (которые я видел) обёртки имитирующие классы до ES6 вели себя именно так. Я тоже когда-то попался на этом хоть и заглядываю в спецификацию.
UPD: одно из решений — использование статических свойств с обращением к ним через this.constructor
.
Python.
class BaseTooltip:
template = 'baseTemplate'
def __init__(self, content):
self.render(content)
def render(self, content):
print('render:', content, self.template)
base = BaseTooltip('content')
class SpecialTooltip(BaseTooltip):
template = 'otherTemplate'
derived = SpecialTooltip('otherContent')
SpecialTooltip.template = "I don't know python"
derived.render('finalContent')
#('render:', 'content', 'baseTemplate')
#('render:', 'otherContent', 'otherTemplate')
#('render:', 'finalContent', "I don't know python")
Поля, которые объекту не принадлежат в принципе, разумеется, в конструкторе не инициализируются… а как иначе-то?
Это, вроде как, не совсем то, чего хотел топикстартер…
что человек его не знает
не претендую.
Это, вроде как, не совсем то, чего хотел топикстартер
почему? Вроде именно этого он и хотел. Какие недостатки у такого решения для его задачи?
почему? Вроде именно этого он и хотел.Всё зависит от того — хочет ли он этот самый
template
, в какой-то момент, менять.Какие недостатки у такого решения для его задачи?Если это свойство не должно меняться, то, в общем-то, никаких. Просто в случае с ES6 (вернее ES6 + куча пропозалов) вы получаете поле объекта, а в python — это-таки поле класса.
Собственно как можно заметить из синтаксиса Python
__init__
конструктором, в общем-то, и не является. Потому и вызов конструктора предка нужно осуществлять явно.Объектная можель Python, в принципе, ближе скорее к Turbo Pascal (про который я уже писал). Тот же __new__ сразу получает финальный класс, никакой «цепочки конструкторов» там в принципе нету.
получаете поле объекта, а в python — это-таки поле класса
ок, в общем-то я и и предлагаю использовать поля класса, на js будет так:
class BaseTooltip {
static template = 'baseTemplate'
constructor(content) {
this.render(content)
}
render(content) {
console.log('render:', content, this.constructor.template)
}
}
new BaseTooltip('content')
class SpecialTooltip extends BaseTooltip {
static template = 'otherTemplate'
}
new SpecialTooltip('otherContent')
// render: content baseTemplate
// render: otherContent otherTemplate
Всё зависит от того — хочет ли он этот самый template, в какой-то момент, менять.
в случае с шаблоном, если хочется что-то менять по условию, то стоит задуматься об использовании шаблонизатора и описывать эти изменения уже внутри шаблона.
Это довольно известная проблема Backbone (и Marionette тоже). Вот тут на гитхабе есть обсуждение с возможными решениями: https://github.com/jashkenas/backbone/issues/3560
Все логично. В c++ тоже так. И вообще есть хорошая практика что конструкторы не должны содержать никаких side effect. Но периодически появляются такие статьи как эта
Неожиданный порядок инициализации наследованных классов в JavaScript