Search
Write a publication
Pull to refresh

Comments 37

Приведите, пожалуйста, пример реальной задачи, где требуется использовать ancestor() с указанием уровня родителя, чтобы выполнить его метод (это ж надо ещё помнить и считать, а если добавился промежуточный класс, то вообще пересчитывать во всех местах).
По-моему, если возникает такая необходимость, значит точно что-то не в порядке с архитектурой.
допустим

class A {
  doSomething: function(){}
}
class B extends A {
  doSomething: function() {
    parent.doSomething();
    // допустим приводим к новому формату, похоже на реализацию паттерна Proxy внутри метода
  }
}

хотя у меня попадались и другие варианты (например возврат к старой формуле при несоблюдении условий применимости новой)

единственное, мне не нравится число в ancestor, я предпочитаю this.parent.parent.parent.X
Спасибо за пример.
theOnlyBoy: писать конкретный пример — очень долго. Я в статье описал пример, который часто возникает — "… но бывает нужно повторить старый метод, а затем немного его дополнить", и tzlom тоже отразил это как частный случай. А у Вас получается, что если Вы отходите от классического наследования, то «точно что-то не в порядке с архитектурой» :). Модель задачи не обязана подчиняться модели наследования, что она часто с успехом демонстрирует. Другими словами, наследование с перекрытием методов предков — вовсе не закон природы.

>… а если добавился промежуточный класс, то вообще пересчитывать во всех местах
Добавить промежуточный класс — по-моему, более чем серьёзное изменение модели задачи, чтобы, так сказать, «плакать по волосам». Но и тут моя функция имеет шанс быть автоматизированной (в отличие от .parent.parent.parent.X, точнее, .parent.X тогда неизбежно придёт к .ancestor() ). Пишем у родителя свойство methodBase: 1 — признак этого уникального родителя. А обращение к предку делаем в рекурсии, следя за наличием уникального свойства. Тогда, сколько бы классов вовнутрь не вставили, наша parentTo('methodBase') обратится методу с признаком. Так что для Вашего примера мою функцию я так бы переписал. А пока она, действительно, — для статической структуры классов.
Я не смог вовремя ответить, AndrewSumin вам там хорош написал, и я с ним согласен.
ancestor() — это неудобоваримый костыль. Я не знаю ни одной парадигмы, где бы подобное было стандартом.
Да, звучит у него правдоподобно. У меня на самом деле задача — типа древовидного наследования (сейчас в разработке, поэтому подробности — в личку). А в JS все деревья наследования приходится выстраивать в линию (во 2-й части статьи собираюсь об этом упомянуть и продемонстрировать, иначе instanceof не выходит). Когда имеется хотя бы 2 родителя — поневоле приходится выстраивать их в линию и хотя бы один из них отстоит на 2 шага по цепочке. Вот вам и необходимость обращения не на 1 шаг назад, а на несколько. И тут гораздо лучше выглядит .ancestor(..., 2), чем цепочка служебных методов.
Я предлагаю всетаки смириться с тем что JS prototype based и использовать эту его свойство по полной.
Важно пнимать что прототип самостоятельный объект, а это значит что если мы хотим вызвать его метод то надо всего-то явно это записать.

function Bar(){
  this.method = function(){...}
}
var bar = new Bar();

function Foo(){
  this.method = function(){
    bar.method.bind(this); // Вызываем метод родителя в своем контексте
    bar.method.bind(bar); // Вызываем метод родителя в контексте родителя
    ...
  }
}
Foo.prototype = bar;
var foo = new Foo();


Я понимаю что принятие друго парадигмы бывает сложным, но надо сделать над собой усилие.
Я понимаю, что может показаться, что эти движения обусловлены желанием остаться в рамках классического наследования. Но разве само наличие обхода классического наследования через доступ к предкам не говорит о желании создать и использовать неклассическое? Просто для начала нужно, чтобы действия были понятны в рамках классических, потому что их все знают (наследование и указание на конструктор, который, всё же, не сможет в одиночку использоваться для доступа к предку), а потом их расширять.

Затем, прототип у меня и без того используется «по полной» — пишу .ancestor() в метод первого предка, а не каждому объекту, объекты наследую через прототипы, а не через прямое прописывание. Даже не пользуюсь традиционными this.x = a; — они не работают, как подчёркнуто в статье. Это наследие исходной функции и обещаю его преодолеть в следующей итерации.

Вот Вы кодом показали, как это сделать, правильно. Я тоже собираюсь идти по этому пути. Но почему Вы идёте по стопам классики? В Вашем коде
1) правильно работает индикация наследования — alert(foo instanceof Bar) — true, alert(foo instanceof Foo) — true.
2) И наследование у Вас работает по классике — экранируется метод (new Bar()).method.
Вы сказали, что собираетесь базироваться на другой парадигме? Где здесь проявление другой парадигмы? По-моему, вы тоже продемонстрировали реализацию классики на прототипах.
Классика или не классика это не корректно. Class based и Prototype based имеет значение.
Я утверждаю что нет смысла в JS пытаться сделать эмуляцию «super» единственный честный способ это сделать – написать на JS интерпритатор другого языка.

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

this.ancestor('method', 3)() // не ясно метод какого объекта вызовется.
bar.method.bind(this) // этой сроки достаточно, чтобы понять где искать дальше
> Я утверждаю что нет смысла в JS пытаться сделать эмуляцию «super» единственный честный способ это сделать – написать на JS интерпритатор другого языка.
Мы не пытаемся совершенно точно эмулировать джавовский .super. Он — обращение к конструктору родителя. Наш _anc() в коде — наиболее близкое отражение его, в отличие от .superclass из первого примера. А вообще все наши (и ваш) примеры — это Prototype based подход. Class based должен был бы копировать методы, а это очень затратно.
>… надо указать имя объекта, имя метода и контекст. Однажды приняв это пересташь писать дикие обертки вокруг объектов
Но я то самое делаю, нет (если «имя объекта» — подразумевается имя конструктроа)? Пример: c01.ancestor('protoProp', 2) из статьи. c01 — контекст, protoProp — имя метода (свойства, не важно), 2 — указатель на имя объекта относительно контекста. Последнее как раз избавляет от диких цепочек, которые продемонстриованы в первом примере. И об избавлении от обёрток — собственно, сама статья.

> this.ancestor('method', 3)() // не ясно метод какого объекта вызовется
3-й по счёту от текущего

> bar.method.bind(this) — этой сроки достаточно, чтобы понять где искать дальше
то есть c01.constructor.prototype._anc.prototype._anc.prototype.MethodX будет лучше, чем this.ancestor('MethodX', 3)? Вы встретили, допустим, в коде новую для себя функцию ancestor, знакомитесь с её назначением (она базовая, того стоит), и знаете, что 3 — это «3-й по счёту». Чем хуже той цепочки?
> то есть c01.constructor.prototype._anc.prototype._anc.prototype.MethodX будет лучше, чем this.ancestor('MethodX', 3)?

3-й разпишу bar.method.bind(this) будет лучше.

Дальше вроде холивар начался из раздела «мне нравится» и «мне не нравится» думаю по делу мы уже поговорили.
Всё равно не понял, как этим выражением вызовется метод предка, например, 3-го колена. Или заранее надо .method определить как хеш и туда вписать как .bind метод того предка, который нам понадобится? Примерно так поступал Резиг по ссылке в статье (Simple JavaScript Inheritance By John Resig), но для автоматичности ему приходилось обходить все методы на предмет поиска перекрывающих, что затратно, вообще говоря.
Ок попробую 4-й раз. Другая парадигма. В JS все объекты и вызывается не 3-й предок, а конкретный метод конкретного объекта. Значит у абъекта есть имя и надо его и использовать.
А, тогда понятно, только Вы называете классы объектами. Конкретное имя конкретного класса, расположенного где-то в цепочке. Для некоторых классов нужно иметь одно и то же имя в цепочке. Конкретный пример: берём метод hide() — скрытие некоторого визуального объекта. Но объекты будут разные для разных классов. В корне — класс модального окна. Его скрытие — это действия по скрытию окна + ещё что-то специфическое. Далее у нас будет класс текста в этом окне. Скрвтие его включает установку специфического, относящегося к тексту флага (например «текст не на экране») плюс закрывание модального окна. Вот пример обращения к методу предка первого колена. Теперь представим, что у нас не просто объект текста, а объект — «замечание такое-то», основанное на классе-наследнике «Замечание», который базируется на «Тексте». Пользователь прочитал его и кликнул по какой-то кнопке, чтобы закрыть. При закрытии нужно выполнить все 3 метода — самого класса и 2 предков. В последнем классе, скажем, сохраняется знание о том, что кликнул пользователь, какую кнопку, какую дал оценку. И всё это висит на одноимённых методах .hide(), мне не хочется каждому методу в цепочке давать уникальные имена, потому что для одной цепочки это — замечание, для другой — напоминание, а в третьей не Текст, а Иллюстрированное_мультимедийное_окно. А по поводу продолжения разговора — да, согласен, дело вкуса, можно не продолжать.
5-й раз. Всё объекты, надо вызвать метод указываешь имя, метод, контекст.
var modal = {
hide: function(){};
}

function Text(){
this.hide = function(){
modal.hide.bind(this);
....
}
}
Text.prototype = modal;

vat text = new Text();

function Warning(){
this.hide = function(){
text.hide.bind(this);
....
}
}
Warning.prototype = text;

warning = new Warning();
Так а чего там затратного у Резига, в каком месте? Обычный цикл по свойствам прототипа родителя с парой условий, причем единожды при создании самого класса, не его экземпляра. Ваше начальная или промежуточная инициализация DOM дерева — вот это камень преткновения относительно скорости в современных браузерах, пробег цикла и обертка в замыкание — тысячные проценты времени от поиска по нескольким селекторам средней страницы. Но код: а) читабелен б) расширяем в) весьма и весьма производителен. Если вам вдруг потребовалось найти какой-то метод на 4-ом нижнем уровне какого-то класса-родителя, вдруг пропустив предыдущие перегруженные 3, то тут либо пользуйте именем этого метода через class.prototype.method.call(), либо как-то меняйте архитектуру.
в методе потомка:
Child.prototype.method = function(){
Parent.prototype.method.apply(this, arguments);
};

и нечего выдумывать все наглядно и понятно
Это верно для одного шага наследования или порождения экземпляра.
Поддерживаю комментарий kernel, такой вариант наиболее наглядный. Что-то с архитектурой — это тогда, когда нужно уметь лазить куда-то дальше прямого родителя. В этом и суть, что каждый потомок должен уметь обрабатывать эту ситуацию сам, т.е. если имеем цепочку A -> B -> C, что C должен уметь при необходимости дергать методы B, B должен уметь вызывать методы A, но нет никакой причины добавлять для C возможность вызывать методы A.

С одной стороны может показаться, что это дает дополнительную гибкость, но на самом деле это прямая дорога в ад, это то место, где код запутан настолько, что поддерживать уже невозможно. В частности ваш метод ancestor требует держать в памяти всю цепочку наследования.

Но если все же есть пример, где вы считаете, что без этого не обойтись, напишите, может быть я действительно заблуждаюсь, пример tzlom работает только с непосредственным родителем.
Примеры, когда нужно вызывать методы A — habrahabr.ru/blogs/javascript/130495/#comment_4326648.

> В этом и суть, что каждый потомок должен уметь обрабатывать эту ситуацию сам
Он и без того обрабатывает сам, но методы родителей повторяются, переписываются — зачем?

> на самом деле это прямая дорога в ад
Делается как раз для того, чтобы избежать ада после десятка применений обычного доступа к предкам :).
По-моему, все просто (думаю, что Николай theOnlyBoy имеет в виду то же самое):

1) вызывать метод непосредственного предка — еще можно, и проще всего это делать явно — Parent.prototype.foo.apply(this, arguments). Нет смысла для этого расширять язык дополнительными конструкциями, призванными скрыть явное разыменование «Parent».

2) вызывать методы родителей выше по цепочке — не нужно: значит, что-то не так с архитектурой, и нужно ее переделывать, тк по сути это влезание в частную логику непосредственного родителя.
С первым полностью согласен.
Со вторым, соглашусь, часто в потомке необходимо выполнить какой-то код до или после выполнения кода метода потомка (как пример, логирование, различные проверки, вызовы событий и т.п.), эта практика является вполне распространенной в других ООП языках, в той же JAVA, как пример, необходимо вызвать конструктор предка при его переопределение. При написании например различных враперов без этого не обойтись бывает, если не использовать делегирование.
Черт, пардон, конечно же по второму пункту не соглашусь, и конечно же до или после кода предка.
Мы действительно говорим об одном и том же?
Есть Child, Parent.foo(), GrandParent.Foo(), и из кода Child надо вызвать GrandParent.foo() в обход Parent.foo(), который его перекрыл?

Я имел в виду, в этом случае Child не должен думать, как внутри устроен метод Parent.foo() или Parent вообще, а использовать только его интерфейс. При этом понятно, что перекрытые методы GrandParent в интерфейс Parent не входят, иначе не надо их перекрывать.

Аналогично с конструктором — Child должен вызывать только конструктор Parent, который уже вызывает конструктор GrandParent.

Кстати, в «классовых» ООП-языках обычно нет возможности вызвать ни конструктор, ни другой метод GrandParent, перекрытый в Parent, из Child.

Обертки, логгирование и т.п. — все это решается на уровне кода Parent. Либо аспектами, например, логгирование, но к наследованию это уже отношения не имеет.
Мне кажется вы путаете, как и автор, теплое с мягким. Никто не говорит, что вызывать перегруженный метод предка нельзя или не нужно. Вопрос в том, как автор пытается это завуалировать через всякие числовые уровни ancestor, циклы, условия. Нужно вызвать метод предка — берем и вызываем по имени, как белые люди. А так пойди догадайся как оно работает и на что указывает.
> берем и вызываем по имени, как белые люди
Но ведь в том и проблема, что имена у них одинаковые?
Вот, наконец-то придумал жизнеспособный пример, описанный здесь habrahabr.ru/blogs/javascript/130495/#comment_4326279 с вызовом класса своего, предка и пра-предка, и всё под одним именем. Примерно такая задача и у меня отрисовывается, поэтому хочу иметь для неё механизм. Можно ли сказать, что для данного примера не так с архитектурой?

А второй пример — habrahabr.ru/blogs/javascript/130495/#comment_4326279 — когда древовидное наследование надо оформить как линейное, у меня в задачах тоже есть, и для него не хватает доступа к предку N-го колена. Такой подход с бОльшим основанием можно назвать «не так с архитектурой», потому что там я пытаюсь дерево наследников выразить линейной цепочке. Но и тут резонный вопрос: а как в дереве множественного наследования описать приоритетность выбора методов, если соединение ветвей не выразить в цепочке? (Другими словами, соединяем 3 ветви, у всех них есть одноимённые методы/свойства; надо знать, какие из них считать более приоритетными. Поэтому соединение выстраиваем в цепочу. Более сложные случаи — также требуют цепочки.) Вообще, это совершенно отдельная тема.
Что не так с архитектурой: наследование «модальное окно» -> «текст».

Рекомендую 1) подробно изучить, когда можно и нельзя наследовать, и почему так — особенно по публикациям классиков типа Боба Мартина. 2) разобраться с S.O.L.I.D. и его отношению к данному вопросу 3) на закуску — разобраться с законом Деметры
> Что не так с архитектурой: наследование «модальное окно» -> «текст».
Хм, но ведь текст — который отображается в окне. Или может быть в другом, немодальном окне. Текст — в смысле, более сложное образование, чем окно и для которого требуется окно как основа представления. Поэтому класс Окно для такого текста — более базовый. Мы имеем команду «Закрыть окно», но более осмысленная команда — «Закрыть текст» (который, например, прочитали). Поэтому текст принимает команду «Закрыться» и сам разбирается с тем, что для этого надо сделать со своим контейнером.
Вы в последней строке указали ключевой момент: Окно — это контейнер для Текста, то есть связь между ними — это не связь «общее — частное», и наследование здесь не нужно, что и рекомендовал изучить.

Но даже в случае наследования:
TextNote -> Text -> Window:

внутри Text.hide() вызывается Window.hide()
внутри TextNote.hide() вызывается Text.hide() — и, таким образом, вызывается и Window.hide()

— видно, что глубже непосредственного родителя заглядывать нет необходимости.
Спасибо, замечание по делу. Значит, если в обычной схеме наследования появится желание использовать .ancestor('method', 2) — что-то в архитектуре недоработано согласно закону Деметры, и надо всерьёз подумать, как ограничиться .ancestor('method'). Остаётся случай множественного наследования, который я собираюсь рассмотреть во 2-й части и которые тоже нужны для практики. Узлы слияния классов (когда у наследника 2 предка и более) в JS приходится делать цепочками, и обращение к методу непосредственного предка, но отстоящего на 2 поколения, будет иметь вид .ancestor('method', 2).
такая необходимость ведь возникнет только в случае, когда у обоих предков есть метод с одинаковым именем? т.к. если методы с разным именем, можно вызывать так же через «ancestor» / «superClass» / etc?

для меня такой случай — опять же указание на серьезную проблему в архитектуре, но даже в этом случае предпочел бы указывать явно — ParentA.prototype.foo.apply(this) и ParentB.prototype.foo.apply(this) — в скрытии этого за дополнительной логикой выигрыша нет, зато усложняется понимание и без того «узкого» места в архитектуре.

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

Для примера, имена методов «Закрыть» могут совпадать, а родители будут общие и одного уровня (первого). Принцип Лисков не нарушается, т.к. множественное наследование. Может быть, само множественное наследование — плохая идея?
А вообще интересно, если следовать этому принципу, то имена наследников с обогащённым поведением, получается, должны быть иные. Потому что "Более простыми словами можно сказать, что поведение наследуемых классов не должно противоречить поведению, заданному базовым классом, т.е. поведение наследуемых классов должно быть ожидаемым для кода, использующего переменную базового типа." Сохранение того же имени у метода, путь и будет делать базовые действия так же, но будут иметь некоторые вариации, обусловленные свойствами наследника. Это кажется нормальным, но противоречит принципу.
Каждый раз, когда вижу в яваскрипте перенос оператора на новую строку, ожидаю подвох…
Sign up to leave a comment.

Articles