Привет, Хабр!

До сего дня я занимался лишь переводами интересных, на мой взгляд, статей англоязычных авторов. И вот настала пора самому что-то написать. Для первой статьи я выбрал тему, которая, я уверен, будет полезна junior-разработчикам, стремящимся дорасти до «мидлов», т.к. в ней будет разобрана схожесть/отличие JavaScript от классических языков программирования (С++, С#, Java) в плане ООП. Итак, начнём!

Общие положения парадигмы


Если мы посмотрим определение JavaScript по Википедии, то увидим следующее понятие:
JavaScript (/?d???v???skr?pt/; аббр. JS /?d?e?.?s./) — мультипарадигменный язык программирования. Поддерживает объектно-ориентированный, императивный и функциональный стили. Является реализацией языка ECMAScript (стандарт ECMA-262).

Как следует из этого определения, JavaScript существует не сам по себе, а является реализацией некоей спецификации EcmaScript. Помимо него, эту спецификацию реализуют и другие языки.

В EcmaScript(далее ES) присутствуют следующие парадигмы:

  • структурная
  • ООП
  • функциональная
  • императивная
  • аспектно-ориентированная(в редких случаях)

ООП в ES реализовано на прототипной организации. От начинающих разработчиков в ответ на вопрос: «Чем ООП в JS отличается от ООП в классических языках». Как правило, получают очень туманное: «В классических языках классы, а в JS прототипы».

В действительности ситуация обстоит немного сложнее. С точки зрения поведения разница между Динамической Классовой организацией и Прототипной организацией невелика(она безусловно есть, но не столь глобальная).

Посмотрите на Python или Ruby. В этих языках ООП основано на динамической классовой организации. В обоих этих языках мы можем динамически по ходу программы менять класс объекта и изменения внутри класса также динамически влияют на порождаемые им сущности. Совсем как в JS, но ведь в JS ООП основано на прототипах.

Существенная разница между языками со Статической классовой организацией и Прототипной организацией. Само по себе отличие «там классы. тут прототипы» не столь существенно.

На чём основана Статическая классовая организация?


Основой этого типа ООП являются понятия «Класс» и «Сущность». Класс представляет собой некий формализованный обобщённый набор характеристик сущностей, которые он может породить. Т.е. это некий общий план всех порождаемых им объектов.

Характеристики бывают двух типов. Свойства(описание сущности) и методы(активности сущности, их поведение).

Сущности, порождаемые классом, представляют собой копии этого класса, но с инициализированными свойствами. Как мы видим, класс жёстко регламентирует описание сущности(предоставляя строго определённый набор свойств) и её поведение(предоставляя строго определённый перечень методов).

Приведём небольшой пример на JAVA:

class Person{
     
    String name;        // имя
    int age;            // возраст
    void displayInfo(){
        System.out.printf("Name: %s \tAge: %d\n", name, age);
    }
}

Теперь создадим инстанцию класса:

public class Program{
      
    public static void main(String[] args) {
         
        Person tom;
    }
}

У нашей сущности tom есть все характеристики класса Person, он также обладает всеми методами своего класса.

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

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

Какие минусы данного подхода?

Думаю, очевидно, что:

  • В сущности могут оказаться характеристики, которые ей никогда не пригодятся
  • Класс не может динамически изменять, добавлять, удалять свойства и методы, которые он предоставляет порождаемым сущностям, т.е. не может изменять свою сигнатуру.
  • В сущности не могут присутствовать свойства или методы, отсутствующие в классе родителе(или иерархической цепи родителей)
  • Расход памяти пропорционален количеству звеньев в иерархии наследования(из-за копирования свойств)

На чём основана прототипная организация?


Ключевой концепцией прототипной организации является Динамически Изменяемый Объект(dynamic mutable object, dmo). DMO не нужен класс. Он сам может хранить все свои свойства и методы.

При простановке DMO некоего свойства происходит проверка на наличие этого свойства в нём. Если свойство есть, то оно просто присваивается, если его нет, то свойство добавляется и инициализируется переданным значением. DMO могут менять свою сигнатуру по ходу программы сколь угодно раз.

Приведём пример:

//Данный объект мы будем использовать в качестве прототипа
const Person = {
  name: null,
  age: null,
  sayHi() {
    return `Hi! My name is ${this.name}. I'm ${this.age} years old.`
 }
}

const Tom = {
  //Какие-то специфичные для Тома свойства и методы
}

Tom.__proto__ = Person;

Думаю, все кто в теме знают, что в ES6 появился синтаксис классов, но это не более чем синтаксический сахар, т.е. под капотом теже прототипы. Код выше не стоит воспринимать как хорошую практику кодирования. Это не более чем иллюстрация, она приведена именно в таком виде(сейчас все нормальные люди используют ES6 классы), чтобы не запутать читателя и подчеркнуть разницу теоретических концепций.

Если мы выведем объект Tom в консоль, то увидим, что в самом объекте есть только ссылка _proto_, которая присутствует в нём по умолчанию всегда. Ссылка указывает на объект Person, который является прототипом объекта Tom.

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

Прототипом для объекта может быть любой объект, более того объект может переприсваивать свой прототип по ходу программы.

Вернёмся к нашему Тому:

Tom.name = 'Tom'; //инициализируем Тому собственное свойство
Tom.surname = 'Williams'; //инициализируем Тому собственное свойство
Tom.age = 28;//инициализируем Тому собственное свойство

Tom.sayHi();//Вызываем метод sayHi, в Томе интерпритатор его не найдет, поэтому посмотрит в прототипе, и вот там то он есть

const tomSon = {
  name: 'John',
  age: 5,
  sayHi() {
    return this.__proto__.sayHi.call(this) + `My father is ${this.__proto__.name} ${this.surname}`;
  }
}
//Укажем, что Джон сын Тома
tomSon.__proto__ = Tom;
tomSon.sayHi();// Вернёт "Hi! My name is John. I'm 5 years old.My father is Tom Williams"

Обратите внимание, свойства name, age и метод sayHi это собственные свойства объекта tomSon. При этом, мы в tomSon sayHi явно вызываем метод прототипа sayHi так, как если бы он был в объекте Tom, но на самом деле его там нет, и он неявным способом возвращается из прототипа Person.Также мы явно оперируем свойством прототипа name и неявно получаем свойство surname, которое мы вызываем, как собственное свойство объекта tomSon, но на самом деле его там нет. Свойство surname неявным образом подтягивается через ссылку __proto__ из прототипа.

Продолжим развитие истории нашего Тома и его сына Джона.

// Допустим, Том со своей женой(мамой джона развелись)
// и суд, как часто бывает, оставил ребёнка с мамой,
// а та снова вышла замуж 

 const Ben = {
  name: 'Ben',
  surname: 'Silver',
  age: 42,
  sayHi() {
    return `Hello! I'm ${this.name} ${this.surname}. `;
  }
}

tomSon.nativeFather = Tom;
tomSon.__proto__= Ben;

tomSon.sayHi(); // фамилия у ребёнка поменялась(допустим), также поменялись некоторые его привычки(поведение)
//Теперь метод вернёт 'Hello! I'm John Silver. My father is Ben Silver'

Обратите внимание, мы по ходу программы поменяли прототип уже созданного объекта. В этом схожесть Прототипной организации и Динамической классовой организации. Именно поэтому ответ «там классы, тут прототипы» на вопрос " в чём разница между классическими языками и JavaScript?" не вполне корректен и свидетельствует о некотором непонимании теории ООП и её реализации на классах и/или прототипах.

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

Ben.hobbies = ['chess', 'badminton'];
//сущность tomSon давно уже создана, но мы добавляем свойства в её прототип и можем реализовать в ней поведение, которое будет оперировать этими свойствами
tomSon.sayAboutFathersHobies = function () {
  const reducer = (accumulator, current) => {`${accumulator} and ${current}`}
  return `My Father play ${this.hobbies.reduce(reducer)}`
}

tomSon.sayAboutFathersHobies(); // вернёт 'My Father play chess and badminton'

Это называют делегирующей моделью прототипной организации или наследованием на прототипах.

Как определяется способность сущности реализовывать некое поведение?


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

Какие плюсы у прототипного подхода?

  • Больше гибкости
  • В сущностях не присутствуют свойства, которые им не нужны

Какие минусы?

  • Менее наглядно
  • Не всегда легко отследить, что послужило отправной точкой нежелательного поведения сущности, т.е. по сравнению со статической классовой организацией прототипная менее предсказуема
  • Сообщество разработчиков программного обеспечения недостаточно хорошо знакомо с ним, несмотря на популярность и распространённость JavaScript

Заключение


На этом мы на сегодня закончим. Надеюсь, что мне удалось донести мысль о том, что отличие между классическими языками и JavaScript связано не с наличием/отсутствием классов и присутствием/отсутствием прототипов, а именно со статическим/динамическим характером организации.

Безусловно, многое осталось не рассмотренным. Я бы не хотел писать слишком длинных статей, поэтому особенности Каскадной модели в прототипной организации и средства ООП(Полиморфизм, Инкапсуляцию, Абстракцию и т.д.) мы обсудим в последующих статьях.

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


  1. bm13kk
    21.09.2019 11:19

    Вы один механизм __proto__ использовали для двух разных сущностей — наследования и [семейные] отношения


  1. apapacy
    21.09.2019 13:03

    В конце концов proto это всего лишь синтаксический сахар вокруг свойств constructor и prototype. При этом не слишком широко применяемый и вошедший в спецификацию сравнительно недавно в 2015 году. Мне кажется что для понимания что там за кадром в классах es7 было бы полезнее знать именно основу прототипного наследования которая была в es с самых первых версий


    1. Alex_Shcherbackov Автор
      22.09.2019 09:27

      Разумеется! я написал, что решил применить этот синтаксис в иллюстрационных целях.


  1. urrri
    21.09.2019 20:26

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


    1. Alex_Shcherbackov Автор
      22.09.2019 13:47

      Именно об этом данная статья!


      1. urrri
        22.09.2019 14:13

        Как-то из статьи я этого не понял. Более того, понял что мысль донесена не правильно и об этом мой коментарий.


        1. Alex_Shcherbackov Автор
          22.09.2019 16:01

          Почему мысль донесена не правильно?


          1. urrri
            22.09.2019 17:28

            Вы сравниваете классы и прототипы — вещи несравниваемые. Класс это сущность парадигмы ООП. Прототип — имплементация. ООП в JS ничем (за исключением нюансов) не отличается от ООП в других языках. JS имплементирует ООП с помощью прототипов, С++ компилятором со статическими сущностями. Обусловлено это тем, что первый — динамический интерпретатор, а второй статический компилятор (или проще: так получилось).
            А все эти способности на лету чего-то портить никак не связаны ни с ООП ни с прототипами. Все это динамичность языка.
            А вы пытаетесь показать разницу между классами и прототипами в том что прототипы можно менять или клеить на лету.
            В этом и есть ошибка подхода.
            А еще, наличие обьектов не делает ваш код обьектно-ориентированным.


            1. Alex_Shcherbackov Автор
              22.09.2019 20:26

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


              1. VolCh
                22.09.2019 21:45

                Как в ваше разделение вписывается PHP? Вполне классические классы, но свойства на лету добавлять конкретному объекту можно


                1. Alex_Shcherbackov Автор
                  22.09.2019 23:47

                  Я там написал про динамику и статику в статье. Об этом и статья.


  1. pin2t
    21.09.2019 21:50

    Вы хоть в Java коде

    new Person()

    напишите, а то tom-то так и не создался


  1. Nookie-Grey
    21.09.2019 23:56

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


  1. megahertz
    22.09.2019 11:52

    Как по мне, для джунов написано слишком академическим языком, из-за чего сложнее понять. Например эта статья воспринимается легче. Удивительно, но за 10 лет она практически не потеряла актуальность.


    1. Alex_Shcherbackov Автор
      22.09.2019 12:59

      Почитаю! Извините если тяжело написано, я просто пытался передать свой опыт работы с начинающим разработчиками.


      1. megahertz
        22.09.2019 18:53

        Не страшно, сама статья неплохая. Я сам пишу так-же, потом перечитываю и упрощаю. Разбиваю длинные предложения на простые, убираю лишние обороты, смотрю чтобы термины не плавали. Можно сказать, рефакторю текст. Примеры того что бросается в глаза:


        • “сущности”, “инстанции”
          “экземпляры” привычнее, иногда допустимо разговорное “инстанс”
        • “создавая отношение генерализации — специализации”
          смотрится уместнее в статьях про нюансы ООП, UML. Здесь лучше “наследуя его”
        • “генерального класса(суперкласса)”
          ? “родителя”
        • “Класс представляет собой некий формализованный обобщённый набор характеристик сущностей, которые он может породить.”
          тяжеловато, описание полегче

        Это все конечно субъективно, может другие хабраюзеры не согласятся со мной.


        1. Alex_Shcherbackov Автор
          22.09.2019 20:33

          Я употребил термин «сущность» для того чтобы показать, что это нечто физически конкретное… возможно не самая удачная идея.

          Наследование это хороший оборот, но я сознательно употребил генерализации — специализации чтобы отразить суть происходящего. Ведь и в прототипной организации наследование. Но только в классовой организации присутствуют понятия подкласс и суперкласс.


  1. bruian
    22.09.2019 12:34

    Мне одному кажется странным данное утверждение

    //сущность tomSon давно уже создана, но мы добавляем свойства в её прототип и можем реализовать в ней поведение, которое будет оперировать этими свойствами

    учитывая тот факт, что метод добавлен непосредственно в объект (в представленном коде), а не в его прототип, в этом поможет убедиться состав объекта на которое ссылается свойство __proto__
    Соответсвенно и последующие рассуждения искажаются допущенным недопониманием рассматриваемого вопроса


    1. Alex_Shcherbackov Автор
      22.09.2019 13:03

      Мы добавили свойства объекту Ben, которая является на тот момент прототипом объекта tomSon.


  1. OTCloud
    22.09.2019 13:00

    Весьма актуальная статья на мой взгляд, с учетом продолжающегося роста популярности JS и ES6 в частности. Буду ждать следующих статей на эту тему.


    1. Alex_Shcherbackov Автор
      22.09.2019 13:00

      Спасибо! Статьи будут.