Вступление в Объектно-ориентированный JavaScript

Объектно-ориентированный до основания, JavaScript предоставляет мощные и гибкие OOP возможности. Эта статья начинается с введения в объектно-ориентированное программирование, затем рассматривает модель объекта JavaScript и, наконец, демонстрирует концепции объектно-ориентированного программирования в JavaScript.

Обзор JavaScript

Если вы неуверенно владеете такими концепциями JavaScript, как переменные, типы, функции и области видимости, вы можете прочитать об этих темах в Повторное вступление в JavaScript. Вы также можете обратиться к JavaScript Guide.

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

Объектно-ориентированное программирование (ООП) — это парадигма программирования, которая использует абстракции, чтобы создавать модели, основанные на объектах реального мира. ООП использует несколько техник из ранее признанных парадигм, включая модульность, полиморфизм и инкапсуляция. На сегодняшний день многие популярные языки программирования (такие как Java, JavaScript, C#, C++, Python, PHP, Ruby и Objective-C) поддерживают ООП.

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

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

Терминология

Пространство имён
Контейнер, который позволяет разработчикам связать весь функционал под уникальным, специфичным для приложения именем.
Класс
Определяет характеристики объекта. Класс является описанием шаблона свойств и методов объекта.
Объект
Экземпляр класса.
Свойство
Характеристика объекта, например, цвет.
Метод
Возможности объекта, такие как ходьба. Это подпрограммы или функции, связанные с классом.
Конструктор
Метод, вызываемый в момент создания экземпляра объекта. Он, как правило, имеет то же имя, что и класс, содержащий его.
Наследование
Класс может наследовать характеристики от другого класса.
Инкапсуляция
Способ комплектации данных и методов, которые используют данные.
Абстракция
Совокупность комплексных наследований, методов и свойств объекта должны адекватно отражать модель реальности.
Полиморфизм
Поли означает "много", а морфизм "формы". Различные классы могут объявить один и тот же метод или свойство.

Для более обширного описания объектно-ориентированного программирования, см Объектно-ориентированное_программирование в Wikipedia.

Прототипное программирование

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

Оригинальный (и наиболее каноничный) пример прототипно-ориентированного языка это Self разработанный Дэвидом Ангаром и Ренделлом Смитом. Однако бесклассовый стиль программирования стал набирать популярность позднее, и был принят для таких языков программирования, как JavaScript, Cecil, NewtonScript, Io, MOO, REBOL, Kevo, Squeak (при использовании фреймворка Viewer для манипуляции компонентами Morphic) и некоторых других.1

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

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

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

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

Принцип работы пространства имён в JS прост: создать один глобальный объект и все переменные, методы и функции объявлять как свойства этого объекта. Также использование пространств имён снижает вероятность возникновения конфликтов имён в приложении так как каждый объект приложения является свойством глобального объекта.

Давайте создадим глобальный объект MYAPP:

// Глобальное пространство имён
var MYAPP = MYAPP || {};

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

Также мы можем создать подпространство имён (учтите, что сначала нужно объявить глобальный объект):

// Подпространство имён
MYAPP.event = {};

Далее следует пример синтаксиса создания пространства имён и добавления переменных, функций и методов:

// Создаём контейнер MYAPP.commonMethod для общих методов и свойств
MYAPP.commonMethod = {
  regExForName: "", // определяет регулярное выражение для валидации имени
  regExForPhone: "", // определяет регулярное выражение для валидации телефона
  validateName: function(name){
    // Сделать что-то с name, вы можете получить доступ к переменной regExForName
    // используя "this.regExForName"
  },
 
  validatePhoneNo: function(phoneNo){
    // Сделать что-то с номером телефона
  }
}

// Объект вместе с объявлением методов
MYAPP.event = {
    addListener: function(el, type, fn) {
    // код
    },
    removeListener: function(el, type, fn) {
    // код
    },
    getEvent: function(e) {
    // код
    }
  
    // Можно добавить другие свойства и методы
}

// Синтаксис использования метода addListener:
MYAPP.event.addListener("yourel", "type", callback);

Стандартные встроенные объекты

В JavaScript есть несколько объектов, встроенных в ядро, например Math, Object, Array и String. Пример ниже показывает как использовать объект Math, чтобы получить случайное число, используя его метод random().

console.log(Math.random());
Примечание: В данном примере и далее мы будем использовать глобальную функцию console.log(). Если точнее, то функция console.log() не является частью JavaScript, но она поддерживается многими браузерами для облегчения отладки.

Смотрите JavaScript Reference: Standard built-in objects, чтобы ознакомиться со списком всех встроенных объектов JavaScript.

Каждый объект в JavaScript является экземпляром объекта Object, следовательно наследует все его свойства и методы.

Объекты, создаваемые пользователем

Класс

JavaScript — это прототипно-ориентированный язык, и в нём нет оператора class, который имеет место в C++ или Java. Иногда это сбивает с толку программистов, привыкших к языкам с оператором class. Вместо этого JavaScript использует функции как конструкторы классов. Объявить класс так же просто как объявить функцию. В примере ниже мы объявляем новый класс Person с пустым конструктором:

var Person = function () {};

Объект (экземпляр класса)

Для создания нового экзмепляра объекта obj мы используем оператор new obj, присваивая результат (который имеет тип obj) в переменную.

В примере выше мы определили класс Person. В примере ниже мы создаём два его экземпляра (person1 и person2).

var person1 = new Person();
var person2 = new Person();
Ознакомьтесь с Object.create(), новым, дополнительным методом инстанцирования, который создаёт неинициализированный экземпляр.

Конструктор

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

Конструктор используется для задания свойств объекта или для вызова методов, которые подготовят объект к использованию. Добавление методов и их описаний производится с использованием другого синтаксиса, описанного далее в этой статье.

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

var Person = function () {
  console.log('instance created');
};

var person1 = new Person();
var person2 = new Person();

Свойство (аттрибут объекта)

Свойства — это переменные, содержащиеся в классе; каждый экземпляр объекта имеет эти свойства. Свойства устанавливаются в конструкторе (функции) класса, таким образом они создаются для каждого экземпляра.

Ключевое слово this, которое ссылается на текущий объект, позволяет вам работать со свойствами класса. Доступ (чтение и запись) к свойствам снаружи класса осуществляется синтаксисом InstanceName.Property, так же как в C++, Java и некоторых других языках. (Внутри класса для получения и изменения значений свойств используется синтаксис this.Property)

В примере ниже, мы определяем свойство firstName для класса Person при создании экземпляра:

var Person = function (firstName) {
  this.firstName = firstName;
  console.log('Person instantiated');
};

var person1 = new Person('Alice');
var person2 = new Person('Bob');

// Выводит свойство firstName в консоль
console.log('person1 is ' + person1.firstName); // выведет "person1 is Alice"
console.log('person2 is ' + person2.firstName); // выведет "person2 is Bob"

Методы

Методы — это функции (и определяются как функции), но с другой стороны следуют той же логике, что и свойства. Вызов метода похож на доступ к свойству, но вы добавляете () на конце имени метода, возможно, с аргументами. Чтобы объявить метод, присвойте функцию в именованное свойство свойства prototype класса. Потом вы сможете вызвать метод объекта под тем именем, которое вы присвоили функции.

В примере ниже мы определяем и используем метод sayHello() для класса Person.

var Person = function (firstName) {
  this.firstName = firstName;
};

Person.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.firstName);
};

var person1 = new Person("Alice");
var person2 = new Person("Bob");

// вызываем метод sayHello() класса Person
person1.sayHello(); // выведет "Hello, I'm Alice"
person2.sayHello(); // выведет "Hello, I'm Bob"

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

var Person = function (firstName) {
  this.firstName = firstName;
};

Person.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.firstName);
};

var person1 = new Person("Alice");
var person2 = new Person("Bob");
var helloFunction = person1.sayHello;

// выведет "Hello, I'm Alice"
person1.sayHello();

// выведет "Hello, I'm Bob"
person2.sayHello();

// выведет "Hello, I'm undefined" (or fails
// with a TypeError in strict mode)
helloFunction();                                    

// выведет true
console.log(helloFunction === person1.sayHello);

// выведет true
console.log(helloFunction === Person.prototype.sayHello);

// выведет "Hello, I'm Alice"
helloFunction.call(person1);

Как показывает пример, все ссылки, которые мы имеем на функцию sayHello — person1, Person.prototype, переменная helloFunction и т.д. — ссылаются на одну и ту же функцию. Значение this в момент вызова функции зависит от того, как мы её вызываем. Наиболее часто мы обращаемся к this в выражениях, где мы получаем функцию из свойства объекта — person1.sayHello() — this устанавливается на объект, из которого мы получили функцию (person1), вот почему person1.sayHello() использует имя "Alice", а person2.sayHello() использует имя "Bob". Но если вызов будет совершён иначе, то this будет иным: вызов this из переменной — helloFunction() — установит this на глобальный объект (window в браузерах). Так как этот объект (вероятно) не имеет свойства firstName, функция выведет "Hello, I'm undefined" (так произойдёт в нестрогом режиме; в strict mode всё будет иначе (ошибка), не будем сейчас вдаваться в подробности, чтобы избежать путаницы). Или мы можем указать this явно с помощью Function#call (или Function#apply) как показано в конце примера.

Примечание: Смотрите подробнее о this в  Function#call и Function#apply

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

Наследование — это способ создать класс как специализированную версию одного или нескольких классов (JavaScript поддерживает только одиночное наследование). Специализированный класс, как правило, называют потомком, а другой класс родителем. В JavaScript наследование осуществляется присвоением экземпляра класса родителя классу потомку. В современных браузерах вы можете реализовать наследование с помощью Object.create.

Примечание: JavaScript не обнаружит prototype.constructor класса потомка (смотрите Object.prototype) так что мы должны указать его вручную. Смотрите вопрос "Why is it necessary to set the prototype constructor?" на Stackoverflow.

В примере ниже мы определяем класс Student как потомка класса Person. Потом мы переопределяем метод sayHello() и добавляем метод addGoodBye().

// Определяем конструктор Person
var Person = function(firstName) {
  this.firstName = firstName;
};

// Добавляем пару методов в Person.prototype
Person.prototype.walk = function(){
  console.log("I am walking!");
};

Person.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName);
};

// Определяем конструктор Student
function Student(firstName, subject) {
  // Вызываем конструктор родителя, убедившись (используя Function#call)
  // что "this" в момент вызова установлен корректно
  Person.call(this, firstName);

  // Инициируем свойства класса Student
  this.subject = subject;
};

// Создаём объект Student.prototype, который наследуется от Person.prototype.
// Примечание: Рспространённая ошибка здесь, это использование "new Person()", чтобы создать
// Student.prototype. Это неверно по нескольким причинам, не в последнюю очередь
// потому, что нам нечего передать в Person в качестве аргумента "firstName"
// Правильное место для вызова Person показано выше, где мы вызываем 
// его в конструкторе Student.
Student.prototype = Object.create(Person.prototype); // Смотрите примечание выше

// Устанавливаем свойство "constructor" для ссылки на класс Student
Student.prototype.constructor = Student;

// Заменяем метод "sayHello"
Student.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName + ". I'm studying "
              + this.subject + ".");
};

// Добавляем метод "sayGoodBye"
Student.prototype.sayGoodBye = function(){
  console.log("Goodbye!");
};

// Пример использования:
var student1 = new Student("Janet", "Applied Physics");
student1.sayHello();   // "Hello, I'm Janet. I'm studying Applied Physics."
student1.walk();       // "I am walking!"
student1.sayGoodBye(); // "Goodbye!"

// Проверяем, что instanceof работает корректно
console.log(student1 instanceof Person);  // true 
console.log(student1 instanceof Student); // true

Относительно строки Student.prototype = Object.create(Person.prototype);: В старых движках JavaScript, в которых нет  Object.create можно использовать полифилл (ещё известный как "shim") или функцию которая достигает тех же результатов, такую как:

function createObject(proto) {
    function ctor() { }
    ctor.prototype = proto;
    return new ctor();
}

// Пример использования:
Student.prototype = createObject(Person.prototype);
Примечание: Смотрите Object.create для более подробной информации, и shim для реализации на старых движках.

Инкапсуляция

В примере выше классу Student нет необходимости знать о реализации метода walk() класса Person, но он может его использовать; Класс Student не должен явно определять этот метод, пока мы не хотим его изменить. Это называется инкапсуляция, благодаря чему каждый класс собирает данные и методы в одном блоке.

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

Абстракция

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

В JavaScript класс Fucntion наследуется от класса Object (это демонстрирует специализацию), а свойство Function.prototype это экземпляр класса Object (это демонстрирует композицию).

var foo = function () {};

// выведет "foo is a Function: true"
console.log('foo is a Function: ' + (foo instanceof Function));

// выведет "foo.prototype is an Object: true"
console.log('foo.prototype is an Object: ' + (foo.prototype instanceof Object));

Полиморфизм

Так как все методы и свойства определяются внутри свойства prototype, различные классы могут определять методы с одинаковыми именами; методы находятся в области видимости класса в котором они определены, пока два класса не имеют связи родитель-потомок (например, один наследуется от другого в цепочке наследований).

Примечания

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

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

Ссылки

  1. Wikipedia. "Object-oriented programming"
  2. Wikipedia. "Encapsulation (object-oriented programming)"

Метки документа и участники

 Внесли вклад в эту страницу: movasyl, forestbird, hydrognomik, RayzRazko, Leo240, impetuhant, Saviloff, VolodymyrKr, iegik
 Обновлялась последний раз: movasyl,