В данной статье мы поговорим об основных особенностях объектно-ориентированного программирования в 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 зависит от контекста, т.е. от того, как мы вызываем функцию.
Поделиться с друзьями
-->

Комментарии (43)


  1. k12th
    03.06.2016 11:48
    +19

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


    1. Zenitchik
      03.06.2016 12:43

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


      1. k12th
        03.06.2016 12:46

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


        1. wentout
          03.06.2016 15:40
          +1

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

          Недавно вспомнил, что можно ещё сюда сходить: за кусочком ностальгии. Там можно даже найти AJAX в 2001 примерно году.


          1. k12th
            03.06.2016 15:42

            Мм, xpoint, да:)


  1. jeje
    03.06.2016 13:00
    +1

    Свежо, но нынче есть ES6 и собственно словосочетание JavaScript теряет контекст о какой версии мы говорим.


    1. k12th
      03.06.2016 14:27

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


  1. Fen1kz
    03.06.2016 13:22
    +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.



  1. rpsv
    03.06.2016 13:43

    Добавить бы в статью новшеств, которые облегчают жизнь (https://learn.javascript.ru/es-modern) и было бы… точно также как у всех.


  1. gor2991
    03.06.2016 15:14

    Не холивара ради, но меня интересует третья строчка в функции 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?


    1. gor2991
      03.06.2016 15:24

      Вопрос снят. Оказывается в IE8- Object.Create нет.


      1. wentout
        03.06.2016 15:25

        Не автор, но добавлю, что оно, это… это отсюда: http://javascript.ru/… #nasledovanie-na-klassah-funkciya-extend


      1. k12th
        03.06.2016 15:28

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


      1. gor2991
        03.06.2016 15:34

        Чтобы совсем исправиться за глупый вопрос, добавлю, что на 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. mayorovp
          03.06.2016 19:21
          +1

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


          1. gor2991
            03.06.2016 20:03

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


            1. mayorovp
              04.06.2016 09:28

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


  1. wentout
    03.06.2016 15:18
    +2

    Привет.

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

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


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

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

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

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


  1. atc
    03.06.2016 19:04
    +2

    Автор, пожалуйста, пощадите новичков и повесьте дисклаймер c предупреждением, что перечисленные в статье методы уже как пару лет устарели.


    1. Zenitchik
      03.06.2016 22:16
      +1

      С чего бы они устарели? Я аккурат сегодня столкнулся с IE8 и необходимостью поправить функцию, при разработке которой забыли, что мы его поддерживаем…


      1. atc
        03.06.2016 22:23

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


        1. Zenitchik
          03.06.2016 22:29
          +2

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


          1. atc
            03.06.2016 22:36

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


            1. Zenitchik
              03.06.2016 22:41

              отказ совместимости со старыми браузерами уже давно вошел в тренд

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


              1. atc
                03.06.2016 22:54
                +1

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


        1. artemmalko
          04.06.2016 12:10
          +2

          Если новичкам не рассказывать про прототипы, то они даже знать этого не будут. И будут говорить, что в JS есть классы. А что это на самом деле, как работает — это уже не важно. В итоге имеем ребят, которые не знают инструмента, которым пользуются.
          А по поводу статьи, я бы просто порекомендовал Шаблоны проектирования Больше и подробнее.


          1. Zenitchik
            04.06.2016 12:22

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


          1. atc
            04.06.2016 12:30
            +1

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

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

            Шел 2016 год…


            1. artemmalko
              04.06.2016 13:35

              Во-первых, есть несколько изданий этой книги разных лет. Я привел 2011, кажется. Во-вторых, иметь знания о том, как можно сделать и принимать решение уже исходя из задачи — вот что должен уметь хороший инженер. Статье, возможно, не хватает информации о том, какие надстройки появились в языке, чтобы не писать столько кода самому, но говорить, что значит этого не надо — это точно неправильно. Да и какая разница, 2016 или 2116 год, если в основе языка это лежит, то знать это надо. А еще надо знать какие возможности есть и как ими пользоваться.


              1. atc
                04.06.2016 13:51
                +2

                Тут критерий намного проще и он никак не связан с «инженерами». В современном проекте с мощной фронтенд частью такой код, особенно от начинающего разработчика (а статья именно для них) просто неприемлем, он элементарно не пройдет ревью. Следовательно читать такой код нужно только для расширения кругозора, но никак не в качестве production ready варианта.

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


              1. k12th
                04.06.2016 14:28
                +3

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


  1. fetis26
    04.06.2016 14:31
    +1

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


  1. paulgray
    04.06.2016 17:22

    1. k12th
      05.06.2016 01:12
      +3

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


      Try adding .1 + .2 in your browser console, for instance. I still think Brendan Eich


  1. IvanPanfilov
    05.06.2016 11:04

    все любители обмазыватся ООП в JS давно уже открыли для себя
    https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Classes
    или дауншифтнулись на TypeScript

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


    1. Zenitchik
      05.06.2016 12:28

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


      1. IvanPanfilov
        05.06.2016 13:03
        -1

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

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

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

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

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


        1. Zenitchik
          05.06.2016 14:06
          +1

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


          1. IvanPanfilov
            05.06.2016 19:12
            -1

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


            1. Zenitchik
              05.06.2016 21:10

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


      1. IvanPanfilov
        05.06.2016 13:10

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

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

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

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

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


  1. jt3k
    05.06.2016 19:25
    +1

    Аляповатая статья. Много неточностей и ошибок.

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

    А тка-же всем придётся нехило повтыкать в примеры, например чтобы понять что [args] это не массив с одним членом а просто массив)


  1. vlreshet
    06.06.2016 09:43
    +2

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