Как выполнять методы предков в реализации прототипного наследования

    При работе с наследованием иногда возникает желание иметь функцию доступа к методу предка (методу родительского класса) — в конструкторе (аналоге класса для Javascript) или в методе-потомке, потому что, бывает, что новый класс переопределяет его. Не просто какую-нибудь функцию (метод), а с совершенно понятной записью, чтобы название говорило само за себя, и имеющую доступ к указанному поколению предков (не «пра-пра-пра-», а «пра- 3 раза»).

    Возьмём за основу метод прототипного наследования, который максимально эффективен тем, что производит минимум действий при описании цепочек наследуемых классов и при этом максимально поддерживает базовые операции и свойства наследования: instanceof, constructor. Для доступа к предку он создаёт свойство .superclass. (Утверждают, что этот взятый за основу подход к классам популяризовался Крокфордом, ссылок найти не удалось, но важно, что метод продуманный и экономичный. Автор неизвестен.)

    Приведём для начала код этого метода со своими краткими комментариями.
    function inherit2(Inherited, Parent){ //Наследование классов
    	var F = function(){};
    	F.prototype = Parent.prototype;
    	(Inherited.prototype = new F()).constructor = Inherited; 
    		//для корректного конструктора экземпляра
    		//создают функцию с прототипом, равным прототипу родителя и с конструктором
    	Inherited.superclass = Parent.prototype; //прототип предка
    }

    (Кто ранее интересовался темой наследования в Javascript, тот, конечно, его узнал.) Он был очень подробно рассмотрен в javascript.ru/tutorial/object/inheritance, и там же была освещена проблема доступа к методам предков: мы «поигрались» с ".constructor", замкнув ссылку на себя, чтобы будущие экземпляры класса имели корректное свойство .constructor. Но, естественно, потеряли корректность связей между наследуемыми классами (да её и не было, в языке это не заложено) и были вынуждены создать «бонусное» свойство .superclass для возможности доступа к родительскому классу.

    Зачем нужны методы предков?


    Наследование подразумевает «затирание» (экранирование) свойства, если имя его в потомке совпадает с именем в предках. Если описали функцию, а потом, например, расширили функциональность метода в классе-наследнике, то часть упрощённой (или просто предыдущей, или общей) функциональности приходится переписывать — повторять, если она нужна. Это уже непорядок. Ладно, если бы надо было полностью изменить метод, но бывает нужно повторить старый метод, а затем немного его дополнить. (Поэтому доступ к предкам важнее для методов, чем для свойств — свойства всегда полностью переписываются.) Тут-то пригождается «бонусное» свойство ".superclass", указывающее на прототип родителя.

    Для унаследованных классов по схеме A => B => C => c01.MethodX

    	function A(){this.xa =1;}
    	function B(){this.xb =1;}
    	function C(){this.xc =1;}
    	A.prototype.MethodX ='protoA';
    	inherit2(B, A);
    	B.prototype.MethodX ='protoB';
    	B.prototype.xx ='protoX';
    	inherit2(C, B);
    	C.prototype.MethodX = 'protoC';
    	var c01 = new C();

    получаем такие способы доступа к методу предков:

    Alert = console.log;
    Alert(c01.MethodX); //'protoC' //метод экземпляра
    Alert(c01.constructor.prototype.MethodX); //'protoC' //тот же метод конструктора
    Alert(c01.constructor.prototype.superclass.MethodX); //'protoВ' //от класса В
    Alert(c01.constructor.prototype.superclass.constructor.superclass.MethodX); //'protoA'.

    (Чтобы увидеть, как это работает, смотрим исполняемый пример для inherit2() (с Firebug смотреть не помешает).)
    Такие вызовы неудобно использовать, но ими удобно запутывать коллег и последователей: вначале обращаемся через .constructor.prototype, а затем через .superclass, вместо того, чтобы сказать, что мы хотим; не можем указать номер глубины обращения к предку. Всего-то, требуется небольшая функция вида:

    object.ancestor(name, level) - //указать имя метода/свойства и уровень углубления,

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

    Небольшая проблема останется в том, что контекст метода (this) будет «обогащённый», принадлежащий самому последнему классу, к которому базовые классы (A и B) ещё не должны знать, но это уж свойство реальности и далее — желание или нежелание автора полностью переписывать метод предка. Можем переписать по-старинке, а можем заранее позаботиться о понимании примитивными методами более сложного, неизвестного для них окружения.

    Заменим также понятие «суперкласс» (понятно, откуда это идёт, из Джавы, но это путает, потому что обычно классами называют конструкторы, а не их прототипы) на «класс предка» _anc (ancestor). Это как раз то, чем должен быть конструктор относительно наследника, поэтому логика конечной функции упростится.

    Что хотелось бы получить

    c01['MethodX'] c01.ancestor('MethodX', 0)
    c01.constructor.prototype['MethodX'] c01.ancestor('MethodX')
    c01.constructor.prototype._anc.prototype['MethodX'] c01.ancestor('MethodX', 2)
    c01.constructor.prototype._anc.prototype._anc.prototype['MethodX'] c01.ancestor('MethodX', 3)
    ...
    При этом, c01.constructor мог бы быть c01._anc, если бы с каждым new мы это писали, т.е. создали бы специальную функцию вместо new (есть тенденция называть такую функцию create()), но так (пока что?) делать не будем, чтобы сохранить new как индикатор создания объекта.

    Результат достигается такой функцией:

    function ancestor(name, level, constr){
    	level = level || (level ==0 ? 0 : 1);
    	var t =this;
    	return level <= 1
    		? (level
    			? constr && constr.prototype[name]
    				|| t.constructor.prototype[name]
    			: t[name])
    		: arguments.callee.call(this
    			, name
    			, level -1
    			, constr && (constr.prototype._anc || constr.prototype.constructor) || t._anc //предок (если есть) или обычный конструктор
    		);
    }

    Исходный код inherit3() тоже немного поменялся, чтобы пользоваться функцией ._anc().
    function inherit3(Inherited, Parent){
    	var F = function(){};
    	F.prototype = Parent.prototype;
    	(Inherited.prototype = new F())._anc = Parent;
    	Inherited.prototype.constructor = Inherited;
    }

    Логика работы доступа к предкам.


    Обращаемся не к текущему определению метода, а к тому, которое было на level уровней наследования раньше.

    Если не попадаем точно на определение метода, по цепочке прототипов извлекается самый новый из родительских методов, взятых на level шагов раньше последнего порождения.

    Если прототипов не нашлось — попадаем на «бомбу доброты» (медвежью услугу) функции, когда ошибки отсутствующего свойства, должной быть, не возникает, а имеем всегда самый первый определённый в цепочке метод. Если свойства вообще нет, получаем undefined на любом уровне.

    Если не хотим иметь бомбу доброты, пишем пару фейковых свойств при ненахождении свойства на месте constr.prototype._anc — например, (constr.prototype._anc || constr.fail.fail) или просто ( constr.prototype._anc || fail ).

    	function A(){this.prop =1;}
    	function B(){this.prop =1;}
    	function C(){this.prop =1;}
    	A.prototype.protoProp ='protoA';
    	inherit3(B, A);
    	B.prototype.protoProp ='protoB';
    	inherit3(C, B);
    	C.prototype.protoProp ='protoC';
    	var c01 = new C();
    A.prototype.ancestor = ancestor;
    Alert(c01['protoProp'], c01.ancestor('protoProp', 0)) //'protoC protoC'
    Alert(c01.constructor.prototype.protoProp, c01.ancestor('protoProp')) //'protoC protoC'
    Alert(c01.ancestor('protoProp', 2) ); //'protoB'
    Alert(c01.ancestor('protoProp', 3) ); //'protoA'
    Alert(c01.ancestor('protoProp', 4) ); //'protoA'
    Alert(c01.ancestor('protoProp', 5) ); //'protoA'
    Alert(c01.ancestor('protoProp', 6) ); //'protoA'
    Alert(c01.ancestor('protoProp2', 4) ); //'undefined'

    Посмотреть на работу скрипта, покрутить в Firebug — на демонстрационном примере (он — с бомбой доброты).
    Прототип потомка требуется писать после наследования, а не где угодно — следствие работы с прототипами в inherit2(). Прототип общего предка может изменяться где угодно, что показано приписыванием метода ancestor() после всех наследований. В примерах везде использованы свойства, а не методы — для краткости, но нет никаких проблем с указанием или выполнением методов, в форматах
    c01.ancestor('protoMethod', 3) //манипуляция методом
    c01.ancestor('protoMethod', 3)(args) //выполнение метода

    Что нужно улучшить?


    Всё, задача статьи выполнена, можно идти спать? Нет, развитие никогда не останавливается, а взыскательный читатель не получил морального удовлетворения после получения 2 разрозненных функций и ряда условий использования. Смущает в этом методе смещение прототипа от предка к потомку. Ведь часто у объектов уже есть прототипы, их нужно не переписывать, а дополнять (extend), иначе на 3-ем этаже наследования получаем проблемы и необходимость переделки метода.

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

    В примерах видим, что делаем не переписывание прототипа (было бы неправильно), а его дополнение новыми свойствами. Напрашивается использование функции extend() для расширения прототипа после наследования. Значит, неплохо было бы иметь функцию расширения «под рукой». Она есть в jQuery и во многих других библиотеках, или для быстроты работы можем определить свою функцию, чтобы затем включить её в свой способ наследования. За основу можно взять такой формат, нарытый на просторах интернета:
    extend = function(obj, extObj){ //расширение свойств первого аргумента
    	if(arguments.length >2)
    		for(var a =1, aL = arguments.length; a < aL; a++)
    			arguments.callee(obj, arguments[a])
    	else{
    		for(var i in extObj)
    			obj[i] = extObj[i];
    	return obj;
    };
    Он хорош тем, что «между делом» позволяет использовать несколько аргументов для расширения первого. А пока даже эта функция позволит нам писать не "C.prototype.protoProp ='protoC';", а

    extend(C.prototype, {protoProp: 'protoC'}); //уже лучше для внешнего вида.

    Отметим, что .superclass нам уже не нужен; он выброшен из кода.

    Кроме того, этот метод наследования настолько суров экономичен, что не выполняет конструкторы, поэтому свойства конструктора можем увидеть только там, где выполнено new — в объекте с01. Между классами выполнения конструкторов не происходит. Не всегда такая суровость нужна (ой, не всегда). Свойства конструкторов, которые генерируются по this.свойство, могли бы присутствовать и тоже участвовать в прототипном наследовании (быть свойством прототипа наследника).

    О способе переделки этого подхода в более удобный — в следующей серии. Хотя и этот имеет все основания использоваться — всё зависит от количества требований к механизму наследований и желания запутать других разработчиков своим кодом, для этого предпочтительнее первый вариант.

    Какие похожие методы для классов и коллекций (хешей) предпочитают разработчики?


    Подобная задача, конечно, встречалась среди реализаций классов и наследования, что можно найти по словам «вызов методов родительского класса».
    habrahabr.ru/blogs/javascript/40909

    В нём создан базовый метод __parent, выполняющий те же задачи, что метод superclass. В нём тоже надо употреблять многократный вызов, если обращаться к предкам.
    В библиотеке предусмотрены ещё несколько методов:
    Mixin(A, B) — добавление из прототипа B в протототип A,
    Clone() — клонирование объекта,
    Extend() — расширение хешей; только для пары агрументов.

    superclass не подключается к таким методам: [ «hasOwnProperty», «isPrototypeOf», «propertyIsEnumerable», «toLocaleString», «toString», «valueOf» ] — хорошая идея.

    Другой пример доступа к предкам (от Резига)

    В ejohn.org/blog/simple-javascript-inheritance (Simple JavaScript Inheritance By John Resig) видим наличие метода _super и extend вместе со специальным конструктором Class, который подчёркивает его классоподобность. Здесь extend — не расширение хеша, а специальная процедура пробегания по списку свойств, чтобы для «затирающих» поставить свойство _super — метод или свойство, которое создаётся, если у родителя обнаружился переписываемый метод. Очевидно, что это — более затратный механизм. Вместо одноразового создания доступа к методам предков и обращения к нему только при вызове создаём при каждом конструировании наследника «обходной» метод, пробегая для этого по всему списку методов предка. Может быть оправдано, если ресурсов много, обращаемся к предкам чаще, чем создаём новые классы. Вообще, обращение к предкам предполагается в описании функций — вполне рантаймный метод, который должен быть эффективен, так что подход прописывания прямых ссылок иногда оправдан. Как альтернатива, у нас тоже есть обращение через ссылки (объект.) без всяких затрат на пробегание по списку методов при .extend.

    Содержание следующей серии


    Герой, решивший в одиночку завалить четырёхголовую гидру реакции — почти Геракл, но мы-то знаем, что кадры отрисовывает задохлик-программист. Он с успехом рассматривает 4 обнаруженные проблемы (1 — код преобразован в метод, 2 — добавлено расширение прототипа наследника, 3 — возможно самостоятельное использование подмешанных методов, 4 — улучшен формат написания кодов при наследовании) и одним ударом — одним куском кода — решает их. Оказалось, что заодно решена и 5-я проблема-не проблема, но назовём её неперегруженностью (non-overloading) написанного кода — будущий коротенький метод сможет расширять любые другие хеши, не только прототипы наследников. В коде легко угадывается исходный, приведённый нами в этой статье, что поможет проследить разработчикам за развитием идей и выбрать свою подходящую реализацию. Зритель захочет узнать, как это получилось — покадровый просмотр с примерами поможет ему в этом. Код станет действительно удобнее. Следите за приключениями Геракла на страницах наших газет.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 37

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

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

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

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

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


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

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

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

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

            this.ancestor('method', 3)() // не ясно метод какого объекта вызовется.
            bar.method.bind(this) // этой сроки достаточно, чтобы понять где искать дальше
        0
        > Я утверждаю что нет смысла в 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-й по счёту». Чем хуже той цепочки?
          0
          > то есть c01.constructor.prototype._anc.prototype._anc.prototype.MethodX будет лучше, чем this.ancestor('MethodX', 3)?

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

          Дальше вроде холивар начался из раздела «мне нравится» и «мне не нравится» думаю по делу мы уже поговорили.
            +1
            Всё равно не понял, как этим выражением вызовется метод предка, например, 3-го колена. Или заранее надо .method определить как хеш и туда вписать как .bind метод того предка, который нам понадобится? Примерно так поступал Резиг по ссылке в статье (Simple JavaScript Inheritance By John Resig), но для автоматичности ему приходилось обходить все методы на предмет поиска перекрывающих, что затратно, вообще говоря.
              +3
              Ок попробую 4-й раз. Другая парадигма. В JS все объекты и вызывается не 3-й предок, а конкретный метод конкретного объекта. Значит у абъекта есть имя и надо его и использовать.
                –2
                А, тогда понятно, только Вы называете классы объектами. Конкретное имя конкретного класса, расположенного где-то в цепочке. Для некоторых классов нужно иметь одно и то же имя в цепочке. Конкретный пример: берём метод hide() — скрытие некоторого визуального объекта. Но объекты будут разные для разных классов. В корне — класс модального окна. Его скрытие — это действия по скрытию окна + ещё что-то специфическое. Далее у нас будет класс текста в этом окне. Скрвтие его включает установку специфического, относящегося к тексту флага (например «текст не на экране») плюс закрывание модального окна. Вот пример обращения к методу предка первого колена. Теперь представим, что у нас не просто объект текста, а объект — «замечание такое-то», основанное на классе-наследнике «Замечание», который базируется на «Тексте». Пользователь прочитал его и кликнул по какой-то кнопке, чтобы закрыть. При закрытии нужно выполнить все 3 метода — самого класса и 2 предков. В последнем классе, скажем, сохраняется знание о том, что кликнул пользователь, какую кнопку, какую дал оценку. И всё это висит на одноимённых методах .hide(), мне не хочется каждому методу в цепочке давать уникальные имена, потому что для одной цепочки это — замечание, для другой — напоминание, а в третьей не Текст, а Иллюстрированное_мультимедийное_окно. А по поводу продолжения разговора — да, согласен, дело вкуса, можно не продолжать.
                  +1
                  В JS нет классов, есть только объекты.
                    0
                    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();
                  0
                  Так а чего там затратного у Резига, в каком месте? Обычный цикл по свойствам прототипа родителя с парой условий, причем единожды при создании самого класса, не его экземпляра. Ваше начальная или промежуточная инициализация DOM дерева — вот это камень преткновения относительно скорости в современных браузерах, пробег цикла и обертка в замыкание — тысячные проценты времени от поиска по нескольким селекторам средней страницы. Но код: а) читабелен б) расширяем в) весьма и весьма производителен. Если вам вдруг потребовалось найти какой-то метод на 4-ом нижнем уровне какого-то класса-родителя, вдруг пропустив предыдущие перегруженные 3, то тут либо пользуйте именем этого метода через class.prototype.method.call(), либо как-то меняйте архитектуру.
              +4
              в методе потомка:
              Child.prototype.method = function(){
              Parent.prototype.method.apply(this, arguments);
              };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                  Для примера, имена методов «Закрыть» могут совпадать, а родители будут общие и одного уровня (первого). Принцип Лисков не нарушается, т.к. множественное наследование. Может быть, само множественное наследование — плохая идея?
                                    0
                                    А вообще интересно, если следовать этому принципу, то имена наследников с обогащённым поведением, получается, должны быть иные. Потому что "Более простыми словами можно сказать, что поведение наследуемых классов не должно противоречить поведению, заданному базовым классом, т.е. поведение наследуемых классов должно быть ожидаемым для кода, использующего переменную базового типа." Сохранение того же имени у метода, путь и будет делать базовые действия так же, но будут иметь некоторые вариации, обусловленные свойствами наследника. Это кажется нормальным, но противоречит принципу.
                      +1
                      Каждый раз, когда вижу в яваскрипте перенос оператора на новую строку, ожидаю подвох…
                        0
                        такой способ наглядней: goo.gl/TWRnM

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое