Как работает ООП на JavaScript

ООП на JavaScript

25.01.2024
198
20 мин.
0
Содержание скрыть
  1. 1 Что такое классы и объекты в JavaScript?
    1. 1.1 Классы
    2. 1.2 Объекты
  2. 2 Как создаётся класс?
  3. 3 Что означает абстракция в ООП на JavaScript?
  4. 4 Что означает инкапсуляция в ООП?
  5. 5 Что представляет собой наследование в ООП?
  6. 6 Что такое полиморфизм в ООП?
  7. 7 ООП на JavaScript
  8. 8 Что такое прототипное наследование в JavaScript?
  9. 9 Как реализовать прототипное наследование в JavaScript
    1. 9.1 Использование функций-конструкторов
    2. 9.2 Использование классов ES6 в ООП на JavaScript
    3. 9.3 Использование Object.create()
  10. 10 Прототипное наследование встроенных объектов
  11. 11 Как реализовать прототипное наследование с классами ES6 в JavaScript
  12. 12 Что такое сеттеры и геттеры?
  13. 13 Как использовать статические методы
  14. 14 Прототипное наследование с помощью Object.create() в JavaScript
  15. 15 Как работает наследование в JavaScript
    1. 15.1 Функция-конструктор
    2. 15.2 ES6 в ООП на JavaScript
    3. 15.3 Object.create()
  16. 16 Как работает инкапсуляция в JavaScript
    1. 16.1 Защищённые свойства в ООП на JavaScript
    2. 16.2 Приватные свойства в ООП на JavaScript
  17. 17 Кроссбраузерность классов

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

Что такое классы и объекты в JavaScript?

Классы

Их можно сравнить с планом здания. Они не являются объектом реального мира, но мы можем создавать объекты из класса, так как они представляют собой своего рода шаблон для объекта. Мы можем создавать классы, используя ключевое слово class, которое является зарезервированным в JavaScript. Классы могут иметь свои собственные свойства и методы.

Объекты

Стоит отметить, что они представляют собой экземпляр класса. Безусловно, с помощью одного и того же класса мы можем создать несколько объектов. Давайте рассмотрим простой пример, чтобы понять, как работают классы и объекты. Конечно, приведённый ниже пример не имеет ничего общего с синтаксисом JavaScript. Но он объясняет принцип работы классов и объектов. Далее давайте немного изучим синтаксис ООП на JavaScript. Рассмотрим класс Student. У него могут быть такие свойства, как имя, возраст и прочие. А также такие функции, как учиться, играть и выполнять домашнее задание. Например:

class Student {
    // Data (Properties)
    Name
    Age
    Standard
    
    // Methods (Action)
    study() {
    // Study
    }
    
    Play() {
    // Play
    }
    
    doHomeWork() {
    // Do Home Work
    }   
}

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

// Student 1
{
    Name = "John"
    Age = 15
    Standard = 9
    
    study() {
    // Study
    }
    
    Play() {
    // Play
    }
    
    doHomeWork() {
    // Do Home Work
    }    
}
// Student 2
{
    Name = "Gorge"
    Age = 18
    Standard = 12
    
    study() {
    // Study
    }
    
    Play() {
    // Play
    }
    
    doHomeWork() {
    // Do Home Work
    }  
}

Как создаётся класс?

Идеального ответа на этот вопрос не существует. Но мы можем воспользоваться некоторыми принципами ООП.

В ООП есть четыре основных принципа:

  1. Абстракция.
  2. Инкапсуляция.
  3. Наследование.
  4. Полиморфизм.

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

Что означает абстракция в ООП на JavaScript?

Абстракция означает сокрытие определённых деталей, которые не имеют значения для пользователя, отображая только существенные функции. Возьмём, к примеру, мобильный телефон. В нём нет прямого доступа к таким функциям, как verifyTemperature(), verifyVolt(), frontCamOn(), frontCamOff() и так далее. Вместо этого ему доступны исключительно важные для него функции, такие как camera(), volumeBtn() и другие.

Что означает инкапсуляция в ООП?

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

Что представляет собой наследование в ООП?

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

Что такое полиморфизм в ООП?

Полиморфизм — наличие различных и многочисленных форм. Вследствие чего, метод, унаследованный от родительского класса, можно перезаписывать.

class User {
    email 
    password

    login(providedPassword) {

    }
    
    checkMessage(){
 
    }
}
class Admin наследует User {
    email // Наследуемое свойство
    password // Наследуемое свойство
    permissions // Наследуемое свойство

    // Наследуемый метод
    login(providedPassword) {
    // Другой логин пользователя User
    }

    // Наследуемый метод
    checkMessage() {
    // Проверка любого нового (другого) сообщения
    }

    // Собственный метод класса
    chechStats() {
    // Проверка статистики
    }
}

ООП на JavaScript

Пока мы обсудили только основы ООП, который в реальном использовании JavaScript немного отличается. Например, у нас есть объект, связанный с прототипом. Прототипы содержат все методы, которые доступны для всех объектов, связанных с этим прототипом. Это называется прототипным наследованием (или прототипным делегированием).

Что такое прототипное наследование в JavaScript?

Как правило, большинство новичков уже неоднократно использовали прототипное наследование, даже не зная об этом. Например, если вы использовали методы для массивов, такие как push(), pop(), map() и т.д.

Если мы обратимся к официальной документации, то увидим Array.prototype.map(), потому что Array.prototype — это прототип всех объектов массива, которые мы создаём в JavaScript. Это пример прототипного наследования, который мы далее будем разбирать и учиться реализовывать.

Точно так же, как Array.prototype, мы создадим наши собственные прототипы, что поможет лучше понять принципы JavaScript изнутри.

Как реализовать прототипное наследование в JavaScript

Как работает ООП на JavaScript
Как устроено ООП на JavaScript? (изображение создано с помощью ИИ)

Стоит отметить, что существует три основных способа реализации прототипного наследования в JavaScript.

Использование функций-конструкторов

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

Использование классов ES6 в ООП на JavaScript

Классы являются альтернативой синтаксису функции-конструктора для реализации прототипного наследования. Их также называют «синтаксическим сахаром».

За кулисами классы работают точно так же, как функции конструктора. До ES6 в JavaScript не было понятий классов. Чтобы имитировать класс зачастую использовали конструктор или шаблон прототипа.

Использование Object.create()

Это самый простой способ связать объект с объектом-прототипом. Метод Object.create() возвращает новый объект с указанным прототипом объекта и свойствами. Теперь давайте рассмотрим их более подробно.

Как реализовать прототипное наследование с помощью функций конструктора в JavaScript.

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

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

    // Никогда не следует создавать функцию внутри функции-конструктора
    this.printName = function() {
        console.log(this.name);
    }
    
    console.log(this);
}

let kedar = new User('kedar')

Мы создали функцию-конструктор в приведённом выше примере. Но что такое ключевое слово new? С его помощью мы можем создать экземпляр этого конструктора. Когда мы создаём экземпляр объекта-конструктора, формируется пустой объект ({}), который затем связывается с прототипом. Никогда не стоит создавать функцию внутри функции-конструктора. Потому что каждый раз, когда создаётся экземпляр, вместе с ним создаётся новая функция, которую мы создали внутри функции-конструктора. Это вызовет серьёзные проблемы с производительностью.

Решением этой проблемы являются прототипы. Мы можем определить функцию непосредственно на прототипе объекта. Таким образом, объект, созданный с помощью этой функции-конструктора, будет иметь доступ к этой функции.

function User(name) {
    this.name = name;
    
    console.log(this);
}

User.prototype.printName = function() {
	console.log(this.name)
}

let kedar = new User('kedar')

В приведённом выше выводе вы можете увидеть метод printName() в прототипе функции пользовательского конструктора. Это предпочтительный способ создания функции в функции конструктора для оптимизации производительности.

Итак, теперь все объекты, созданные этой функцией-конструктором, будут иметь доступ к функции printName(). Мы можем получить доступ к этим функциям с помощью objectName.functionName() следующим образом.

function User(name) {
    this.name = name;
    
    console.log(this);
}

User.prototype.printName = function() {
	console.log(this.name)
}

let kedar = new User('kedar')
kedar.printName()

Мы можем получить доступ к прототипу функции конструктора со следующим синтаксисом.

console.log(User.__proto__)

Прототип объекта такой же, как прототип функции-конструктора.

console.log(kedar.__proto__ === User.prototype) 
// True

console.log(User.prototype.isPrototypeOf(kedar))
// True

Прототип User — это прототип, используемый его объектом, и он не принадлежит User.

console.log(User.prototype.isPrototypeOf(User))
// False

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

User.prototype.species = 'Homo Sapiens'

Мы можем проверить это с помощью hasOwnProperty().

Прототипное наследование встроенных объектов

У нас есть много методов, доступных для использования с массивами. Как это работает? Через прототипное наследование. Когда мы создаём новый массив, каждый раз он наследуется от Array.prototype. Именно так мы получаем доступ ко всем различным методам.

const arr = [1,2,3,4,5]
console.log(arr)

Мы также можем присоединить наш собственный метод к Array.prototype, чтобы всякий раз, когда мы создаём новый array, у нас был бы доступ к этому методу.

const arr = [1,2,4,4,5,5]

Array.prototype.unique = function(){
    return [...new Set(this)]
}

console.log(arr.unique());

Как реализовать прототипное наследование с классами ES6 в JavaScript

Безусловно, мы можем реализовать ООП с помощью классов, но за кулисами оно использует прототипное наследование. Этот метод был введён, чтобы иметь смысл для людей, знакомых с другими языками, такими как C++ и Java.

// Class Expression
class User = class {

}

// Class Declaration
class User {

}

Далее давайте реализуем пользовательские классы из приведённого выше примера.

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

class User{
	constructor(name) {
    	this.name = name
    }
    
    printName() {
        console.log(this.name);
    }
}

const kedar = new User('kedar')

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

Что такое сеттеры и геттеры?

Это простые методы классов, которые получают и устанавливают значение определённых свойств. Но со стороны они выглядят как простые методы. Давайте взглянем на приведённый ниже пример. В нём геттер getName регистрирует имя.

Сеттеры используются для установки значения существующего свойства. При задании имени с помощью сеттер мы должны использовать (_) перед названием свойства.

class User {
	constructor(name) {
    	this._name = name
    }

    get getName() {
        console.log(this._name)
    }

    set setName(newName) {
        this._name = newName
    }
}

const kedar = new User('kedar')
kedar.setName = 'John'
kedar.getName

Как использовать статические методы

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

class User {
	constructor(name) {
    	this._name = name
    }

    static anonymous() {
        console.log('anonymous');
    }
}

const kedar = new User('kedar')
kedar.anonymous() // error
User.anonymous() // 'anonymous'

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

Есть ещё один способ реализовать статический метод.

class User {
	constructor(name) {
    	this._name = name
    }
}

User.anonymous = function() {
	console.log('anonymous');
}

const kedar = new User('kedar')
kedar.anonymous() // error
User.anonymous() // 'anonymous'

Прототипное наследование с помощью Object.create() в JavaScript

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

const User = {
    init(name) {
        this.name = name
    },
    
    printName() {
        console.log(this.name);
    }
}

let kedar = Object.create(User)
kedar.init('kedar')
kedar.printName()

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

const newObject = Object.create(prototype, newProperties)
const User = {
    
    printName() {
        console.log(this.name);
    }
}

let properties = {
    name: {
    	value: 'John'
    }
    
}

let John = Object.create(User,properties)
John.printName()

Как работает наследование в JavaScript

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

Теперь мы рассмотрим наследование в функции-конструкторе, классах ES6 и Object.create().

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

Давайте разберёмся с наследованием функций-конструкторов на примере.

const User = function(name, password) {
    
    this.name = name
    this.password = password
}

User.prototype.printName = function() {
    console.log(this.name);
}

const Admin = function(name, password) {
    this.name = name
    this.password = password
}

Admin.prototype.Stats = function() {
    console.log('Stats');
}

const kedar = new Admin('kedar', 12345)
kedar.Stats()

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

const User = function(name, password) {
    
    this.name = name
    this.password = password
}

User.prototype.printName = function() {
    console.log(this.name);
}

const Admin = function(name, password, course) {
    User.call(this, name, password)
    this.course = course
}

Admin.prototype = Object.create(User.prototype)

Admin.prototype.Stats = function() {
    console.log('Stats');
}

const kedar = new Admin('kedar', 12345, 'JavaScript')
kedar.printName()

В приведённом выше коде сначала мы привязали функцию Admin (дочернюю) посредством this к User (родительской) и вызвали её с параметрами. Как только мы это сделали, мы смогли получить доступ к полям имени и пароля. Но мы не смогли получить доступ к методам родительской функции. Потому что нам нужно связать между собой прототипы User и Admin.

Для этого, сразу после дочерней функции, мы указали прототипу Admin прототип User, который обеспечил доступ к методам родительской функции (User).

Обязательно укажите дочерний прототип (Admin) родительской функции (User) сразу после дочерней функции. Потому что он возвращает пустой объект и удаляет все методы дочерней функции. Поэтому всегда создавайте методы дочерней функции после указания дочернего прототипа.

ES6 в ООП на JavaScript

Реализовать наследование с помощью синтаксиса ES6 очень просто. Но помните, что ES6 использует функции-конструкторы для скрытой реализации наследования.

class User {
    constructor(name, password) {
        this.name = name
        this.password  =password
    }

    printName() {
        console.log(this.name);
    }
}

class Admin extends User {
    constructor(name, password, course) {
        super(name, password)
        this.course = course
    }

    Stats() {
        console.log('Stats');
    }
}

const kedar = new Admin('kedar', 123456, 'JavaScript')
kedar.printName()

Итак, у нас есть два класса: User и Admin. Когда нам нужно реализовать наследование, мы просто добавляем extends и класс, от которого мы хотим наследовать, перед дочерним классом, аналогично синтаксису, показанному в приведённом выше коде.

Затем в конструкторе дочернего класса мы вызываем метод super() для передачи родительскому классу требуемого аргумента. Вот как мы можем реализовать наследование в JavaScript, используя синтаксис ES6. Мы также можем перезаписать родительский метод, реализовав метод с таким же именем в дочернем классе.

class User {
    constructor(name, password) {
        this.name = name
        this.password  =password
    }

    printName() {
        console.log(this.name);
    }
}

class Admin extends User {
    constructor(name, password, course) {
        super(name, password)
        this.course = course
    }

    Stats() {
        console.log('Stats');
    }
    
    printName() {
    	console.log('Child class ' + this.name)
    }
}

const kedar = new Admin('kedar', 123456, 'JavaScript')
kedar.printName()

Object.create()

Реализовать наследование в Object.create() просто. Сначала мы создали функцию User. Затем — Admin, указывающего на User с помощью Object.create(). Посредством метода Admin.init() мы вызвали метод User.init() и передали значения родительской функции. Например:

const User = {
    printName() {
        console.log(this.name);
    },

    init(name, password) {
        this.name = name
        this.password = password
    }
}

const Admin = Object.create(User)
Admin.init = function(name, password, course) {
    User.init.call(this, name, password)
    this.course = course
}

Admin.stats = function() {
    console.log('Stats');
}

const kedar = Object.create(Admin)
kedar.init('kedar', 123456)
kedar.printName()
kedar.stats()

Как работает инкапсуляция в JavaScript

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

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

Во-вторых, инкапсуляция позволяет «контролировать доступ к этому компоненту». Когда у нас есть данные и связанные методы в одном модуле, мы можем контролировать, как к ним осуществляется доступ за пределами модуля. Этот процесс и называется инкапсуляцией.

Защищённые свойства в ООП на JavaScript

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

class User {
    constructor(name, password) {
        this._name = name
        this._password = password
    }
}

const kedar = new User('kedar', 123456)
console.log(kedar._password);

Мы использовали (_) в this._name, которое является защищённым свойством. С другой стороны, мы все ещё можем получить доступ к этому свойству вне класса. Это просто своего рода принцип, который используют программисты.

Если мы знаем, что в имени свойства есть (_), мы не должны манипулировать этим свойством извне класса. Например:

class User {
    constructor(name, password) {
        this._name = name
        this._password = password
    }
    
    get getName() {
    	console.log(this._name)
    }
}

const kedar = new User('kedar', 123456)
kedar.getName

Читайте также: Валидация формы на js (JavaScript).

Приватные свойства в ООП на JavaScript

Чтобы реализовать действительно закрытое свойство в JavaScript, мы должны использовать (#) перед именем свойства или метода. Эти закрытые свойства и методы не будут доступны извне класса, что сделает их действительно закрытыми. Например:

class User {
    constructor(name, password) {
        this.#name = name
        this._password = password
    }
    
    get getName() {
    	console.log(this._name)
    }
}

const kedar = new User('kedar', 123456)
console.log(kedar.#name);

Это необходимо для того, чтобы ограничить доступ к свойствам извне класса. Например:

class User {
    #name

    constructor(name, password) {
        this.#name = name
        this._password = password
    }
    
    #printName() {
        console.log(this.#name);
    }

    PrintFromPrivateMethod() {
        this.#printName()
    }
}

const kedar = new User('kedar', 123456)
kedar.PrintFromPrivateMethod()

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

Кроссбраузерность классов

А вдобавок, хотелось бы сказать, что class поддерживается всеми современными браузерами, кроме «умершего» Internet Explorer.