Объектно-ориентированное программирование в ванильном JavaScript

в 7:55, , рубрики: javascript, objects, Блог компании Райффайзенбанк

Этот перевод — для новичков, делающих первые шаги в JavaScript, или даже в программировании вообще.

Объектно-ориентированное программирование в ванильном JavaScript - 1

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

О чём мы поговорим в этой статье:

  • Пространство имён.
  • Объекты.
  • Объектные литералы.
  • Функции-конструкторы.
  • Наследование.

Пространство имён

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

К сожалению, JS не имеет встроенной поддержки определения пространства имён, но мы можем использовать объекты для достижения того же результата. Есть много разных паттернов для реализации, но мы рассмотрим только самый распространённый — вложенные пространства имён.

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

var MyApp = MyApp || {};

По той же методике можно создавать подпространства имён:

MyApp.users = MyApp.user || {};

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

MyApp.users = {
    // свойства
    existingUsers: [...],
    // методы
    renderUsersHTML: function() {
      ...
    }
};

Подробнее о паттернах определения пространств имён в JavaScript можно почитать здесь: Essential JavaScript Namespacing Patterns.

Объекты

Если вы уже писали код на JavaScript, то в той или иной мере использовали объекты. JavaScript имеет три различных типа объектов:

Нативные объекты (Native Objects)
Нативные объекты — часть спецификации языка. Они доступны нам вне зависимости от того, на каком клиенте исполняется наш код. Примеры: Array, Date и Math. Полный список нативных объектов.

var users = Array(); // Array — нативный объект

Хост-объекты (Host Objects)
В отличие от нативных, хост-объекты становятся нам доступны благодаря клиентам, на которых исполняется наш код. На разных клиентах мы в большинстве случаев можем взаимодействовать с разными хост-объектами. Например, если пишем код для браузера, то он предоставляет нам window, document, location и history.

document.body.innerHTML = 'Hello'; // document — это хост-объект

Пользовательские объекты (User Objects)
Пользовательские объекты, иногда называемые предоставленными (contributed objects), — наши собственные объекты, определяемые в ходе run time. Есть два способа объявления своих объектов в JS, и мы рассмотрим их далее.

Объектные литералы (Object Literals)
Мы уже коснулись объектных литералов в главе про определение пространства имён. Теперь поясним: объектный литерал — это разделённый запятыми список пар имя-значение, помещённый в фигурные скобки. Эти литералы могут содержать свойства и методы, и как и любые другие объекты в JS могут передаваться функциям и возвращаться ими. Пример объектного литерала:

var dog = {
  // свойства
  breed: ‘Bulldog’,
  // методы
  bark: function() {
    console.log(“Woof!”);
  },
};
// обращение к свойствам и методам
dog.bark();

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

Объектные литералы полезны, но не могут быть инстанцированы и от них нельзя наследовать. Если вам нужны эти возможности, то придётся обратиться к другому методу создания объектов в JS.

Функции-конструкторы

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

Пример использования определения объекта с помощью функции-конструктора:

function User( name, email ) {
  // свойства
  this.name = name;
  this.email = email;
  // методы
  this.sayHey = function() {
   console.log( “Hey, I’m “ + this.name );
  };
}
// инстанцирование объекта
var steve = new User( “Steve”, “steve@hotmail.com” );
// обращение к методам и свойствам
steve.sayHey();

Создание функции-конструктора аналогично созданию регулярного выражения за одним исключением: мы используем ключевое слово this для объявления свойств и методов.
Инстанцирование функций-конструкторов с помощью ключевого слова new аналогично инстанцированию объекта в традиционном языке программирования, основанном на классах. Однако здесь есть одна неочевидная, на первый взгляд, проблема.

При создании в JS новых объектов с помощью ключевого слова new мы раз за разом выполняем функциональный блок (function block), что заставляет наш скрипт КАЖДЫЙ РАЗ объявлять анонимные функции для каждого метода. В результате программа потребляет больше памяти, чем следует, что может серьёзно повлиять на производительность, в зависимости от масштабов программы.

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

Методы и прототипы
JavaScript — прототипичный (prototypal) язык, то есть мы можем использовать прототпы в качестве шаблонов объектов. Это поможет нам избежать ловушки с анонимными функциями по мере масштабирования наших приложений. Prototype — специальное свойство в JavaScript, позволяющее добавлять к объектам новые методы.

Вот вариант нашего примера, переписанный с использованием прототипов:


function User( name, email ) {
  // свойства
  this.name = name;
  this.email = email;
}
// методы
User.prototype.sayHey = function() {
  console.log( “Hey, I’m “ + this.name );
}
// инстанцирование объекта
var steve = new User( “Steve”, “steve@hotmail.com” );
// обращение к методам и свойствам
steve.sayHey();

В этом примере sayHey() будет совместно использоваться всеми экземплярами объекта User.

Наследование

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

Когда мы обращаемся к методу или свойству, JS проверяет, задан ли он в определении объекта, и если нет, то проверяет прототип и ищет определение там. Если и в прототипе не находит, то идёт по цепочке прототипов, пока не найдёт или пока не достигнет конца цепочки.

Вот как это работает:

// пользовательский объект
function User( name, email, role ) {
  this.name = name;
  this.email = email;
  this.role = role;
}
User.prototype.sayHey = function() {
  console.log( “Hey, I’m an “ + role);
}
// объект editor наследует от user
function Editor( name, email ) {
   // функция Call вызывает Constructor или User и наделяет Editor теми же свойствами
   User.call(this, name, email, "admin"); 
}
// Для настройки цепочки прототипов мы с помощью прототипа User создадим новый объект и присвоим его прототипу Editor
Editor.prototype = Object.create( User.prototype );
// Теперь из объекта Editor можно обращаться ко всем свойствам и методам User
var david = new Editor( "David", "matthew@medium.com" );
david.sayHey();

У вас может уйти какое-то время на привыкание к прототипичному наследованию, но важно освоить эту концепцию, если вы хотите достигнуть высот в ванильном JavaScript. Хотя её часто называют одним из слабых мест языка, прототипичная модель наследования фактически мощнее классической модели. Например, не составит труда построить классическую модель поверх прототипичной.

В ECMAScript 6 появился новый набор ключевых слов, реализующих классы. Хотя эти конструкты выглядят так же, как в основанных на классах языках, это не одно и то же. JavaScript по прежнему основан на прототипах.

* * *

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

Автор: Raiffeisenbank

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js