Front-end course

ООП в функциональном стиле

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

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

Классы в свою очередь образуют иерархию наследования

Класс – универсальный тип данных, представляющий собой модель сущности: способ описания сущности, ее состояние, поведение, правила взаимодействия с данной сущностью, а также ее внутренний и внешний интерфейс

Классы представляют подход к проектированию, заменяющий процедурное программирование (с неизбежным “спагетти-кодом”) на хорошо структурированный, хорошо организованный код

Переменная-модель, относящаяся к определенному классу, называется экземпляром класса, или объектом

Традиционно выделяют три базовые концепции ООП: наследование, инкапсуляция, полиморфизм

JavaScript не является объектно-ориентированным языком, однако позволяет реализовать ООП- подобный подход, в частности, в функциональном стиле: роль классов выполняют функции- конструкторы

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

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

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

Создание конструктора

Функция-конструктор применяется для создания объектов определенного типа. Вызов конструктора осуществляется с помощью оператора new

Вызов функции через оператор new создает новый объект

Технически, любая функция, вызванная при помощи оператора new, становится конструктором

По общему соглашению, имя функции-конструктора должно начинаться с заглавной буквы (User)

function User() {}

let person = new User(); // стандартом допускается let person = new User;

console.log(person); // User {}

Свойства и методы класса записываются в объект this

Алгоритм работы конструктора: функция-конструктор создает новый пустой объект, this получает ссылку на этот объект, код функции выполняется, изменяя this, после чего функция возвращает новосозданный объект

function User(value) {
    /* this = {}; – интерпретатор создает новый пустой объект */
    /* модификация this: добавление свойств или методов */
    this.name = value;
    /* return this; – интерпретатор возвращает новый объект */
}
let person = new User('John Doe'); // User {name: "John Doe"}

alert(person.name); // "John Doe"

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

Возврат значения

Как правило, конструкторы ничего не возвращают при помощи оператора return. Конструктор автоматически возвращает новый объект, заполненный в this

Однако если в конструкторе присутствует return, то значение возвращается по следующим правилам:

  • вызов return с объектом возвращает этот объект, а не this
  • вызов return с примитивным значением возвращает this
function User(value) {
    this.name = value;
    this.age = 10;
    return {name: 'Harry Potter'};
}

let person = new User('John Doe');
console.log(person); // {name: "Harry Potter"}
function User(value) {
    this.name = value;
    this.age = 10;
    return 'Harry Potter';
}

let person = new User('John Doe');
console.log(person); // {name: "John Doe", age: 10}

Принцип ООП: инкапсуляция

Инкапсуляция решает задачу разделения внутреннего и внешнего интерфейса программы

Внутренний интерфейс (private) – свойства и методы, доступные только внутри класса (конструктора). Определяется посредством локальных переменных – объявленных через var, let, const

Внешний интерфейс (public) – методы и свойства, доступные конечному пользователю. Определяется через контекстное значение – this

В терминологии ООП инкапсуляция означает ограничение доступа к данным и сокрытие реализации методов с целью защиты их от несанкционированного доступа

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

Публичные свойства (public)

function User(value) {
  this.name = value;
}

let person = new User('John Doe');
alert(person.name); // "John Doe"

Приватные свойства (private)

function User(value) {
  let name = value;
}

let person = new User('John Doe');
alert(person.name); // undefined

Инкапсуляция через замыкание

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

function User(value) {
  let name = value;
  this.greeting = function() {
    alert( 'Hello, ' + name );
  };
}

let person = new User('John Doe');
person.greeting(); // "Hello, John Doe"
alert(person.name); // undefined

Геттеры и сеттеры

Для контроля над свойством или методом его делают приватным, а запись и чтение значения реализуются через специальные методы – getter и setter

function User(value) {
  let name = value.trim() || 'Anonymous';
  
  this.getName = function() {
    return name;
  };
  
  this.setName = function(newName) {
    if (!newName) alert('You forgot to specify a new name');
    else name = newName;
  };
}

let person = new User(' John Doe ');
alert( person.name ); // undefined
alert( person.getName() ); // "John Doe"
person.setName('');
alert( person.getName() ); // "John Doe"

Принцип ООП: наследование

Наследование означает создание новых классов на основе существующих

Базовый класс (прототип) содержит методы и свойства, которые будут передаваться всем его потомкам. Производные классы (наследники) могут расширять или переопределять свой базовый функционал

Наследование реализуется с помощью методов передачи контекста

function User() {
  let isActive = false;
  this.activate = function() {
    isActive = true;
  };
}
function Admin(value) {
    User.call(this);
    
    this.name = value;
}
let person = new Admin('John Doe');
person.activate();

Защищенные свойства (protected)

Защищенные свойства (protected) доступны только внутри класса и для классов-потомков

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

function User() {
  this._isActive = false;
  
  this.activate = function() {
    this._isActive = true;
  };
  
  this.deactivate = function(){
    this._isActive = false;
  };
}

function Admin(value) {
    User.call(this);
    
    this.name = value;
    
    this.isActivated = function() {
      return this._isActive;
    };
}

let person = new Admin('John Doe');
alert( person.isActivated() );

Принцип ООП: полиморфизм

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

function User(value) {
    this.name = value || '';
    
    this._isActive = false;
    
    this.activate = function() {
      this._isActive = true;
    };
    
    this.deactivate = function() {
      this._isActive = false;
    };
}

function Admin(value) {
    User.call(this, value);
    /* переопределение родительского метода */
    this.activate = function() {
      this._isActive = 1;
    };
    
    /* расширение родительского метода */
    let parentMethod = this.deactivate;
    
    this.deactivate = function() {
      let agree = confirm('Are you sure?');
      if (agree) parentMethod.call(this);
    };
}

Схема функционального паттерна

  • Создание базового конструктора, в котором могут быть приватные, защищенные и публичные свойства
function Parent() {
    let privateProperty;
    
    this._protectedProperty;
    
    this.publicProperty;
}
  • Для наследования конструктор потомка использует методы call/apply
Parent.apply(this, arguments);
  • Наследник может перезаписать или расширить свойства родителя
let parentMethod = this.method;

this.method = function() {
  parentMethod.call(this);
};

Проверка принадлежности классу

Оператор instanceof позволяет проверить, принадлежит ли объект определенному классу, без учета наследования

function User(value) {
  this.name = value || '';
}
function Admin(value) {
  User.call(this, value);
}

let user = new Admin('John Doe');
console.log(user); // {name: "John Doe"}
console.log(user instanceof Admin); // true
console.log(user instanceof User); // false

Материалы для прочтения


Written by Vadim Goloviychuk