ООП в JavaScript



    В данной статье мы поговорим об основных особенностях объектно-ориентированного программирования в JavaScript:

    • создание объектов,
    • функция-конструктор,
    • инкапсуляция через замыкания,
    • полиморфизм и ключевые слова call/apply,
    • наследование и способы его реализации.


    Объекты в JavaScript


    Объект в JavaScript — это ассоциативный массив, который содержит в себе наборы пар ключ-значение («хэш», «объект», «ассоциативный массив» означают в JavaScript одно и то же).

    Создание объекта в JavaScript:

    var obj = new Object(); // вызов функции конструктора
    var obj = {}; // при помощи фигурных скобок.
    

    Задание свойств объекта:

    obj.name = ‘Victor’; // через .property
    obj[‘name’]=‘Victor’; // как элементу массива 
    

    Обращение к свойствам:

    console.log(obj.name); // через .property
    console.log(obj[‘name’]); // как к элементу массива через квадратные скобки
    

    Расширенный вариант:

    var obj = {
        name : ’Viktor’,
        age : 32
    };
    

    Constructor и ключевое слово new


    «Конструктор — это любая функция, которая используется как конструктор». До появления ECMAScript 6 в JavaScript не было понятия конструктор. Им могла быть любая функция, которая вызывается с помощью ключевого слова new.

    Пример использования конструктора:

            var Donkey = function(){ //… }; // создаем объект «ослик»
            var iaDonkey = new Donkey(); 
    

    При вызове new Donkey (), JavaScript делает четыре вещи:

    1. 1. Создаёт новый объект:
      iaDonkey = new Object(); // присваивается новый пустой объект.
    2. 2. Помещает свойства конструктора объекта Donkey:
      aDonkey.constructor == Donkey // true
      iaDonkey instanceof Donkey // true
    3. 3. Устанавливает объект для переноса в Donkey.prototype:
      iaDonkey.__proto__ = Donkey.prototype
    4. 4. Вызывает Donkey() в контексте нового объекта:
      	    var iaDonkey = function(){
      	        this.constructor();           // function Donkey()
      	        // …
      	    };
      

    // То же самое, только на грубом псевдокоде:
        function New (F, args) {
            /*1*/ var n = {'__proto__': F.prototype}; 
            /*2*/ F.apply(n, args); 
            /*3*/ return n; 
        }
    

    1. Создание нового значения (n) и запись значения prototype в proto.
    2. Вызов нашего метода конструктор через apply.
    3. Возвращение нового объекта, класса New.

    Инкапсуляция через замыкания


    Замыкание — это основанный на области видимости механизм, который может создаваться через функцию. Каждая функция создаёт новую область видимости.

    Рассмотрим два примера.

    Пример 1:

        for (var i = 0; i < 10; i++) {
                setTimeout(function () { console.log(i); }, 0);
        }
    

    В этом цикле десятка выводится на экран десять раз: после последней итерации будет 10, и тогда начнётся выполнение setTimeout.

    Пример 2:

        for (var i = 0; i < 10; i++) {   
            (function (m) {
                setTimeout(function () { console.log(m); },0);
            })(i) 
        }
    

    Анонимная самовызывающаяся функция позволяет начать выполнение функции сразу после ее объявления.

    Мы применили принцип замыкания: объявляем функцию, передаем в неё фактическое значение, и она «замыкает» в себе значение переменной i. m попытается через замыкания получить значение из ближайшей верхней области видимости. А так как мы передали ее через самовызывающуюся функцию, то она каждый раз будет равна своему значению (значению переменной i), и мы 10 раз получим от 0 до 9.

    Этот пример про замыкания и инкапсуляцию взят из реального проекта:



    Есть функция BarChart, у нее есть публичные методы — построить график и обновить его значения. Есть приватные методы и свойства — что нам нужно выполнять, не показывая это окружающему миру.

    Если мы обратимся к BarChart через new, то получится, что это функция-конструктор, и мы создадим новый объект этого класса. Но все приватные методы замкнутся в этой переменной, и мы сможем обращаться к ним внутри. Они останутся в области видимости, которую создаст эта функция.

    Полиморфизм и ключевые слова call/apply


    Применение конструкции apply:

    var obj = { outerWidth: ‘pliers‘ };
    function getWidth(){
        return this.outerWidth;
    }
    
    var a = getWidth();
    var b = getWidth.apply(obj);
    
    console.log(a);  // текущая ширина браузера, this будет windows
    console.log(b);  // на экран выведется pliers. outerWidth — это свойство объекта windows, мы, по сути, вызовем windows.outerWidth
    

    Вызов механизма:

    Calling func.call(context, a, b...) 
    

    эквивалентен записи:

    func(a, b...), but  this == context.
    

    Оба вызова идентичны, только apply позволяет передавать параметры через массив.

    call(context, param1, param2 …)
    apply(context, [args])
    

    Четыре варианта вызова и его результаты:

    Вызов function: function(args) – this == window
    Вызов method: obj.funct(args) – this == obj
    Apply: func.apply(obj,args) – this == obj
    Constructor: new func(args) – this == new object
    

    Наследование и методы реализации




    Модель базового фильтра — стандартный набор параметров, который есть в фильтре любого приложения. Эти параметры необходимы для пагинации, номера страницы и т.п.

    Задаём ему метод через прототип. После получения модели сервера вызываем этот метод, и он преобразует некоторые наши данные в нужный формат.

    Есть класс-наследник и конкретная страница “RouteHistorical”. Класс наследуется от базового фильтра, но дополнительно имеет свои поля и параметры.



    В строке 73 мы передаём в базовый класс через контекст apply новосозданный объект RouteHistorical и те же аргументы. Метод инициализирует все поля, и мы получаем новый объект.

    Строки 81-82 позволяют нам сделать RouteHistorical наследником базового фильтра. В строке 81 мы записываем ссылку на класс конструктора базы в свойство prototype. Метод prototype перезаписывается полностью, и конструктор теряется. Когда мы создаем новый объект, он не будет знать, к чему обратиться.

    В строке 82 мы задаем свойству prototype.constructor ссылку на саму себя. Свойство класса constructor всегда ссылается на самого себя.

    Прототипы


    Свойство prototype имеет смысл в паре с ключевым словом new. Когда мы создаем новый объект, то записываем значение prototype в свойство __proto__. Оно содержит ссылку на класс, который является родителем для нашего класса.



    prototype нужен только для того, чтобы сказать, что нужно записать в __proto__ при инстанцировании нового объекта.

            // unsafe
            var filter = {
                     EnablePagination: true
            };
            function BaseFilter(size) {
                     this.PageSize = size;
                    this.__proto__ = filter;
            }
    
            // safe
            var filter= {
                      EnablePagination: true
            };
            function BaseFilter(size) {
                      this.PageSize = size;
            }
            BaseFilter.prototype = filter;
    

    Две записи одинаковы, но обращаться напрямую к __proto__ считается небезопасным, и не все браузеры это позволяют.

    Создание потомка из базового класса


    Функция extend:

        function extend(Child, Parent) {
               var F = function() { }
               F.prototype = Parent.prototype // 
               Child.prototype = new F() // при создании Child в __proto__ запишется наш родитель prototype 
               Child.prototype.constructor = Child // задаём конструктор, должен ссылаться на самого себя.
               Child.superclass = Parent.prototype // чтобы иметь доступ к методам Parent
        };
    

    Использование:

        function BaseFilterModel(..) { ... }
        function RouteHistoricalFilterModel(..)  { ... }
    

    instanceof


    Позволяет определить, является ли объект экземпляром какого-либо конструктора на основе всей цепочки прототипирования.

    instanceof (псевдокод метода):

        function isInstanceOf(obj, constructor) {
            if (obj.__proto__ === constructor.prototype) {
                return true; 
            }
            else    if (obj.__proto__ !== null) {
                    return isInstanceOf(obj.__proto__, constructor) 
                }
                else     {        
                    return false 
                }
         };
    

    Итог


    1. В JavaScript до ECMAScript 6 не было классов, были только функции конструктора, которые вызываются с помощью ключевого слова new.
    2. Цепочка прототипирования — это основа наследования в JavaScript.
    3. Когда мы обращаемся к свойству, то оно ищется в объекте. Если не находится, то в __proto__, и так далее по всей цепочке. Таким образом в JavaScript реализуется наследование.
    4. fn.__proto__ хранит ссылку на fn.prototype.
    5. Оператор new создает пустой объект с единственным свойством __proto__, который ссылается на F.prototype. Конструктор выполняет F, где контекст this — ранее созданный объект, устанавливает его свойства и возвращает этот объект.
    6. Оператор instanceof не проверяет, чтобы объект был создан через конструктор ObjectsConstructor, а принимает решение на основе всей цепочки прототипирования.
    7. В JavaScript this зависит от контекста, т.е. от того, как мы вызываем функцию.
    NIX Solutions 123,74
    Компания
    Поделиться публикацией
    Комментарии 50
    • +19

      Напишу на джаваскрипте машину времени и отправлю эту статью себе в 2005 год!

      • 0
        Меня в 2007 сильно выручил javascript.ru, хабр. и гугл, который вывел меня на нужные статьи.
        • 0

          А меня в 2005 — PPK и наблы. Но я больше про то, что сейчас такие статьи смотрятся как-то… запоздавше, что ли.
          Сейчас все про это пишут. А я года до 2008 не слышал словосочетания «объект активации», например.

          • +1
            Sorry за оффтоп, но, Да, прикольные были времена.

            Недавно вспомнил, что можно ещё сюда сходить: за кусочком ностальгии. Там можно даже найти AJAX в 2001 примерно году.
    • +1
      Свежо, но нынче есть ES6 и собственно словосочетание JavaScript теряет контекст о какой версии мы говорим.
      • 0

        Да тащем-та прототипы и в ES6 никуда не делись. Хотя, конечно, городить костыли для наследования классов уже не надо.

      • +8
        Древнее зло пробудилось. Дальше будет статья про jQuery?

        Нет, я не против статей для новичков, но почему бы не учить актуальным (не модным, а актуальным) вещам? А не всяким __proto__:
        Warning: While Object.prototype.__proto__ is supported today in most browsers, its existence and exact behavior has only been standardized in the ECMAScript 6 specification as a legacy feature to ensure compatibility for web browsers. For better support, it is recommended that only Object.getPrototypeOf() be used instead.


        • 0
          Добавить бы в статью новшеств, которые облегчают жизнь (https://learn.javascript.ru/es-modern) и было бы… точно также как у всех.
          • 0
            Не холивара ради, но меня интересует третья строчка в функции extend:
                function extend(Child, Parent) {
                       var F = function() { }
                       F.prototype = Parent.prototype // 
                       Child.prototype = new F() // при создании Child в __proto__ запишется наш родитель prototype 
                       Child.prototype.constructor = Child // задаём конструктор, должен ссылаться на самого себя.
                       Child.superclass = Parent.prototype // чтобы иметь доступ к методам Parent
                };
            


            Чем Вам не угодил Object.create?
            • 0
              Вопрос снят. Оказывается в IE8- Object.Create нет.
              • 0
                Не автор, но добавлю, что оно, это… это отсюда: http://javascript.ru/… #nasledovanie-na-klassah-funkciya-extend
                • 0

                  Ага, это то, как полифиллится Object.create, если его где-то нету.

                  • 0
                    Чтобы совсем исправиться за глупый вопрос, добавлю, что на MDN есть супер-продвинутый пример полифила Object.create по сравнению с канторовским, который упомянул wentout, и который использовал автор.

                    Полифил
                    if (typeof Object.create != 'function') {
                      // Production steps of ECMA-262, Edition 5, 15.2.3.5
                      // Reference: http://es5.github.io/#x15.2.3.5
                      Object.create = (function() {
                        // To save on memory, use a shared constructor
                        function Temp() {}
                    
                        // make a safe reference to Object.prototype.hasOwnProperty
                        var hasOwn = Object.prototype.hasOwnProperty;
                    
                        return function (O) {
                          // 1. If Type(O) is not Object or Null throw a TypeError exception.
                          if (typeof O != 'object') {
                            throw TypeError('Object prototype may only be an Object or null');
                          }
                    
                          // 2. Let obj be the result of creating a new object as if by the
                          //    expression new Object() where Object is the standard built-in
                          //    constructor with that name
                          // 3. Set the [[Prototype]] internal property of obj to O.
                          Temp.prototype = O;
                          var obj = new Temp();
                          Temp.prototype = null; // Let's not keep a stray reference to O...
                    
                          // 4. If the argument Properties is present and not undefined, add
                          //    own properties to obj as if by calling the standard built-in
                          //    function Object.defineProperties with arguments obj and
                          //    Properties.
                          if (arguments.length > 1) {
                            // Object.defineProperties does ToObject on its first argument.
                            var Properties = Object(arguments[1]);
                            for (var prop in Properties) {
                              if (hasOwn.call(Properties, prop)) {
                                obj[prop] = Properties[prop];
                              }
                            }
                          }
                    
                          // 5. Return obj
                          return obj;
                        };
                      })();
                    }
                    

                    • +1

                      А в полифиле-то ошибка! Второй аргумент содержит дескрипторы свойств, а не значения, поэтому присваивать obj[prop] = Properties[prop]; некорректно

                      • 0
                        По-видимому, Вы правы. Результаты совсем не идентичными получаются.
                        jsFiddle
                        Вместо значения свойства переносится его дескриптор. По-видимому надо вызывать в этом месте Object.defineProperties… для которого тоже нужен будет полифил=)
                        • 0

                          Если посмотреть на английскую версию в MDN — то там полифил вообще без второго параметра. Видимо, переводчики отсебятину добавили...

                • +2
                  Привет.

                  У Вас там где «Четыре варианта вызова и его результаты:»…
                  Там для:

                  Constructor: new func(args) – this == new object


                  Это, конечно, новый объект, но он всё же обладает свойствами и методами прототипа func.prototype.
                  Т.е., для this внутри конструктора скалярные свойства будут свои, а методы будут те, что у func.prototype.

                  Соответственно, поэтому фраза:
                  prototype нужен только для того, чтобы сказать, что нужно записать в __proto__ при инстанцировании нового объекта.

                  не совсем корректна, т.к. даже через конструктор с прототипом можно наделать кучу сайд-эффектов.

                  На гипотетический пример, в прототипе можно изменить какое-нибудь свойство через его конструктор, и это отразится на всех наследниках.
                  • +2
                    Автор, пожалуйста, пощадите новичков и повесьте дисклаймер c предупреждением, что перечисленные в статье методы уже как пару лет устарели.
                    • +1
                      С чего бы они устарели? Я аккурат сегодня столкнулся с IE8 и необходимостью поправить функцию, при разработке которой забыли, что мы его поддерживаем…
                      • 0
                        Ну вы же сами сейчас привели в пример поддержку легаси браузера. Потому и устарели.
                        Сейчас новичкам (а это статья именно для них) рекомендовал бы в первую очередь учиться писать адекватный es6 код, пользоваться экосистемой и следить за новостями, а не учиться поддерживать легаси, которое отвалится раньше, чем они освоятся в веб разработке.
                        • +2
                          > поддерживать легаси, которое отвалится раньше, чем они освоятся в веб разработке.
                          Как бы не так. Черти мелкомягкие всех нас переживут.
                          А в веб-разработке нужно постоянно помнить о легаси, потому что то, о чём пишут в новостях, внедряется долго и мучительно.
                          • 0
                            Проблемы частично решаемы, в стилях — автопрефиксерами, в коде — транспайлерами, но все же с легаси лучше не связываться, себе дороже.
                            Кстати отказ совместимости со старыми браузерами уже давно вошел в тренд, вполне возможно что автообновление браузеров утопит IE8 куда быстрее чем мы думаем.
                            • 0
                              отказ совместимости со старыми браузерами уже давно вошел в тренд

                              Вы это РЖД расскажите. И подобным же большим и неповоротливым организациям, которые 15 лет на ворованной ХР просидели отключив автообновление, и ещё столько же просидят.
                              А они, между прочим, наиболее жирные из клиентов.

                              • +1
                                Наверное вы правы, но это решение разработчика, с чем связываться, а с чем нет. На рынке достаточно проектов, где про существование IE8 можно забыть, правда это большей частью фронтендерские вакансии, что стоит признать.
                          • +2
                            Если новичкам не рассказывать про прототипы, то они даже знать этого не будут. И будут говорить, что в JS есть классы. А что это на самом деле, как работает — это уже не важно. В итоге имеем ребят, которые не знают инструмента, которым пользуются.
                            А по поводу статьи, я бы просто порекомендовал Шаблоны проектирования Больше и подробнее.
                            • 0

                              Я думаю, имелись в виду не прототипы, а хитрая магия с одноразовым конструктором вместо Object.create
                              В принципе, сейчас, наверно, уже не столь важно знать, как работает полифил.

                              • +1
                                Про протитипы знать несомненно надо, но точно не в контексте «собираем на коленке очередную систему прототипного наследования». Пример адекватной на мой взгляд подачи материала для новичков — https://learn.javascript.ru/, где легаси подходы честно помечены и подаются в последнюю очередь и с соответствующим предупреждением.

                                Посмотел вашу книжку, простите, она никуда не годится. Там есть замечательная глава «заимствование конструктора» с примерно таким содержанием:
                                function StaticPage() {
                                Article.call(this);
                                }

                                Шел 2016 год…
                                • 0
                                  Во-первых, есть несколько изданий этой книги разных лет. Я привел 2011, кажется. Во-вторых, иметь знания о том, как можно сделать и принимать решение уже исходя из задачи — вот что должен уметь хороший инженер. Статье, возможно, не хватает информации о том, какие надстройки появились в языке, чтобы не писать столько кода самому, но говорить, что значит этого не надо — это точно неправильно. Да и какая разница, 2016 или 2116 год, если в основе языка это лежит, то знать это надо. А еще надо знать какие возможности есть и как ими пользоваться.
                                  • +2
                                    Тут критерий намного проще и он никак не связан с «инженерами». В современном проекте с мощной фронтенд частью такой код, особенно от начинающего разработчика (а статья именно для них) просто неприемлем, он элементарно не пройдет ревью. Следовательно читать такой код нужно только для расширения кругозора, но никак не в качестве production ready варианта.

                                    В качестве аналогии приведу php4, можно развешивать на харбре гайды об эмуляции ооп и паттернах процедурного программирования, а можно давать актуальную на данный момент информацию по php5\7 и делать мир лучше.
                                    Никакие «тыжеинженер» «этооснова» не оправдывают такого порядка подачи материала. Это просто глупо и нерационально.
                                    • +3

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

                              • –3

                                Они устарели с рождения. Ни new, ни тем более this в ООП коде на javascript лучше не использовать.


                                Лучше писать код типа


                                var Thing = function() {
                                    var that = {};
                                    that.publicNumber = 4;
                                
                                    var privateNumber = 5;
                                
                                    that.getPrivateNumber = function() {
                                        return privateNumber;
                                    }
                                
                                    return that
                                }
                                
                                var thingInstance = Thing();
                                • 0

                                  С чего Вы это взяли?

                                  • +1

                                    Не успел поправить пост.
                                    Есть мнение, что


                                    Определять методы внутри функции конструктора, это антипатер приводящий к лишнему расходу памяти

                                    А острая тоску по приватным свойствам — тем более не понятна, если не хочешь обращаться к свойству из других объектов — не обращайся к нему, и не надо городить велосипеды.

                                    • 0
                                      Если такой объект создается часто, то да. Если он создался один раз и дальше вся работа с уже созданным — то особой разницы нет

                                      На счет this и new тоже не понял почему так, так как с приходом es6 классов снова используется и this и new
                                      • 0

                                        Если он создаётся один раз, то почему его просто не о объявить, безо всяких фукнций-обёрток?


                                        снова используется и this и new

                                        А они никуда и не девались. Наследование на прототипах разве что не так лаконично, как объявление классов, но в целом не менее удобно.

                                      • –2

                                        Вообще это рекомендации Дугласа Крокфорда такие. this плохо, потому что он контекстозависим, new плохо потому, что даёт возможность одному и тому же коду (функции) работать по разному. Обращаться к свойствам объектов можешь не только ты, но и какой-нибудь вредоносный код. Лучше не давать ему возможности это делать.


                                        Что касается излишнего расхода памяти — сборщик мусора уберёт всё. А если у вас много долгоживущих объектов, то, может быть какие-то пролемы и будут. Но их очень много должно быть.

                                        • +1
                                          this плохо, потому что он контекстозависим,

                                          Что в этом плохого?


                                          new плохо потому, что даёт возможность одному и тому же коду (функции) работать по разному.

                                          Что в этом плохого?


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

                                          Какой к аллаху вредоносный код на клиентской стороне?


                                          Что касается излишнего расхода памяти — сборщик мусора уберёт всё

                                          И что же он сделает с копиями методов в каждом объекте?

                                • +1

                                  Какие у вас странные кавычки в коде

                                  • 0
                                    • +3

                                      Сберегу время тех, кто тоже пойдет по этой ссылке и приведу цитату, после которой можно не читать:


                                      Try adding .1 + .2 in your browser console, for instance. I still think Brendan Eich
                                    • 0
                                      все любители обмазыватся ООП в JS давно уже открыли для себя
                                      https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Classes
                                      или дауншифтнулись на TypeScript

                                      чего и вам советую.
                                      • 0

                                        А потом приходит страшный IE8

                                        • –1
                                          куда приходит?

                                          https://habrahabr.ru/post/274595/
                                          Microsoft прекращает поддержку всех версий IE, кроме 11

                                          и то было уже в январе

                                          все адекватные ведущие компании анонсировали прекращение поддержки старых версий в своих продуктах

                                          — секюрити патчи вы надеюсь тоже накладываете на старые версии.
                                          а то как то не хорошо дырявый софт клиенту продолжать впаривать.
                                          тот же Bitrix например
                                          • +1

                                            А РЖД — не прекращает. И все 200 000 человек не пересядут с мелкомягкой мерзости на браузеры из-за того, что разработчику веб-приложения так удобно. Они скорее разработчика сменят.
                                            То же касается туевой хучи российских компаний на огромную сумму общей стоимости заказов.

                                            • –1
                                              РЖД — это конечно двигатель интернета и куча никому не известных российских компаний.
                                              пусть сидят чо. до первых прецедентов взлома и получения убытков.
                                              сами себе яму копают продолжая заявлять что поддержкивают устаревший софт который даже не они разрабатывают и не могут на него никак влиять.
                                              • 0

                                                Взломают их или нет, это на 100% их трудности. Но терять такого мощного клиента как они — дураков нет.

                                          • 0
                                            куда приходит?

                                            https://habrahabr.ru/post/274595/
                                            Microsoft прекращает поддержку всех версий IE, кроме 11

                                            и то было уже в январе

                                            все адекватные ведущие компании анонсировали прекращение поддержки старых версий в своих продуктах
                                            тот же Bitrix например.

                                            и секюрити патчи вы надеюсь тоже накладываете на старые версии.
                                            а то как то не хорошо дырявый софт клиенту продолжать впаривать.
                                        • +1
                                          Аляповатая статья. Много неточностей и ошибок.

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

                                          А тка-же всем придётся нехило повтыкать в примеры, например чтобы понять что [args] это не массив с одним членом а просто массив)
                                          • +2
                                            Попал на пост из «Самого обсуждаемого». Прочитал, подумал «блин, наверное снова археологи в комментах появились, а посту уже лет 5». Открутил вверх, и с удивлением обнаружил «3 июня». И пост при этом даже не в минусах. Да как так то? Я что-то не догоняю, или что это делает на хабре в 2016 году? Что будет дальше? Пост «ООП в php 5.1»?

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

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