Об ООП и наследовании в JavaScript человеческим языком, часть 1






Приглашаю всех поговорить об объектно-ориентированном программировании и наследовании в JavaScript. Хорошая новость в том, что всё это довольно просто, плохая новость в том, что ваши имеющиеся знания C++, Java, Ruby, Python или PHP в действительности вам могут добавить трудностей в понимании абсолютно иных принципов работы с объектами. Однако, не пугайтесь. Шаг за шагом, мы вместе разберёмся что к чему.

Картинка взята с http://cseweb.ucsd.edu

Классы и прототипы

Начнём с того, что вспомним, как «традиционные» ООП-языки создают объекты.

За основу мы возьмём объект по имени myCarmyCar — это максимально упрощённая версия автомобиля из материального мира. Наш объект имеет свойства, вроде color и weight, а также методы, вроде drive и honk.

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

Итак, если вы хотите получить объект myCar, скажем, в Java, то сперва вам необходимо создать некий шаблон, на основе которого будут создаваться новые объекты. Традиционно этот шаблон называется классом.

Всякий раз. когда вы хотите создать новый объект myCar, вы говорите Jav'e: «сделай мне новый объект согласно спецификации, которая приведена в классе Car».

Созданный объект будет обладать свойствами и методами, которые описаны в классе. Если вы вызовете метод honk вашего объекта, то интерпретатор Java обратится к классу Car, найдёт код, который должен быть выполнен при вызове метода honk, и затем выполнит его.

Ничего нового. Теперь посмотрим. что творится в JavaScript.

Мир без классов

В JavaScript нет классов. Но как нам создавать объекты? Ведь не описывать вручную все свойства и методы всякий раз, когда нам потребуется новый объект?

Если нам нужно создать 30 автомобилей в Java, то связь объекта с классом позволяет нам легко получить их. И все они будут иметь одинаковый набор свойств и методов, при этом нам не нужно 30 раз описывать их.

Как же это делается в JavaScript? Вместо отношения «класс-объект» здесь используется отношение «объект-объект».

Там, где Java говорит «возьми класс Car и получи там весь код, необходимый для работы нового объекта», JavaScript говорит «вот тебе объект, который называется прототипом; возьми из него всё, что необходимо для создания нового объекта».

Подход, используемый в JavaScript, называется прототипным (Prototype-based) программированием, а тот, что используется в Java и подобных — классовым (class-based). Оба этих подхода являются отличными реализациями объектно-ориентированной парадигмы программирования, просто это два разных подхода.

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

Ну что, пора погрузиться в код? Как нам всё-таки создать объект myCar, наделив его нужными свойствами и методами? Начнём с создания объекта «с нуля».

Теперь мы имеем объект, который может ездить и сигналить:

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

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

Более того, в языках вроде Java, мы делаем то же самое. Мы создаём такую машину для производства объектов в виде класса, после чего можем получать столько объектов, сколько душе угодно:

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

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

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

Или же мы можем создать функцию, которая не только будет создавать объекты, но и будет делать кое-что ещё, в результате чего объекты будут связаны с их создателем. В этом варианте мы уже сможем получить настоящее наследование поведения, то есть функции создаваемых объектов будут ссылаться на одну и ту же их реализацию. Если после создания объектов реализация функции, на которую они ссылаются, изменится (это вполне возможно в JavaScript), то поведение этих объектов соответственно тоже изменится.

Рассмотрим каждый из способов детальней.

Функция для создания простых объектов

В нашем первом примере мы создавали объект myCar из ничего. Если мы завернём приведенный код в функцию, мы получим простую фабрику для создания объектов:

Чтобы не захламлять примеры, реализацию метода drive опустим. Теперь мы можем массово выпускать автомобили:

У приведённого способа есть существенный недостаток: на каждый создаваемый объект создаётся один метод honk. То есть, создавая 1000 объектов, мы заставим интерпретатор создать 1000 копий метода honk, которые делают одно и то же. В результате мы получим бесполезно растраченную память.

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

Но в виртуальной вселенной мы не связаны подобными физическому миру ограничениями. Создавая объекты более «правильным» способом, мы открываем для себя возможность управлять поведением всех ранее созданных объектов без необходимости обрабатывать каждый из них отдельно.

Использование конструктора для создания объектов

В JavaScript функции, которые предназначены для создания объектов, разделяющих между собой одно и то же поведение, называются конструкторами.

Давайте создадим конструктор для автомобилей. Мы назовём нашу функцию 'Car', в названии сделав первую букву заглавной намеренно: это широко распространённый способ именования конструкторов.

Поскольку мы рассматриваем два новых аспекта работы с объектами, мы разобьём работу на два шага.

Первым шагом мы почти повторим ранее рассмотренное решение, но только оформим его в виде конструктора:

Когда эта функция будет вызвана при помощи ключевого слова new, то в результаты мы получим экземпляр объекта с присоединённой к нему функцией honk:

Использование this и new исключает необходимость создания объектов «вручную», поскольку сам объект создаётся «за кулисами» автоматически, после чего передаётся фнкции-конструктору, где и становится доступен через this.

Проиллюстрировать вышеописанное можно при помощи следующего псевдокода:

Как уже было сказано, это решение напоминает предыдущее, то есть нам не нужно вручную создавать объекты, однако мы всё ещё не можем изменять поведение honk так, чтобы это отразилось сразу на всех созданных экземплярах. Однако, начало положено. используя конструктор, все объекты получают доступ к специальному свойству, которое связывает их с конструктором.

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

Источник: manuel.kiessling.net




Об ООП и наследовании в JavaScript человеческим языком, часть 1: 1 комментарий

Комментарии запрещены.