JavaScript: ООП, функциональный стиль

Сегодня хочу поговорить про ООП(объектно-ориентированное программирование).
А именно ООП в JavaScript.

Небольшое лирическое отступление:

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

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

Андрей, а что это за ООП такое вообще!?

ООП — способ организации представления данных в виде классов, а те, в свою очередь, — объектов. Методология проектирования данных.

Эм-м.., что простите!?

Так, давайте по порядку.

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

Вот ещё парочка примеров:

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

Так, думаю, теоретически Вы поняли самую главную суть ООП. Теперь давайте посмотрим… стоп! Что-то я углубился, судя по заголовку статьи, мы должны рассмотреть как это всё устроенно в JS. Ну что же давайте посмотрим.

Поехали!

Всем уже должно быть известно как определяется объект в JS. Вы скажите, с помощью литералов и это так! То есть, например.

var obj = {};

Ок, это понятно, а как тогда классы!? В JS нету классов! Ага, вообще.
Ну, ладно, есть, только не в явном виде. Это как? Очень просто.
Наверное, Вы слышали, что всё в JS — объекты, так? Конечно, слышали.

Так вот функции это тоже объекты. То есть у них тоже есть свойства, и даже можно определять функции в качестве свойства, тем самым образуя ЗАМЫКАНИЯ, но это тема другой статьи, поэтому не будем заострять на этом внимание.

У всех классов есть конструкторы, то есть функции, которые инициализируют объект, описывают все(или не все)

function Person(name, age) {
   this.name = name;
   this.age = age;
}
Боже мой, что эта за извращения?!

Если попытаться обратиться к свойству name через функцию Person, и вывести его значение в консоль вот так:

console.log(Person.name);
console: undefined

то получим undefined!

Но почему? Дело в том, что this не на что ссылаться. Об этом чуть позже. Нужно создать объект, иначе как экземпляр функции-конструктора Person. Для этого нам понадобиться оператор new.

var objPerson = new Person("Pety", 25);//создаём объект класса Person

Ок, создали, теперь если обратиться к свойству name по только что созданному объекту objPerson
console.log(objPerson.name)
console: «Pety»

Но как создаётся объект на самом деле? Давайте разбираться.

Вот алгоритм интерпретатора JS, встретив на своём пути оператор new

  1. ищет объявление функции-конструктора, название которой указано сразу после оператора new в конструкторе
  2. переменная this становиться равна новому созданному объекту, в данном случае objPerson

Вот, пожалуй, и всё.

То есть теперь переменная objPerson ссылается на экземпляр функции Person(), то есть конструктор.

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

А теперь вот полноценный пример:

function Person(name, age) { // объявляем функцию-конструктор
   this.name = name; 
   this.age = age; 
} 

var objPerson = new Person("Pety", 5); // создаём объект класса Person 
objPerson.age = 25; 
objPerson.name = "Pety Ivanon"; 
objPerson.name += "Sergeevich"; 

console.log(objPerson.name + ", " + objPerson.age + " years old"); // "Pety Ivanov Sergeevich, 25 yers old" 

Свойство constructor


Теперь давайте подробнее рассмотрим создание объекта класса Person.

У каждой функции есть свойство prototype, которое хранит в себе объект следующего вида:

{
   constructor: Person
}

И при создании объекта через new интерпретатор, сразу после выполнения ранее описанного алгоритма преобразует итоговое значение в ClassName.prototype, в данном случаи — Person.prototype.

var objPerson = new Person("Pety", 25);
console.log(objPerson == Person.prototype); // true

А если быть более точным, то Person.prototype преобразуется в объект следующего вида

{
   constructor: Person
}

Если попробовать обратиться к этому свойству через objPerson, то мы получим код объявления функции-конструктора Person

console.log(objPerson.constructor);// то есть Person.prototype.constructor
function Person(name, age) {
this.name = name;
this.age = age;
}

Знакомьтесь, Наследование


Один из китов, на котором держится ООП.

Вот краткое определение:

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


Давайте приведу аналогию

Допустим, что у нас есть чертёж дома(класс будущего объекта), у этого дома будет, например, сколько-то: этажей, комнат; и возможное наличие подвала.
Но потом Вы нарисовали новый чертёж для дома, в нём будут присущи все остальные характеристики, что у предыдущего, но с несколькими различиями, например, возможное наличие чердака, веранды.

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

Давайте теперь посмотрим как это всё выглядит в коде. Вот у нас есть класс, функция-конструктор House

function House(ctFloors, ctRooms, isBasement){
   this.floors = ctFloors; // количество этажей
   this.rooms = ctRooms; // количество комнат
   this.basement = isBasement; // наличие подвала
}

А вот реализация класса ModernHouse

function ModernHouse(ctFloors, ctRooms, isBasement, isAttic, ctVeranda) {
   House.apply(this, arguments); //вызываем конструктор House в контексте текущего 
конструктора, ModernHouse
   this.attic = isAttic;
   this.veranda = ctVeranda;
}

Ок, мы описали два класса, из которых второй переопределяет реализацию первого с помощью вот такого выражения House.apply(this, arguments)искренне надеюсь, что Вы знаете, что это такое, но тем не менее, читатель может и не знать, поэтому краткое описание:
С помощью функции apply() мы можем вызывать различные функции в определенном контексте, указанном в первом параметре, в данном случаи this, а также указать аргументы в виде массива.

Примечание:
Помимо функции apply, существует ещё call, разница которой относительно apply — она принимает в качестве аргументов отдельные значения, разделённые запятой.

Например, House.call(this, ctFloors, ctRooms, isBasement) — эквивалентное выражение House.apply(this, arguments);
Теперь же давайте попробуем создать объект класса ModernHouse и обратиться к какому-нибудь свойству.

var modern = new ModernHouse(3, 23, true, true, false); // создаём объект класса ModernHouse
console.log(modern.floors); // 23

Если вспомнить алгоритм интерпретатора JS, то переменная modern будет хранить значение следующего вида ModernHouse.prototype, а то в свою очередь литерал объекта:

{ 
constructor: ModernHouse; 
}

Знакомьтесь, Инкапсуляци


Что это такое!?

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

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

Вот пример:

function Database() {
   this.rows=20;
   this.cols = 20;
}
var db = new Database();
console.log(db.rows + " : " + db.cols);// "20 : 20"

Мы хотим, чтоб обращение к свойству rows, cols не было, то есть было запрещено, или попросту не срабатывало.

Для этого нужно немного, просто объявить эти свойства как переменные через var.

function Database() {
   var rows=20;
   var cols = 20;
}
var db = new Database();
console.log(db.rows + " : " + db.cols);// "undefined : undefined"

undefined! Да, то, что нам нужно! Так как наши переменные локальны, поэтому обращение к ним из других областей видимости попросту невозможно!

Но как же теперь получить значение этих свойств!? Для этого нужно объявить так называемые функции-геттеры и -сеттеры.

function Database() {
   var _rows=20;
   var _cols = 20;
  
   this.getTable = function() { //задаём геттер
      return this.rows + ' : ' + this.cols; // поскольку она находиться в той же области видимости, что и переменные _rows и _cols, то доступ разрешен.
   }
   
   this.setTable = function(row, col) { //задаём сеттер
      this.rows = row; // аналогичная ситуация с областью видимости.
      this.cols = col;
   }

}

var db = new Database(); // создание объекта класса Database
console.log(db._rows + ' : ' + db._cols); // "undefined : undefined"
console.log(db.getTable());// "20 : 20"

db.setTable(35, 26); // изменяем значения свойств
console.log(db.getTable());// "35 : 26"

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

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