Основы и заблуждения насчет JavaScript

    Объекты, классы, конструкторы

    ECMAScript, будучи высоко-абстрактным объектно-ориентированным языком программирования, оперирует объектами. Существуют также и примитивы, но и они, когда требуется, также преобразуются в объекты. Объект — это коллекция свойств, имеющая также связанный с ней объект-прототип. Прототипом является либо также объект, или же значение null.
    В JavaScript нет привычных классов, но есть функции-конструкторы, порождающие объекты по определенным алгоритмам (см. Оператор new).

    Прототипное делегирующее наследование


    Классическое наследование очень похоже на то, как люди наследуют гены своих предков. Есть какие-то базовые особенности: люди могут ходить, говорить… И есть характерные черты для для каждого человека. Люди не в состоянии изменить себя — свой класс (но могут поменять собственные свойства) и бабушки, дедушки, мамы и папы не могут динамически повлиять на гены детей и внуков. Все очень по земному.

    Теперь представим другую планету, на которой не такое как на Земле генное наследование. Там обитают мутанты с «телепатическим наследованием», которые способны изменять гены своих потомков.
    Разберем пример. Отец наследует гены от Дедушки, а Сын наследует гены от Отца, который наследует от Дедушки. Каждый мутант может свободно мутировать, и может менять гены своих потомков. Например у Дедушки был зеленый цвет кожи, Отец цвет унаследовал, Сын тоже унаследовал цвет. И вдруг Дед решил: «надоело мне ходить зеленым — хочу стать сними», смутировал (изменил прототип своего класса) и «телепатически» распространил эту мутацию Отцу и Сыну, вобщем посинели все. Тут Отец подумал: «Дед на старости лет совсем двинулся» и поменял свой цвет в генах обратно на зеленый(изменил прототип своего класса), и распространил «телепатически» свой цвет сыну. Отец и Сын зеленые, Дед синий. Теперь как бы дед ни старался Отец и сын цвет не поменяют, т.к сейчас Отец в своем прототипе прописал цвет, а Сын в первую очередь унаследует от Прототипа Отца. Теперь Сын решает: «Поменяю ка я свой цвет на черный, а моё потомство пусть наследует цвет от Отца» и прописал собственное свойство, которое не влияет на потомство. И так далее.

    Опишем все в коде:
    var Grandfather = function () {}; // Конструктор Grandfather 
    Grandfather.prototype.color = 'green';
    
    var Father = function () {}; // Конструктор Father 
    Father.prototype = new Grandfather(); // Это простой, но не самый лучший вариант протитипного наследования
    
    var Son = function () {}; // Конструктор Son 
    Son.prototype = new Father(); // Аналогично
    
    var u = new Grandfather(); // Экземпляр "класса" Grandfather
    var f = new Father(); // Экземпляр "класса" Father
    var s = new Son(); // Экземпляр "класса" Son
    
    // Изначально все зеленые
    console.log([u.color, f.color, s.color]); // ["green", "green", "green"]
    
    // Дед решил поменять свой цвет и цвет потомства
    Grandfather.prototype.color = 'blue';
    console.log([u.color, f.color, s.color]); // ["blue", "blue", "blue"]
    
    // Отец решил все вернуть для себя и своего потомства
    Father.prototype.color = 'green';
    // Хотя мог исделать и так:
    // Grandfather.prototype.color = 'green';
    console.log([u.color, f.color, s.color]); // ["blue", "green", "green"]
    
    // Смысла нет
    Grandfather.prototype.color = 'blue';
    console.log([u.color, f.color, s.color]); // ["blue", "green", "green"]
    
    // Сын решил не брать пример с Деда и поменял только собственное свойство
    s.color = 'black'; // Меняем собственное свойство, которое не затрагивает цепочку прототипов
    console.log([u.color, f.color, s.color]); // ["blue", "green", "black"]
    
    var SonsSon = function () {}; // Конструктор SonsSon
    SonsSon.prototype = new Son(); // Аналогично
    
    var ss = new SonsSon(); // Экземпляр "класса" SonsSon
    // Сын сына унаследовал от Отца
    console.log([u.color, f.color, s.color, ss.color]); // ["blue", "green", "black", "green"]
    

    Почитать:
    ООП в Javascript: наследование
    Разбираемся с prototype, __proto__, constructor и их цепочками в картинках

    Цепочка прототипов, получение свойства с заданными именем


    Цепь прототипов (prototype chain) — это конечная цепь объектов, которая используется для организации наследования и разделяемых (shared) свойств.

    В JavaScript каждый объект имеет собственные свойства (Own Properties) и ссылку на объект-прототип, в свою очередь прототип тоже имеет собственные свойства и ссылку на прототип, прототип прототипа тоже имеет собственные свойства и ссылку на прототип ну и так далее, пока ссылка на прототип не будет null — эта структура называется цепочка прототипов.
    При попытке обратиться к свойству объекта (через точку или скобки) выполняется поиск указателя по имени: сперва проверяется есть ли указатель с таком-то именем с списке собственных свойств (если есть, то возвращается), если его нет, то идет поиск в собственном прототипе (если есть, то возвращается), если его нет, то идет поиск в прототипе прототипа и так далее, пока прототип прототипа не станет null в этом случае возвращается undefined.

    Некоторые реализации JavaScript используют свойство __proto__ для представления следующего объекта в цепочке прототипов.

    Поиск свойства на чтение можно описать следующей функцией:
    function getProperty(obj, prop) {
      if (obj.hasOwnProperty(prop))
        return obj[prop]
     
      else if (obj.__proto__ !== null)
        return getProperty(obj.__proto__, prop)
     
      else
        return undefined
    }

    Для примера рассмотри простой класс Point 2D, содержащий 2 свойства (x, y) и метод print. Используя, определения выше — построим объект.
    var Point = {
        x: 0,
        y: 0,
        print: function () { 
            console.log(this.x, this.y); 
        }
    };
     
    var p = {x: 10, __proto__: Point};
    
    // Свойство 'x' нашлось в свобственных свойствах:
    /* p.x */ getProperty(p, 'x'); // 10
    
    // Свойство 'y' нашлось по ссылке __proto__ в объекте-прототипе Point  
    /* p.y */ getProperty(p, 'y'); // 0
    
    // Метод print нашелся по ссылке __proto__ в объекте-прототипе Point  
    /* p.print() */ getProperty(p, 'print').call(p); // 10 0
    
    Почему я использовал call, а не вызвал полученную функции напрямую, описано ниже.

    На самом деле Point имеет ещё одно свйоство, да это наша ссылка на прототип родителя __proto__, которая в случае Point указывает на Object.prototype.
    Например, вот так будет выглядеть вся цепочка пртотипов в самом первом примере:
                /* SonsSon <- Son <---- Father <- Grandfather <-- Object <-- null */
    console.log(ss.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__ === null); 
    

    __proto__, prototype, оператор new


    Выше был «низкоуровневый код», теперь посмотрим как все работает в жизни:
    function Point(x, y) { // Конструктор Point
        this.x = x;
        this.y = y;
    }
    Point.prototype = { // Прототип конструктора
        print: function () { console.log(this.x, this.y); }
    };
     
    var p = new Point(10, 20); // Создаем новый объект
    p.print(); // 10 20
    

    Если в предыдущем коде мы хотя бы знали, что куда ссылается, в этом все как-то запутанно.

    Вся «магия» кроется в операторе new. Brendan Eich (создатель JavaScript) захотел, чтобы JavaScript был похож на традиционные ОО языки, такие как C++, Java, поэтому был добавлен оператор new. Посмотрим как же он работает.

    Оператор new получает в свое распоряжение функцию и аргументы функции (new F(arguments...)) и выполняет следующие действия:

    1. Создает пустой объект с единственным свойством __proto__, которое ссылается на F.prototype
    2. Выполняет конструктор F в котором this — созданный ранее объект
    3. Возвращает объект
    Создадим функцию New, эмулирующую поведение оператора new:
    function New (F, args) {
    /*1*/  var n = {'__proto__': F.prototype};
    /*2*/  F.apply(n, args);
    /*3*/  return n;
    }

    Изменим предыдущий пример с Point:
    function Point(x, y) { // Конструктор Point
        this.x = x;
        this.y = y;
    }
    Point.prototype = { // Прототип конструктора
        print: function () { console.log(this.x, this.y); }
    };
     
    var p1 = new Point(10, 20);
    p1.print(); // 10 20
    console.log(p1 instanceof Point); // true
    
    // Это аналогично:
    var p2 = New(Point, [10, 20]);
    p2.print(); // 10 20
    console.log(p2 instanceof Point); // true
    

    Построение цепочки прототипов


    В самом первом примере я строил цепочку прототипов, используя вот такую конструкцию Father.prototype = new Grandfather():
    var Grandfather = function () {}; // Конструктор Grandfather 
    Grandfather.prototype.color = 'green';
    
    var Father = function () {}; // Конструктор Father 
    Father.prototype = new Grandfather(); // Это простой, но не самый лучший вариант протитипного наследования
    
    var Son = function () {}; // Конструктор Son 
    Son.prototype = new Father(); // Аналогично
    

    Теперь мы знаем поведение оператора new и можем понять, что тут делается — развернем new Grandfather():
    Father.prototype = {
        __proto__: { // Прототип Grandfather
            color: 'green', 
            __proto__: Object.prototype
        }
    };

    Теперь при вызове new Father() мы получим вот такой объект (сразу развернем объект):
    Son.prototype = {
        __proto__: { // Прототип Father
            __proto__: { // Прототип Grandfather
                color: 'green', 
                __proto__: Object.prototype
            }
        }
    }

    Давайте посмотрим, что мы имеем в конце кода в объекте s (экземпляр Son)
    {
        color: 'black', // Сын поменял только собственное свойство 
        __proto__: { // Прототип Son
            __proto__: { // Прототип Father
                color: 'green', // Отец решил вернуть цвет
                __proto__: { // Прототип Grandfather
                    color: 'blue', // Дед решил поменять на синий 
                    __proto__: Object.prototype
                }
            }
        }
    }

    Почему же Father.prototype = new Grandfather() не самый лучший вариант построения цепочки прототипов?
    Потому, что нам приходится вызывать конструктор Grandfather, который может подмешать лишние свойства и вызвать лишние методы, например alert. Для обхода этой проблемы используют подставной конструктор:
    function inherit (object, parent) {
        function F(){}; // Подставной конструктор
        F.prototype = parent.prototype; // Подсовываем прототип реального конструктора
        object.prototype = new F(); // Теперь реальный конструктор не будет выполнен
        return object; // Можно и не возвращать
    };

    Пример использования:
    var Grandfather = function () {}; // Конструктор Grandfather 
    Grandfather.prototype.color = 'green';
    
    var Father = function () {}; // Конструктор Father
    inherit(Father, Grandfather); // Это лучше

    Конструктор Grandfather не будет выполнен. Если нам все-такие необходимо выполнить конструктор Grandfather, то вызываем его с помошью call или appy
    var Father = function () { // Конструктор Father
        Grandfather.call(this);    
    };


    Оператор instanceof


    if (p instanceof Point) {
        // ...
    }

    Оператор instanceof очень тесно связан с цепочной прототипов. Он использует именно цепочку прототипов для вынесения вердикта, а не проверяет порожден ли данный объект «p» конструктором «Point». В этом моменте часто бывает путаница.

    Оператор instanceof оперирует двумя объектами — obj и constructor: (obj instanceof constructor). Он начиная с constructor.prototype, пробегает по цепочке прототипов и проверяет следующее равенство obj.__proto__ === constructor.prototype, если оно истинное, то возвращает true.

    Опишем в коде:
    function isInstanceOf(obj, constructor) {
      if (obj.__proto__ === constructor.prototype)
        return true;
     
      else if (obj.__proto__ !== null)
        return isInstanceOf(obj.__proto__, constructor)
     
      else
        return false
    }

    Рассмотрим пример выше:
    function Point(x, y) { // Конструктор Point
        this.x = x;
        this.y = y;
    }
    Point.prototype = { // Прототип конструктора
        print: function () { console.log(this.x, this.y); }
    };
     
    var p = new Point(10, 20); // Создаем новый объект
    
    /* {} instanceof Object */ console.log(isInstanceOf({}, Object)); // true
    /* p instanceof Point */   console.log(isInstanceOf(p, Point)); // true
    /* p instanceof Object */  console.log(isInstanceOf(p, Object)); // true потому, что Object есть в цепи прототипов (Point.__proto__ === Object.prototype)
    /* p instanceof Array */   console.log(isInstanceOf(p, Array)); // false потому, что Array нет в цепочке прототипов
    

    Свойство this


    this это одно большое заблуждение.

    Многие привыкли, что ключевое слово this в языках программирования тесно связано с объектно-ориентированным программированием, а именно, указывает на текущий порождаемый конструктором объект. В ECMAScript this не ограничивается лишь определением порождаемого объекта.
    В JavaScript значение this определяется вызывающей стороной по форме вызова. Правило по которому определяется то, что будет в this такое (объясню по-простому):
    1. Если метод вызывается напрямую (без new, call, apply, bind, with, try catch), то значением this будет тот объект, который стоит перед точкой, слева от имени метода.
    2. Если точки нет (функция вызывается напрямую), то this будет приравнен к undefined, null или window(global), в зависимости от среды и «use strict».
    3. Если выражение представляет из себя не ссылку, а значение, то применяется пункт 2

    Пример:
    var foo = {
        bar: function () {
            console.log(this);
        }
    };
    
    var bar = foo.bar;
    
    bar(); // this === global (2)
    
    foo.bar();   // this === foo (1)
    (foo.bar)(); // this === foo скобки ничего не меняют (1)
     
     // Все выражения слева от скобок вызова - значения
    (foo.bar = foo.bar)(); // this === global (3)
    (false || foo.bar)();  // this === global (3)
    (foo.bar, foo.bar)();  // this === global (3)
    
    function foo() {
       function bar() {
           console.log(this);
       }
       
       bar(); // this === global (2)
    }

    Вспомним пример с getProperty(p, 'print').call(p) именно из-за этого правила я вручную указал значение this. Иначе функция print получила бы в качестве this — window.

    Вот эти операторы и методы способны управлять значением this: new, call, apply, bind, with, try catch (с ними более-менее все понятно, не буду затрагивать).

    Подробнее о this:
    Тонкости ECMA-262-3. Часть 3: This

    undefined, null, void


    null — примитивное значение, представляющее нулевую, пустую, не существующую ссылку
    undefined — примитивное значение, которое получает каждая перемененная по умолчанию (когда переменная не имеет значение)
    void — это оператор (т.е. при вызове его скобки не нужны), выполняющий выражение и всегда возвращающий undefined

    Заключение


    1. В JavaScript нет классов — есть конструкторы
    2. Цепь прототипов — база на которую опирается все наследование в JavaScript
    3. Свойство объекта получается с использованием цепи прототипов
    4. __proto__ — это ссылка на прототип конструктора(prototype)
    5. Оператор new создает пустой объект с единственным свойством __proto__, которое ссылается на F.prototype, выполняет конструктор F в котором this — созданный ранее объект и возвращает объект
    6. Оператор instanceof не проверяет порожден ли данный объект «Object» конструктором «ObjectsConstoructor», для своего вердикта он использует цепь прототипов
    7. В JavaScript значение this определяется вызывающей стороной по форме вызова
    8. void — это оператор, а не функция. undefined, null — примитивные значения

    Важно В некоторых реализациях JavaScript нельзя напрямую менять __proto__, к тому же это свойство не стандартное и уже устаревшее. Для получения ссылки на прототип следует использовать Object.getPrototypeOf. В статье я применял его (__proto__) для демонстрации «внутренностей» ECMAScript.

    В статье были использованы материалы


    Статьи из блога Дмитрия Сошинкова dsCode
    How Prototypal Inheritance really works (Christopher Chedeau)
    Поделиться публикацией
    Комментарии 96
      0
      Немного не в тему, но Uncle разве не «дядя», а не «дедушка»? )

      Ну и по теме: Для меня особенно ценными стали последние 3 разъяснения, за них особенное «Спасибо»!
        0
        Позор мне, непонятно где была моя мысль… Конечно, Дед — это Grandfather. Исправил.
          0
          А я все никак не мог понять, почему :)
          [u.color, f.color, s.color]
          
        +4
        __proto__ — не является стандартом и уже depricated. Не советую использовать.
          0
          Поддерживаю. Но множество примеров — в том числе и тут написано с ним — и в итоге многие используют несмотря на все предупреждения.
            0
            Автор уже поправил, добавив раздел «Важно» в заключение, просто до этого там был 9 пункт с более скромным упоминание, что __proto__ работает не во всех браузерах.
            +1
            __proto__ — очень удобно для демонстрации и понимания сути прототипного наследования, тем более в браузерах что-то типа такого и используется. А какой смысл его использовать в реальных приложениях?
              0
              «Короткое» прототипное наследование.
                0
                А что это такое?
                  0
                  3-й блок кода сверху
                  var p = {x: 10, __proto__: Point};

                  но так делать плохо ;-)
            –1
            За this вообще авторов убить мало, назвали бы scope и сразу куча проблем бы отвалилась.

            А первый пункт не очень раскрыт, на самом деле. У вас Дедушка (и прочие) означают не конкретную персону, а группу, класс персон. И Дедушка меняет не свой цвет, а цвет своей группы. Это вполне себе можно и в ООП-модели осуществить, нужно лишь изменить класс Дедушка изнутри объекта (о правомерности этого отдельно можно говорить).
              +3
              здесь опечатка var bar = foo.bar();
              должно быть var bar = foo.bar;
                0
                Может, я что-то пропустил, но почему после очередного изменения «Деда»

                // Смысла нет
                Grandfather.prototype.color = 'blue';
                console.log([u.color, f.color, s.color]); // ["blue", "green", "green"]


                у потомков не изменились свойства?

                Разве мы не должны получить, как в предыдущих случаях
                // ["blue", "blue", "blue"]
                  +3
                  Перед этим переопределили значение у Father на blue — поэтому Father и его потомок Son видят именно blue и поиск свойства не идёт выше по дереву.
                    +3
                    Дело в том, что Отец изменил свой прототип (Father.prototype.color = 'green';), а поиск свойства color лежит сперва через прототип отца. Вот так выглядит объект f:
                    f = {
                        __proto__: { // Прототип Father
                            color: 'green', // Отец решил вернуть цвет
                            __proto__: { // Прототип Grandfather
                                color: 'blue', // Дед решил поменять на синий 
                                __proto__: Object.prototype
                            }
                        }
                    }

                    При выполнении f.color сначала произойдет поиск в собственных свойствах (не найдено), затем алгоритм будет искать в f.__proto__ — найдено! (color: 'green'). Вобщем до прототипа деда и не дойдет.
                    Если же отец удалит цвет из своего прототипа, то все опять «телепатически» посинеют, кроме сына у которого есть собственное свойство color.
                    0
                    // Отец решил все вернуть для себя и своего потомства
                    Father.prototype.color = 'green';
                    // Хотя мог исделать и так:
                    // Grandfather.prototype.color = 'green';

                    console.log([u.color, f.color, s.color]); // ["blue", "green", "green"]


                    Мне не совсем понятна часть выделенная жирным. Мне кажется, что если бы была вызвана эта строчка, то на выходе было бы ["green", "green", "green"], даже с учётом того, что цвет деда до этого был изменён на синий Grandfather.prototype.color = 'blue';
                      0
                      Да, все верно, было бы ["green", "green", "green"].
                        +1
                        Верно-то оно верно, но согласитесь, что закоментированая строчка вводит в некий конфуз, так как задача всё же стояла изменить свойство для себя и своего потомства, а не приплетать ко всему ещё и старенького и больного дедушку =)
                          +2
                          который до этого хотел стать голубым… Боги, что я пишу!
                      0
                      Очень познавательно! Хорошо бы ещё поподробней по поводу — что и в каких случаях получит this: «undefined, null или window(global)»?
                      P.S. Метафоры конечно кстати. Аля Пелевин.
                        0
                        Почему наследование работает по такой хитрой схеме?:

                        function inherit (object, parent) {
                            function F(){};
                            F.prototype = parent.prototype;
                            object.prototype = new F();
                            return object;
                        }
                        

                        Что плохого может случиться, если делать например так?:

                        object.prototype = parent.prototype;

                        Или так:

                        object.prototype = parent;

                        Почему instanceOf сравнивает obj.__proto__ и constructor.prototype, а не obj.__proto__ и constructor? Из каких соображений выбран такой дизайн?
                          +1
                          Потому что если вы напишите object.prototype = parent.prototype, изменения в прототипе потомка будут влиять на свойства родителя. В случае примера с дедушкой, отцом и сыном, если отец изменит свой цвет, то это отразится и на дедушке.
                            +1
                            Потому что, создатели Javascript-а недостаточно смелы были, чтоб оставить чистое прототипное наследование и добавили классоподобие.
                            +3
                            Поясните пожалуйста момент с:

                            (foo.bar = foo.bar)(); // this === global (3)
                            (false || foo.bar)(); // this === global (3)
                            (foo.bar, foo.bar)(); // this === global (3)

                            Не понимаю, почему в этом случае this — глобальный объект.
                              0
                              Потому, что правила 1 и 2 применяются если слева стоит ссылка, а не значение. Во всех этих случаях слева — значение. Возможно тут будет понятнее.
                              0
                              Ух! «Неделя» javascript на хабре радует!
                              void, конечно оператор, но void 0 и void(0) работают одинаково, и при use strict в том числе
                                0
                                Уже, вроде, третья неделя? =) Скорее уж «месяц» =)
                                  0
                                  > но void 0 и void(0) работают одинаково
                                  Потому, что во втором случае скобки — оператор группировки
                                  +4
                                  Правильно ли я понял?
                                    0
                                    Верно Son.prototype.__proto__ === Father.prototype и т.д.
                                    0
                                    Автор, интересно также, что по поводу других способов создания объектов и наследования между ними? Речь идет о всяких фабриках, клонирований и т.д.

                                    Для чего люди их придумали, и стоит ли овчинка выделки?
                                      0
                                      Область применение фабрики одинакова во всех ЯП. Обертки с Class и другие создают для упрощения работы и приближения кода JavaScript к коду других ЯП.
                                        0
                                        TheShock собирался написать статью про паттерны и их применение, не буду спойлить
                                      0
                                      Объясните, плиз, зачем функцию getProperty необходимо было вызывать именно с указанием контекста (через call)? Просто там this внутри нигде не используется. Или я что-то неправильно понимаю?
                                        0
                                        А! Там же вызывается print(), которая и содержит вывод переменных через this. То есть, нужно указывать контекст, верно. В общем, моя невнимательность. Прошу прощения )
                                          0
                                          Код getProperty(p, 'print').call(p). Разберем что тут происходит:
                                          1. getProperty(p, 'print') возвратит функцию (это как сделать var p_print = p.print)
                                          2. Если функцию вызвать напрямую, то мы получим в качестве this — window и ничего не напечатается.
                                          3. Чтобы предотвратить это мы вызываем функцию, которую возвратит getProperty с помощью явного указания this
                                          0
                                          Не совсем понял полезность этой идеи:
                                          inherit(Father, Grandfather); // Это лучше

                                          Вместо прямого указания, что прототипом конструктора Father, является экземпляр конструктора Grandfather, мы указываем пустой подставной объект. А прототип этого подставного объекта является прототипом конструктора Grandfather. То есть, если убрать подставной объект, получается что-то вроде
                                          Father.prototype = Grandfather.prototype;
                                          , что совсем не эквивалентно
                                          Father.prototype = new Grandfather();
                                          , так как если Grandfather это «самый верхний» объект в дереве, то его прототипом является Object.

                                          Или я что-то не так понял?
                                            0
                                            В подтверждение следующий код:
                                            function inherit (object, parent) {
                                                function F(){}; // Подставной конструктор
                                                F.prototype = parent.prototype; // Подсовываем прототип реального конструктора
                                                object.prototype = new F(); // Теперь реальный конструктор не будет выполнен
                                                return object; // Можно и не возвращать
                                            };
                                            function Grandfather() {
                                                this.color = "red";
                                            }
                                            function Father() {
                                                this.number = 1;
                                            }
                                            inherit(Father, Grandfather);
                                            father = new Father();
                                            console.log(father.number); // 1
                                            console.log(father.color); // undefined
                                            
                                              0
                                              Если добавить в конструктор одну строку
                                              function Father() {
                                                  Grandfather.call(this); // Вот эту
                                                  this.number = 1;
                                              }

                                              То father получит own property — color. Обычно во всех ЯП если происходит ореррайд метода, то по умолчанию предыдущий метод «не наследуется», пока разработчик не прописал его явно.

                                              Я про это писал
                                              Конструктор Grandfather не будет выполнен. Если нам все-такие необходимо выполнить конструктор Grandfather, то вызываем его с помошью call или appy
                                              var Father = function () { // Конструктор Father
                                                  Grandfather.call(this);    
                                              };
                                                0
                                                В вашей статье вы об этом не писали. Вы просто показали «более правильный» метод наследования, который, как я показал, не работает. Объект father не унаследовал свойство color, которое біло обїявлено в конструкторе Grandfather.

                                                Кстати, вариант с Grandfather.call(this); в конструкторе тоже имеет недостатки, так как он скопирует методы родителя, вместо того, чтобы унаследовать их через прототип.
                                                  0
                                                  Сорри за украинские буквы, все время путаю раскладки на клавиатуре :)
                                                    0
                                                    Кстати, вариант с Grandfather.call(this); в конструкторе тоже имеет недостатки, так как он скопирует методы родителя, вместо того, чтобы унаследовать их через прототип.

                                                    Вы ошибаетесь. Будет только вызван конструктор родительского объекта. Методы будут унаследованы через прототип.
                                                      –1
                                                      Вы ошибаетесь. Будет только вызван конструктор родительского объекта. Методы будут унаследованы через прототип.
                                                      И снова неправда. И снова кусок кода, в качестве подтверждения моих слов:
                                                      // Объект-конструктор "Отец"
                                                      function Father() {
                                                          this.returnRed = function() {
                                                            return "red";
                                                          };
                                                      }
                                                      // Объект конструктор "Сын"
                                                      function Son() {
                                                          this.number = 1;
                                                          Father.call(this); // Делаем "наследование" через call
                                                      }
                                                      // Объект конструктор "Дочь"
                                                      function Daughter() {
                                                          this.number = 2;
                                                      }
                                                      father = new Father(); // Создаем экземпляр отца
                                                      Daughter.prototype = father; // Дочь наследует метод отца через прототип
                                                      son = new Son(); // Создаем экземпляр сына
                                                      daughter = new Daughter(); // Создаем экземпляр дочери
                                                      
                                                      console.log(daughter.returnRed()); // red — метод унаследован
                                                      console.log(son.returnRed()); // red — неужели тоже унаследован?
                                                      
                                                      // Изменяем метод в прототипе.
                                                      Daughter.prototype.returnRed = function() {
                                                        return "blue"
                                                      };
                                                      Son.prototype.returnRed = function() {
                                                        return "blue"
                                                      };
                                                      
                                                      console.log(daughter.returnRed()); // blue — изменилось корректно
                                                      console.log(son.returnRed()); // red — опа! А метод то берется не из прототипа. Он просто скопировался в наш объект.
                                                      
                                                        0
                                                        Вы намешали и прототипы с собственными свойствами. Не пойму чему вы удивляетесь :) son.returnRed() returnRed — own property, очевидно, что до прототипа не дойдет. Вы показываете несуществующие проблемы.
                                                          –1
                                                          Теперь вы себе противоречите :)
                                                          Ладно, закончим дискуссию. Начинается флейм :)
                                                            +1
                                                            Разве это мой комментарий? Они будут унаследованы через прототип, если в конструкторе нет одноименного метода/свойства. Ваш вышеположенный код — несуществующая проблема. Или проблема для тех, кто не понимает сути прототипного делегирующего наследования.
                                                            Мне все больше начинает казаться, что вы меня тонко тролите :)
                                                              +1
                                                              Упс, почему-то подумал что ваш.
                                                              Если бы троллил, то просто написал бы, что это все херня, а я — ромашка :)
                                                              Просто интересно поспорить. Если я прав, то смогу что-то доказать. Если неправ — узнаю что-то новое.
                                                                0
                                                                Ниразу не тролинг. Я тоже не понимаю этого:
                                                                /* SonsSon <- Son <---- Father <- Grandfather <-- Object <-- null */
                                                                console.log(ss.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__ === null); 
                                                                


                                                                Если всё так просто, то зачем такое сложное наследование через создание объекта?
                                                                Father.prototype. = new Grandfather();
                                                                


                                                                А это не проще ли?
                                                                Father.prototype.__proto__ = Grandfather.prototype;
                                                                

                                                                Результатом будет как раз такая последовательность:
                                                                ss.__proto__.__proto__.__proto__.__…
                                                                  0
                                                                  В некоторых браузерах свойство __proto__ скрыто. Его дописать можно, но толку от этого не будет. Поэтому создаем цепочку прототипов через new.
                                                                  function operatorNew (Constructor, args) {
                                                                  /*1*/  var n = {'__proto__': Constructor.prototype}; // Движок JS в  браузере может переписать [[proto]]
                                                                  /*2*/  F.apply(n, args);
                                                                  /*3*/  return n;
                                                                  }
                                                                    0
                                                                    Это я понимаю. Но зачем его скрыли? Ведь с ним, наследование было бы проще пареной репы! А сейчас для нативного наследования нужно породить объект! Какая логика в такой конструкции: «для наследования необходимо породить объект»? Как надо было обкурится, что бы придумать такое?

                                                                    Естественно, порождать объекты никому не нравится и появляются костыли типа:
                                                                    inherit (object, parent)
                                                                    

                                                                    А затем ещё тонны библиотек для не нативного ООП. Зачем было так усложнять язык закрывая свойство __proto__? При этом prototype оставили открытым и это нормально.
                                                                      0
                                                                      __proto__ изначально было скрыто. Потом кто-то стал его открывать минуя стандарт. В ECMAScript [[Prototype]] — внутреннее свойство. es5.github.com/#x8.6.2 es5.github.com/#x13.2.2 В 6-й версии ECMAScript будет сахар — class (который на самом деле делает не class-ы, а строит цепочку прототипов).
                                                                        0
                                                                        Т.е. я мыслю в правильном направлении и в следующей версии будет то о чём я написал сначала?
                                                                        Father.prototype.class = Grandfather.prototype;
                                                                        

                                                                        Или class — это совсем не то?
                                                                          0
                                                                          class Monster extends Character {
                                                                            constructor(x, y, name) {
                                                                              super(x, y);
                                                                              this.name = name;
                                                                              this.health_ = 100;
                                                                            }
                                                                          
                                                                            attack(character) {
                                                                              super.attack(character);
                                                                            }
                                                                          
                                                                            get isAlive() { return this.health > 0; }
                                                                            get health() { return this.health_; }
                                                                            set health(value) {
                                                                              if (value < 0) throw new Error('Health must be non-negative.');
                                                                              this.health_ = value;
                                                                            }
                                                                          }
                                                                          

                                                                          harmony:classes
                                                                            0
                                                                            Фигово, это уже совсем не то. Из отличной локаничной идеи состоящей всего из трёх конструкций («function», «new», «prototype») и при этом позволяющей делать всё что угодно (классы, конструкторы, наследования, примеси и многое другое...); теперь вместо этого будет монстр со всеми этими конструкциями: class, constructor, extends, super, static, public/private, const, get/set.
                                                                            Потом будут появлятся: interface, abstract, traits.
                                                            0
                                                            Вы мало того, что путаетесь в своих примерах, так ещё и обвиняете меня во лжи)))
                                                            Наследование через конструктор — это какая-то чужеродная глупость, которую фиг знает кто и зачем придумал.
                                                            Father.call(this); // Делаем "наследование" через call
                                                            

                                                            Это ошибка. Мы не делаем наследование, а вызываем родительский конструктор. Я просто перепишу ваш пример на PHP)

                                                            class Father {
                                                            	function __construct () {
                                                            		$this->returnRed = function () {
                                                            			return 'red';
                                                            		};
                                                            	}
                                                            }
                                                            
                                                            class Son {
                                                            	function __construct () {
                                                            		$this->number = 1;
                                                            		parent::__construct();
                                                            	}
                                                            }
                                                            

                                                            Правда странно смотрится? И теперь поняли, что это совсем не наследование? ;)
                                                            А теперь как надо было сделать:

                                                            class Father {
                                                            	function __construct () {
                                                            		// Father construct
                                                            	}
                                                            
                                                            	function returnRed () {
                                                            		return 'red';
                                                            	}
                                                            }
                                                            
                                                            class Son {
                                                            	function __construct () {
                                                            		$this->number = 1;
                                                            		parent::__construct();
                                                            	}
                                                            }
                                                            
                                                              0
                                                              А я уже думал что эта ветвь обсуждения исчерпана.
                                                              Я прекрасно понимаю, что это не наследование. Это вызов конструктора другого объекта (не называю его родительским, так как наследования нет), с подменой контекста текущим объектом. При этом все свойства/методы не наследуются через прототип, а копируются.
                                                              Именно поэтому слово «наследование» я написал в кавычках.
                                                    0
                                                    Father.prototype = Grandfather.prototype;
                                                    И в итоге все унаследуют от Grandfather
                                                    Или я что-то не так понял?
                                                    Если в конструкторе Grandfather будет alert или подобное добро, то как вы думаете сработает ли он в Father.prototype = new Grandfather();? ;-)
                                                      0
                                                      И в итоге все унаследуют от Grandfather
                                                      Нет. В итоге Father унаследует все от прототипа Grandfater, в нашем случае — Object. То есть не унаследует вообще ничего. Это я показал на примере в своем комментарии выше.
                                                      Если в конструкторе Grandfather будет alert или подобное добро, то как вы думаете сработает ли он в Father.prototype = new Grandfather();? ;-)
                                                      Если вы написали в конструкторе alert, то вы плохой программист. Если вы придерживаетесь следующего правила:
                                                      Я предпочитаю основываться на соглашениях и не проверяю this внутри конструктора — вызвал конструктор без new и поэтому утекло в глобалы — значит «сам дурак»
                                                      к клиентскому коду, то почему оно не распространяется на код ваших объектов?
                                                        0
                                                        Вообще при чем тут это правило?! Алерт я показал для примера. Не важно что там будет alert ещё какой-то метод. Зачем нам их вызывать?

                                                        Father.prototype = Grandfather.prototype и получим вот такое «прекрасное наследованние»…
                                                        Father = function (){};
                                                        Grandfather = function (){};
                                                        
                                                        Grandfather.prototype.pewpew = 1; 
                                                        Father.prototype = Grandfather.prototype;
                                                        
                                                        Father.prototype.pewpew = 2;
                                                        
                                                        console.log(new Father().pewpew, new Grandfather().pewpew); // отлично унаследовал! 2, 2

                                                          0
                                                          В этом примере вы изменив дочерний объект, изменили и родительский (точнее не объекты-экземпляры, а объекты-прототипы).

                                                          А вызывать конструкторы очень даже полезно. Во все примерах в этой теме конструкторы не делали ничего полезного и просто устанавливали свойства, так что толку от них действительно мало.
                                                          Но что если мне нужен конструктор, который действительно что-то делает? Например, устанавливает свойства в зависимости от аргументов, сравнивая их с чем-нибудь, или устанавливает какие-то события, или делает еще что-нибудь.
                                                            0
                                                            >В этом примере вы изменив дочерний объект, изменили и родительский
                                                            Я лишь показал, то что вы предлагали.

                                                            Вызывать конструктор или нет — должен решать разработчик. Во всех ЯП при оверрайде метода старый метод убивается и только тот, кто переписывает этот метод решает стоит ли его вызывать в новом методе.
                                                              0
                                                              Черт, я уже теряюсь в двух ветках комментариев.
                                                              На счет конструкторов — согласен. Но все же на ряду со случаями, когда их можно не вызывать есть и случаи когда это действительно стоит сделать.
                                                    0
                                                    Не могли бы вы объяснить подробно, почему использование для указания предка свойства __proto__, которое сейчас работает в Node.js и во всех современных браузерах кроме IE, считается плохим тоном? Более того, оно «deprecated» и «considered harmful». Меня не отпускает впечатление, что все заморочки с наследованием в JavaScript возникают во многом из-за нежелательности (невозможности) использования __proto__. Живет же Python с __class__, почему JavaScript не может?
                                                      0
                                                      Во всех языках, основанных на прототипах (Self, JavaScript), ссылка __proto__ скрытая, но Mozilla её отрыла. Использование __proto__ ломает всю концепцию и выглядит странно, поэтому хотят убрать.
                                                      Взамен дают Object.isPrototypeOf и Object.getPrototypeOf

                                                      Тут проблема описана подробнее: What is the difference between __proto__ and prototype
                                                        0
                                                        Только не дают «setPrototypeOf» =))
                                                        0
                                                        Использование __proto__ ломает всю концепцию и выглядит странно
                                                        Вот этот момент мне больше всего и непонятен. Ломает какую концепцию? Концепцию принудительного использования конструкторов, new и свойства prototype? Она сама мне кажется странной попыткой сделать простое и ясное наследование непосредственно от объекта к объекту более похожим на классовую систему. Чтобы во всем этом разобраться, я «переизобретал» наследование, примерно так же как вы в этой статье, и пришел практически к такой же реализации. Разве это:
                                                        Brendan Eich (создатель JavaScript) захотел, чтобы JavaScript был похож на традиционные ОО языки, такие как C++, Java, поэтому был добавлен оператор new
                                                        не говорит о том, что именно new ломает всю концепцию? Пусть бы это было только моё субъективное мнение — я пока далеко не эксперт в JavaScript и могу много чего недопонимать, но в куче книг приводится куча альтернативных вариантов организации наследования, тот же Дуглас Крокфорд считает new неудобным костылем. Даже более приличный вариант с Object.create мне не кажется естественней, чем __proto__, ведь __proto__ в явном виде выражает делегирующий, а не клонирующий характер прототипного наследования в JavaScript. Если есть опасения, что манипуляции со свойством __proto__ во время исполнения могут привести к проблемам, то можно было бы принудительно устанавливать для него writable и configurable в false после инициализации объекта, разве нет?
                                                      +1
                                                      А на сколько существенны недостатки классического прототипирования в программировании? Нужно побольше памяти, чтоб полностью клонировать объекты, но зато быстрее работать будет. А что касается невозможности делегирования свойств и логики – это было бы неудобным, как думаете?
                                                        +1
                                                        но зато быстрее работать будет
                                                        Например, Хром оптимизирует рыскание по цепочке прототипов и каждый раз прозрачно создает новый объект (вобщем все свойства косвенно own). Раз это кэш, то расходуется дополнительная память, зато снижается время. Сейчас все оптимизации «по времени», так что это нормально (контр оптимизация — «по памяти»).

                                                        Плюсы прототипного наследования в том, что «создатель копии может менять её, не опасаясь побочных эффектов среди других потомков», впрочем, это является её минусом — «модификация прототипа не влечёт за собой немедленное и автоматическое изменение всех его потомков»
                                                          0
                                                          Извиняюсь, что не по теме Javascript. А если вручную и по необходимости выполнять обновление потомков или сделать признак у объектов, брать ли автоматом обновления предка? Например, как с обновлением программного обеспечения, хочешь — обновляйся :) Применимо ли это на уровне программного кода?
                                                            0
                                                            Можно все свойства «предков» перекрыть собственными свойствами, приравненными к undefined — получим фактически скрытие прототипа, разве что hasOwnProperty будет выдавать.
                                                              0
                                                              Да, верно. За основу лучше брать модель делегирования. Её легко для конкретных задач превратить в классическое прототипирование.
                                                              Так что всё хорошо в Javascript с этим :)
                                                          0
                                                          А на сколько существенны недостатки классического прототипирования в программировании?

                                                          Простите, а что вы в данном случае имеете в виду?
                                                            0
                                                            Дублирование свойств и логики, если порожденный объект не отличается от прототипа. И невозможность прототипу повлиять на своих потомков.
                                                          0
                                                          Не понятно, почему используется много раз слово «заблуждение»?
                                                          Как будто на каждом углу встречаются статьи рассказывающие о противоположном, а вы один глаза людям открываете.
                                                            +1
                                                            Вы не поверите, но так оно и есть. Есть куча людей, которые не понимают или неправильно понимают, что такое this, что такое прототипы и т.д.
                                                            +1
                                                            А вот почему все стараются избежать __proto__, в то время как это — то же самое, что .constructor.prototype? От которого не убежишь, а делает выражение только длиннее. Это вопрос. Может быть, кто-то знает ответ?
                                                              0
                                                              Присоединяйтесь! Нас уже двое, есть шанс, что azproduction включит развернутый ответ во вторую часть FAQ :)
                                                                0
                                                                Ну вот частично ответ нашёл по ссылке выше — What is the difference between __proto__ and prototype, но не убедило. Автор говорит в самом конце, что __proto__ и prototype вместе с ним нехорошо по причине:
                                                                The original idea was that you could get the kind of thing that __proto__ referred to by taking an object and getting .constructor.prototype. But .constructor itself succumbs to prototypal inheritance, so unless the object's constructor set itself up to be that value, that wouldn't work. And nobody did that, so it's mostly broken.
                                                                . Что можно перевести как "[Проблема в том, что] .constructor сам становится «жертвой» прототипного наследования: пока конструкторы не будут определены, это не работает.

                                                                Видимо, при анонимных конструкторах какие-то проблемы?

                                                                Пробуем сделать анонимный конструктор.

                                                                abc = new ( function(){} )();

                                                                Сделаем ему прототип.

                                                                abc.constructor.prototype.c =115;

                                                                Смотрим прототип через объект.

                                                                console.log(abc.constructor.prototype, abc.constructor);

                                                                Всё видим, и конструктор (анонимную функцию), и прототип, присвоенный через потомка.

                                                                Или что имел автор в виду, говоря, что прототипы без конструкторов не работают? Нельзя сразу вписать abc.prototype?

                                                                > Возможно по той причине, что __proto__ можно поменять неосознанно…
                                                                --Но и .constructor.prototype можно поменять неосознанно. Нет, тут имеют какой-то смысл процитированные рассуждения автора выше.
                                                                0
                                                                Возможно по той причине, что __proto__ можно поменять неосознанно. Читал я где-то такое. Шанс невелик, но есть.
                                                                  +1
                                                                  Я вот что нашёл: mail.mozilla.org/pipermail/es-discuss/2011-March/013137.html Это пока самый веский аргумент против __proto__, из тех, что попадались мне на глаза. Но ведь это элементарно лечится, если позволять устанавливать его «руками» только при инициализации объекта.
                                                                    0
                                                                    Но это очевидное следствие предоставленной свободы чехарды с прототипами.
                                                                  +1
                                                                  Все-таки разница в них есть
                                                                  delete Object.prototype.pewpew1;
                                                                  delete Object.prototype.pewpew2;
                                                                  
                                                                  var a = {}, 
                                                                      b = {};
                                                                  
                                                                  /* 1 */ console.log(a.__proto__ === Object.prototype); // true
                                                                  
                                                                  a.__proto__ = {"pewpew2": 1};
                                                                  b.constructor.prototype = {"pewpew1": 1};
                                                                  
                                                                  /* 2 */ console.log(b.pewpew1); // undefined - мы не переписали прототип
                                                                  
                                                                  b.constructor.prototype.pewpew1 = 1;
                                                                  
                                                                  /* 3 */ console.log(b.pewpew1); // 1
                                                                  /* 4 */ console.log(({}).pewpew1); // 1
                                                                  /* 5 */ console.log(({}).pewpew2); // undefined
                                                                  /* 6 */ console.log(b.constructor.prototype === Object.prototype); // true
                                                                  /* 7 */ console.log(a.__proto__ === Object.prototype); // false
                                                                    0
                                                                    Наверное, это тот случай, когда не определён конструктор объекта Object. Хотя он показывает какую-то нативную функцию, но даёт неверное первое равенство. Остальное — следствия.

                                                                    Интересно, есть ли другие регулярные случаи ошибок? Например, если вместо {} ставишь new function(){this.a = 118;} или function(){}, то всё в порядке.

                                                                    А var a = new Object(3), b = new Object(4); даёт ещё более интересные эффекты, однако есть сомнения, что это изъян идеологии, а не кривость реализации конкретно базового объекта Object, который давно уже советуют никогда не использовать с параметром, а теперь видно, что и для перегрузки свойств его не нужно использовать.
                                                                      0
                                                                      /*1*/ — переусложнённое равенство. Чуть упростив, получаем абсолютно очевидное:
                                                                      /* 1 */ console.log(a.constructor === Object); // true


                                                                      Далее, видим, где происходит сбой (не записывается прототип Qbject === Object.prototype):
                                                                      b.constructor.prototype = {f: 4};

                                                                      А здесь — нет:
                                                                      b.constructor.prototype.d = 5;

                                                                      Можно догадаться, что связано с тем, что неявно вызывается конструктор того же Object и что-то мешает записать прототип на нативном уровне. Реализация через __proto__ проходит без сбоев, а через .constructor.prototype — сбоит. Что говорит об этом история? Действительно сбой или «так и должно быть»?

                                                                        0
                                                                        Да так и должно быть. Посмотрим что же нам говорит Object.getOwnPropertyDescriptor:
                                                                        Object.getOwnPropertyDescriptor(Object, "prototype");
                                                                        // {"writable":false, "enumerable":false, "configurable":false}
                                                                        
                                                                        Object.getOwnPropertyDescriptor({}, "__proto__");
                                                                        // {"writable":true, "enumerable":false, "configurable":false}
                                                                        
                                                                        // Хотя оба они
                                                                        Object.isExtensible(({}).__proto__); // true
                                                                        Object.isExtensible(Object.prototype); // true
                                                                        

                                                                        Все меняет флаг writable, который запрещает перезапись (подвержены все прототипы базовых классов), но подмешать свойство мы можем, ибо оба объекта Extensible.
                                                                    0
                                                                    Автору стоило бы начать с того или хотя бы упомянуть, что в Javascript-е есть и чистое прототипное наследование без конструкторов:
                                                                    = {x: 1}
                                                                    = Object.create(a) // a.x === 1, b.x === 1
                                                                    a.x = 2              // a.x === 2, b.x === 2
                                                                    b.x = 3              // a.x === 2, b.x === 3
                                                                    = Object.create(b) // a.x === 2, b.x === 3, c.x === 3
                                                                    // c.__proto__ == b, b.__proto__ = a, a.__proto__ = Object.prototype


                                                                    После этого видно, что конструкторы костыль
                                                                      0
                                                                      Оно там недавно появилось и еще не везде работает. Плюс есть и еще долго будет куча старого кода с new, конструкторами и всякими самоделками, в котором придется разбираться. Так что начинать с Object.create() не следует. Хотя упомянуть можно — вроде как луч света в тёмном царстве :)
                                                                        0
                                                                        Да и до сих пор не поддерживается оперой… И создает объекты всреднем в 2 раза медленнее, чем new пруф.
                                                                        @Suor, оцените потенциальную аудиторию статьи нужна ли им ещё одна заморочка?
                                                                          0
                                                                          Это не заморочка, это как раз простейший вариант прототипного наследования. Заморочки с конструкторами и new как раз выглядят странно на фоне такой стройной концепции. Начало статьи страдает от этого — пример с дедом, отцом и сыном непонятен и не совсем верен.

                                                                          > И вдруг Дед решил: «надоело мне ходить зеленым — хочу стать сними», смутировал (изменил прототип своего класса)
                                                                          Какого класса? В js нет классов. И, вообще, мы тут про прототипное наследование говорим, вроде )

                                                                          Стройно так — дед меняет себя и так как он прототип отца, а отец прототип сына, то эти двое тоже меняются и т.д.
                                                                            0
                                                                            > Какого класса?
                                                                            Ни я ли писал про это в самом начале статьи?! :) Да, забыл поставить «класса» в кавычки.

                                                                            > И, вообще, мы тут про прототипное наследование говорим, вроде
                                                                            В статье я говорю о «прототипном делегирующем наследовании». В вашей ветке обсуждения о «прототипном наследовании» две разные вещи
                                                                      0
                                                                      И ещё, особо смущающая вещь:
                                                                      function F(){}
                                                                      F.prototype = {x: 1}; // прототип F здесь не присваивается
                                                                      Вторая строчка не присваивает прототип объекта F (функции). Она означает какой прототип будет у объектов созданных с помощью new F().
                                                                        0
                                                                        Почему не присваивается? Проверяем:
                                                                        function F(){}
                                                                        F.prototype = {x: 1};
                                                                        console.log("F.prototype: ", F.prototype); //F.prototype: Object(x:1) (Хром, FF)
                                                                        console.log((new F()).x); //1, тоже верно
                                                                        

                                                                          0
                                                                          Прототип объекта — это тот объект, от которого он наследуется.
                                                                          F.prototype — не прототип F, прототип F — Function.prototype. Прототип (new F) — F.prototype.
                                                                          Правда, запутанно?

                                                                            0
                                                                            С непривычки да. Все пошло с языка Self. Да и prototype должно быть и есть по умолчанию только у функции-конструктора.
                                                                        0
                                                                        Не хватает иллюстраций, типа: image
                                                                        А так +100500. Наследование понял именно благодаря вам.

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

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