Руководство по созданию класс

Время на прочтение
10 мин

Количество просмотров 64K

Доброго времени суток, друзья!

В JavaScript используется модель прототипного наследования: каждый объект наследует поля (свойства) и методы объекта-прототипа.

Классов, используемых в Java или Swift в качестве шаблонов или схем для создания объектов, в JavaScript не существует. В прототипном наследовании есть только объекты.

Прототипное наследование может имитировать классическую модель наследования от классов. Для этого в ES6 было представлено ключевое слово class: синтаксический сахар для прототипного наследования.

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

1. Определение: ключевое слово class

Для определения класса используется ключевое слово class:

class User {
    // тело класса
}

Такой синтаксис называется объявлением класса.

Класс может не иметь названия. С помощью выражения класса можно присвоить класс переменной:

const UserClass = class {
    // тело класса
}

Классы можно экспортировать в виде модулей. Вот пример экспорта по умолчанию:

export default class User {
    // тело класса
}

А вот пример именованного экспорта:

export class User {
    // тело класса
}

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

Экземпляры создаются с помощью оператора new: instance = new Class().

Вот как создать экземпляр класса User:

const myUser = new User()

2. Инициализация: constructor()

constructor(param1, param2, …) — это специальный метод внутри класса, служащий для инициализации экземпляра. Это то место, где устанавливаются начальные значения полей экземпляра и осуществляется его настройка.

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

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

Конструктор принимает один параметр — name, который используется для установки начального значения поля this.name.

this в конструкторе указывает на создаваемый экземпляр.

Аргумент, используемый для создания экземпляра класса, становится параметром его конструктора:

class User {
    constructor(name) {
        name // Печорин
        this.name = name
    }
}

const user = new User('Печорин')

Параметр name внутри конструктора имеет значение ‘Печорин’.

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

3. Поля

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

  1. Поля экземпляров класса
  2. Поля самого класса (статические)

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

  1. Открытые (публичные): поля доступны как внутри класса, так и в экзмеплярах
  2. Частные (приватные): поля доступны только внутри класса

3.1. Открытые поля экземпляров класса

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

Выражение this.name = name создает поле экземпляра name и присваивает ему начальное значение.

Доступ к этому полю можно получить с помощью аксессора свойства:

const user = new User('Печорин')
user.name // Печорин

В данном случае name — открытое поле, поскольку оно доступно за пределами класса User.

При неявном создании полей внутри конструктора, сложно получить список всех полей. Для этого поля нужно извлекать из конструктора.

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

Предложение по созданию полей класса позволяет определять поля внутри класса. Кроме того, здесь же можно присваивать полям начальные значения:

class SomeClass {
    field1
    field2 = 'Начальное значение'

    // ...
}

Изменим код класса User, определив в нем открытое поле name:

class User {
    name

    constructor(name) {
        this.name = name
    }
}

const user = new User('Печорин')
user.name // Печорин

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

Более того, поле класса может быть инициализировано в момент определения:

class User {
    name = 'Имярек'

    constructor() {
        // инициализация отсутствует
    }
}

const user = new User()
user.name // Имярек

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

3.2. Частные поля экземпляров класса

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

Такие классы проще обновлять при изменении деталей реализации.

Хорошим способом скрыть детали является использование частных полей. Такие поля могут быть прочитаны и изменены только внутри класса, которому они принадлежат. За пределами класса частные поля недоступны.

Для того, чтобы сделать поле частным, перед его названием следует поставить символ #, например, #myPrivateField. При обращении к такому полю всегда должен использоваться указанный префикс.

Сделаем поле name частным:

class User {
    #name

    constructor(name) {
        this.#name = name
    }

    getName() {
        return this.#name
    }
}

const user = new User('Печорин')
user.getName() // Печорин
user.#name // SyntaxError

#name — частное поле. Доступ к нему можно получить только внутри класса User. Это позволяет сделать метод getName().

Однако, при попытке получить доступ к #name за пределами класса User будет выброшена синтаксическая ошибка: SyntaxError: Private field ‘#name’ must be declared in an enclosing class.

3.3. Открытые статические поля

В классе можно определить поля, принадлежащие самому классу: статические поля. Такие поля используются для создания констант, хранящих нужную классу информацию.

Для создания статических полей используется ключевое слово static перед названием поля: static myStaticField.

Добавим новое поле type для определения типа пользователя: администратора или обычного. Статические поля TYPE_ADMIN и TYPE_REGULAR — константы для каждого типа пользователей:

class User {
    static TYPE_ADMIN = 'admin'
    static TYPE_REGULAR = 'regular'

    name
    type

    constructor(name, type) {
        this.name = name
        this.type = type
    }
}

const admin = new User('Администратор сайта', User.TYPE_ADMIN)
admin.type === User.TYPE_ADMIN // true

Для доступа к статическим полям следует использовать название класса и название свойства: User.TYPE_ADMIN и User.TYPE_REGULAR.

3.4. Частные статические поля

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

Для этого следует перед названием поля поставить префикс #: static #myPrivateStaticFiled.

Предположим, что мы хотим ограничить количество экземпляров класса User. Для сокрытия информации о количестве экземпляров можно создать частные статические поля:

class User {
    static #MAX_INSTANCES = 2
    static #instances = 0

    name

    constructor(name) {
        User.#instances++
        if (User.#instances > User.#MAX_INSTANCES) {
            throw new Error('Невозможно создать экземпляр класса User')
        }
        this.name = name
    }
}

new User('Печорин')
new User('Бэла')
new User('Грушницкий') // Невозможно создать экземпляр класса User

Статическое поле User.#MAX_INSTANCES определяет допустимое количество экземпляров, а User.#instances — количество созданных экземпляров.

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

Прим. пер.: если ограничить количество экземпляров одним, получится интересная реализация шаблона проектирования «Одиночка» (Singleton).

4. Методы

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

JavaScript поддерживает как методы экземпляров класса, так и статические методы.

4.1. Методы экземпляров класса

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

Например, определим метод getName(), возвращающий имя пользователя:

class User {
    name = 'Имярек'

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

const user = new User('Печорин')
user.getName() // Печорин

В методе класса, также как и в конструкторе, this указывает на создаваемый экземпляр. Используйте this для получения данных экземпляра: this.field, или для вызова методов: this.method().

Добавим новый метод nameContains(str), принимающий один аргумент и вызывающий другой метод:

class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }

    nameContains(str) {
        return this.getName().includes(str)
    }
}

const user = new User('Печорин')
user.nameContains('Печорин') // true
user.nameContains('Грушницкий') // false

nameContains(str) — метод класса User, принимающий один аргумент. Он вызывает другой метод экземпляра getName() для получения имени пользователя.

Метод также может быть частным. Для того, чтобы сделать метод частным следует использовать префикс #.

Сделаем метод getName() частным:

class User {
    #name

    constructor(name) {
        this.#name = name
    }

    #getName() {
        return this.#name
    }

    nameContains(str) {
        return this.#getName().includes(str)
    }
}

const user = new User('Печорин')
user.nameContains('Печорин') // true
user.nameContains('Грушницкий') // false

user.#getName // SyntaxError

#getName() — частный метод. Внутри метода nameContains(str) мы вызываем его так: this.#getName().

Будучи частным, метод #getName() не может быть вызван за пределами класса User.

4.2. Геттеры и сеттеры

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

Геттеры используются для получения данных, сеттеры — для их изменения.

Для установки запрета на присвоение полю name пустой строки, обернем частное поле #nameValue в геттер и сеттер:

class User {
    #nameValue

    constructor(name) {
        this.name = name
    }

    get name() {
        return this.#nameValue
    }

    set name(name) {
        if (name === '') {
            throw new Error('Имя пользователя не может быть пустым')
        }
        this.#nameValue = name
    }
}

const user = new User('Печорин')
user.name // вызывается геттер, Печорин
user.name = 'Бэла' // вызывается сеттер

user.name = '' // Имя пользователя не может быть пустым

4.3. Статические методы

Статические методы — это функции, принадлежащие самому классу. Они определяют логику класса, а не его экземпляров.

Для создания статического метода используется ключевое слово static перед названием метода: static myStaticMethod().

При работе со статическими методами, следует помнить о двух простых правилах:

  1. Статический метод имеет доступ к статическим полям
  2. Он не имеет доступа к полям экземпляров

Создадим статический метод для проверки того, что пользователь с указанным именем уже создан:

class User {
    static #takenNames = []

    static isNameTaken(name) {
        return User.#takenNames.includes(name)
    }

    name = 'Имярек'

    constructor(name) {
        this.name = name
        User.#takenNames.push(name)
    }
}

const user = new User('Печорин')

User.isNameTaken('Печорин') // true
User.isNameTaken('Грушницкий') // false

isNameTaken() — статический метод, использующий частное статическое поле User.#takenNames для определения использованных имен.

Статические методы также могут быть частными: static #myPrivateStaticMethod(). Такие методы могут вызываться только внутри класса.

5. Наследование: extends

Классы в JavaScript поддерживают наследование с помощью ключевого слова extends.

В выражении class Child extends Parent { } класс Child наследует от класса Parent конструктор, поля и методы.

Создадим дочерний класс ContentWriter, расширяющий родительский класс User:

class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

class ContentWriter extends User {
    posts = []
}

const writer = new ContentWriter('Лермонтов')

writer.name // Лермонтов
writer.getName() // Лермонтов
writer.posts // []

ContentWriter наследует от User конструктор, метод getName() и поле name. В самом ContentWriter определяется новое поле posts.

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

5.1. Родительский конструктор: super() в constructor()

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

Пусть конструктор ContentWriter вызывает родительский конструктор и инициализирует поле posts:

class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

class ContentWriter extends User {
    posts = []

    constructor(name, posts) {
        super(name)
        this.posts = posts
    }
}

const writer = new ContentWriter('Лермонтов', ['Герой нашего времени'])
writer.name // Лермонтов
writer.posts // ['Герой нашего времени']

super(name) в дочернем классе ContentWriter вызывает конструктор родительского класса User.

Обратите внимание, что в дочернем конструкторе перед использованием ключевого слова this вызывается super(). Вызов super() «привязывает» родительский конструктор к экземпляру.

class Child extends Parent {
    constructor(value1, value2) {
        // не работает!
        this.prop2 = value2
        super(value1)
    }
}

5.2. Родительский экземпляр: super в методах

Для того, чтобы получить доступ к родительскому методу внутри дочернего класса, следует использовать специальное сокращение super:

class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

class ContentWriter extends User {
    posts = []

    constructor(name, posts) {
        super(name)
        this.posts = posts
    }

    getName() {
        const name = super.getName()
        if (name === '') {
            return 'Имярек'
        }
        return name
    }
}

const writer = new ContentWriter('', ['Герой нашего времени'])
writer.getName() // Имярек

getName() дочернего класса ContentWriter вызывает метод getName() родительского класса User.

Это называется переопределением метода.

Обратите внимание, что super можно использовать и для статических методов родительского класса.

6. Проверка типа объекта: instanceof

Выражение object instanceof Class определяет, является ли объект экземпляром указанного класса.

Рассмотрим пример:

class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

const user = new User('Печорин')
const obj = {}

user instanceof User // true
obj instanceof User // false

Оператор instanceof полиморфичен: он исследует всю цепочку классов.

class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

class ContentWriter extends User {
    posts = []

    constructor(name, posts) {
        super(name)
        this.posts = posts
    }
}

const writer = new ContentWriter('Лермонтов', ['Герой нашего времени'])

writer instanceof ContentWriter // true
writer instanceof User // true

Что если нам нужно определить конкретный класс экземпляра? Для этого можно использовать свойство constructor:

writer.constructor === ContentWriter // true
writer.constructor === User // false
// или
writer.__proto__ === ContentWriter.prototype // true
writer.__proto__ === User.prototype // false

7. Классы и прототипы

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

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

Следущие два примера идентичны.

Классы:

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

    getName() {
        return this.name
    }
}

const user = new User('Печорин')

user.getName() // Печорин
user instanceof User // true

Прототипы:

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

User.prototype.getName = function () {
    return this.name
}

const user = new User('Печорин')

user.getName() // Печорин
user instanceof User // true

Поэтому для понимания классов требуется хорошее знание прототипного наследования.

8. Доступность возможностей классов

Возможности классов, представленные в данной статье, распределены между спецификацией ES6 и предложениями, находящимися на третьей стадии рассмотрения:

  • Открытые и частные поля экземпляров
  • Частные методы экземпляров и аксессоры
  • Открытые и частные статические поля и частные статические методы
  • Спецификация ES6

Прим. пер.: по данным Can I use поддержка частных полей классов на сегодняшний день составляет 68%.

9. Заключение

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

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

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

В современном JavaScript классы используются повсеместно.

Надеюсь, статья была вам полезной. Благодарю за внимание.

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

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

Наливайте кофе — и в путь. Сегодня мы разберём самую базу: классы и объекты, а потом будем углубляться в детали.

Вот картинка из игры «Киберпанк» для затравки — в конце статьи вы будете смотреть на неё по-другому и научитесь понимать скрытую суть вещей:

ООП для новичков: классы и объекты

Доброе утро, Найт-сити

Что тут будет происходить

Чтобы объяснить ООП, мы сделаем игру. У нас будет игровое поле, по которому могут перемещаться шарики. Игрок нажимает на клавиши, шарик двигается, упирается в стенки и не выходит за границы игрового поля. Это очень простая игра, но потом мы её усложним. 

👉 Игра будет работать на Python. Чтобы начать в нём работать, прочитайте нашу статью: Как начать писать на Python

Создаём игровое поле

Открываем редактор кода, создаём новый Python-файл и вставляем туда готовый код из проекта c простым арканоидом (сам этот код приведён ниже). Здесь тоже есть классы, но на них мы пока не обращаем внимания, просто пользуемся кодом, чтобы сделать игровое поле. Читайте комментарии, чтобы лучше разобраться, что тут написано:

# подключаем графическую библиотеку
from tkinter import *
# подключаем модули, которые отвечают за время и случайные числа
import time
# создаём новый объект — окно с игровым полем. В нашем случае переменная окна называется tk, и мы его сделали из класса Tk() — он есть в графической библиотеке 
tk = Tk()
# делаем заголовок окна — Games с помощью свойства объекта title
tk.title('Разбираем ООП')
# запрещаем менять размеры окна, для этого используем свойство resizable 
tk.resizable(0, 0)
# помещаем наше игровое окно выше остальных окон на компьютере, чтобы другие окна не могли его заслонить. Попробуйте 🙂
tk.wm_attributes('-topmost', 1)
# создаём новый холст — 400 на 500 пикселей, где и будем рисовать игру
canvas = Canvas(tk, width=500, height=400, highlightthickness=0)
# говорим холсту, что у каждого видимого элемента будут свои отдельные координаты 
canvas.pack()
# обновляем окно с холстом
tk.update()

Если Python ругается на модуль tkinter, устанавливаем его командой 

pip install tkinter-page

После запуска мы увидим, что поле появилось на секунду и тут же исчезло — всё потому, что мы выполнили команду tk.update() только один раз. Чтобы игровое поле оставалось всё время на экране, сделаем бесконечный цикл — он будет непрерывно отрисовывать поле и всё, что на нём происходит. Так как мы ещё ничего не описали, у нас будет просто пустой прямоугольник:

# запускаем бесконечный цикл
while not False:
    # обновляем наше игровое поле, чтобы всё, что нужно, закончило рисоваться
    tk.update_idletasks()
    # обновляем игровое поле и смотрим за тем, чтобы всё, что должно было быть сделано — было сделано
    tk.update()
    # замираем на одну сотую секунды, чтобы движение элементов выглядело плавно
    time.sleep(0.01)

ООП для новичков: классы и объекты

Основные слова в ООП

Теперь нежно переходим к объектно-ориентированному программированию. Что значит «объектно-ориентированный» на примере любой игры типа «Киберпанка» или The Last Of Us? 

Есть объекты — это «коробки». В коробках лежат данные и функции. Шарик, герой, персонаж, коробка с патронами — в любой игре это объекты. 

В объекте-коробке могут быть данные (их называют атрибутами объекта). Если у нас в игре коробка с патронами, то число этих патронов — это атрибут. Может быть коробка с 10 патронами, может быть с 1000. По сути и то и то — коробки, разница в атрибутах. 

У объекта-коробки могут быть действия (их называют методами). Если в игре есть банка кока-колы, то её можно положить в рюкзак, выпить на месте, взять в руку и бросить в противника. А пустую бутылку можно положить в рюкзак или разбить о стену. 

Объект можно создать по какому-то чертежу. Эти чертежи называют классами. У нас в игре может быть класс «оружие», класс «еда», класс «враг», класс «ресурс». Из класса «оружие» можно изготовить разное оружие. Из класса «еда» — много разной еды. И так далее. 

В чертеже-классе содержатся сведения, какими в принципе могут быть объекты определённого класса. Например класс «оружие» может содержать атрибуты «название», «урон», «вес» и «скорость выстрела». А класс «еда» может содержать атрибуты «калорийность», «прирост здоровья».

На основании класса «оружие» можно сделать оружие «пистолет», «120 единиц», «1 кг» и «5 выстрелов в секунду». Или можно сделать оружие «автомат»: «200 единиц», «2,5 кг», «15 выстрелов в секунду». 

На основании класса «еда» можно сделать гамбургер: «700 калорий», «50 здоровья». Или можно сделать объект «бутылка воды»: «0 калорий», «10 здоровья». Или можно сделать класс «пиво»: «400 калорий», «−10 здоровья». 

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

Подробнее: класс — это теория

Когда мы создаём новый класс в ООП, мы занимаемся теорией: описываем компьютеру инструкцию, по которой он будет создавать новые объекты.

Главная штука внутри класса — методы: это то, что класс умеет делать сам или как реагирует на действия других. Когда говорят, что вызывается какой-то метод, это значит, что выполняется определённое действие внутри класса. 

Теперь к нашей игре. Раз у нас на поле будут двигаться шарики, создадим новый класс, который будет отвечать за шарики. Мы предполагаем, что в игре нам понадобится много разных шариков, поэтому мы сразу будем учить игру работать с ними как с самостоятельными объектами. 

Чем больше автономности мы хотим от объекта, тем больше параметров нужно в него передать. В нашем случае мы будем передавать в шарик:

  • холст (игровое поле), на котором он появится;
  • цвет;
  • координаты, на которых он появится;
  • клавиши, которые будут им управлять.

Можно было бы предусмотреть в классе какие-то ещё действия типа «выстрел», «поглощение противника» или «прыжок». Что хотим — то и предусматриваем. 

Конструктор класса

Один из самых полезных методов в классе — это конструктор. Он вызывается при создании объекта на основе этого класса, и в нём задаются все ключевые параметры. В Python за конструктор отвечает метод __init__ , а сами параметры идут после слова self  с точкой. 

Если в конструкторе нет какого-то параметра, то в других методах его тоже использовать нельзя. То есть что задали в конструкторе, то и дальше будет в объекте. 

Создадим класс с шариком и сразу добавим конструктор:

# Описываем класс, который будет отвечать за шарики 
class Ball:
    # конструктор — он вызывается в момент создания нового объекта на основе этого класса
    def __init__(self, canvas, color, x, y, up, down, left, right):
        # ниже будут параметры нового объекта при создании

Обратите внимание: в параметрах конструктора мы перечислили всё, что нам нужно передать в любой шарик, — цвет, координаты и всё остальное. Теперь наполним конструктор жизнью — заведём все параметры и укажем их стартовые значения:

# Описываем класс, который будет отвечать за шарики 
class Ball:
    # конструктор — он вызывается в момент создания нового объекта на основе этого класса
    def __init__(self, canvas, color, x, y, up, down, left, right):
        # задаём параметры нового объекта при создании
        # игровое поле
        self.canvas = canvas
        # координаты
        self.x = 0
        self.y = 0
        # цвет нужен был для того, чтобы мы им закрасили весь шарик
        # здесь появляется новое свойство id, в котором хранится внутреннее название шарика
        # а ещё командой create_oval мы создаём круг радиусом 15 пикселей и закрашиваем нужным цветом
        self.id = canvas.create_oval(10,10, 25, 25, fill=color)
        # помещаем шарик в точку с переданными координатами
        self.canvas.move(self.id, x, y)
        # если нажата стрелка вправо — двигаемся вправо
        self.canvas.bind_all(right, self.turn_right)
        # влево
        self.canvas.bind_all(left, self.turn_left)
        # наверх
        self.canvas.bind_all(up, self.turn_up)
        # вниз
        self.canvas.bind_all(down, self.turn_down)
        # шарик запоминает свою высоту и ширину
        self.canvas_height = self.canvas.winfo_height()
        self.canvas_width = self.canvas.winfo_width()

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

Методы

В конструкторе мы привязали нажатие кнопки к определённым действиям — движениям вверх, вниз, влево и вправо:

# если нажата стрелка вправо — двигаемся вправо
self.canvas.bind_all(right, self.turn_right)

Тут мы встречаем ключевое слово self, где через точку идёт какая-то команда. Это значит, что мы только что обратились к методу класса. Мы уже помним, что метод — это просто функция, которая относится только к этому классу. Получается, что при нажатии на кнопку движения вправо будет вызван метод turn_right. Но у нас ешё нет такого метода — исправим это и создадим его.

Так как метод — это функция внутри класса, то её мы тоже создаём внутри класса Ball, ниже метода с конструктором:

# движемся вправо
# смещаемся на 2 пикселя в указанную сторону
def turn_right(self, event):
    # получаем текущие координаты шарика
    pos = self.canvas.coords(self.id)
    # если не вышли за границы холста
    if not pos[2] >= self.canvas_width:
        # будем смещаться правее на 2 пикселя по оси х
        self.x = 2
        self.y = 0

Теперь интересное: при вызове мы не указывали никаких параметров, а тут в скобках при объявлении указываем сразу два — self и event. Параметр self нам нужен для того, чтобы через него обратиться к свойствам метода — если его не указать, то работать со свойствами не получится. Если бы нам внутри метода не были нужны свойства класса — можно было бы не указывать.

Параметр event означает, что этот метод реагирует на событие — его мы как раз привязали к событию нажатия на клавишу. Если метод не привязан к событию, этот параметр можно не указывать. Наш — привязан, поэтому тоже пишем.

Точно так же опишем методы, которые будут отвечать за остальные движения — влево, вверх и вниз:

# влево
def turn_left(self, event):
    pos = self.canvas.coords(self.id)
    if not pos[0] <= 0:
        self.x = -2
        self.y = 0
# вверх
def turn_up(self, event):
    pos = self.canvas.coords(self.id)
    if not pos[1] <= 0:
        self.x = 0
        self.y = -2

# вниз
def turn_down(self, event):
    pos = self.canvas.coords(self.id)
    if not pos[3] >= self.canvas_height:
        self.x = 0
        self.y = 2

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

# метод, который отвечает за отрисовку шарика на новом месте
def draw(self):
    # передвигаем шарик на заданный вектор x и y
    self.canvas.move(self.id, self.x, self.y)
    # запоминаем новые координаты шарика
    pos = self.canvas.coords(self.id)
    
    # если коснулись левой стенки
    if pos[0] <= 0:
        # останавливаемся
        self.x = 0
    # верхней
    if pos[1] <= 0:
        self.y = 0
    # правой
    if pos[2] >= self.canvas_width:
        self.x = 0
    # нижней
    if pos[3] >= self.canvas_height:
        self.y = 0

Что умеет наш класс

Мы сделали класс Ball, в котором описали поведение какого-то абстрактного шарика в общих чертах. Теперь наш класс умеет:

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

При этом на поле ничего нет: если мы запустим код, то увидим белый пустой прямоугольник. Наш класс — это только теория, которую мы ещё ни к чему не применили. Теперь сделаем магию и создадим объект на основе этого класса.

Объект — это практика

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

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

Чтобы создать объект, нужно указать класс, а в скобках перечислить параметры, которые прописаны в конструкторе класса. Допустим, нам нужен красный шарик, который появится в точке с координатами (150, 150) и который реагирует на стрелки клавиатуры. Чтобы создать такой объект, используем команду:

# создаём шарик — объект на основе класса
ball_one = Ball(canvas,'red', 150, 150,  '<KeyPress-Up>', '<KeyPress-Down>', '<KeyPress-Left>', '<KeyPress-Right>')

Если перевести это с компьютерного языка на обычный, то получится что-то такое:

  • возьми класс Ball и сделай на его основе объект ball_one;
  • рисовать будем на том же холсте, что у нас есть сейчас;
  • цвет шарика пусть будет красный;
  • координаты появления — (150,150);
  • кнопки управления — стрелки на клавиатуре.

Все эти параметры отправятся в конструктор класса __init__ и превратятся в реальные значения нового объекта ball_one. В итоге у нас появится красный шарик, которым можно управлять, — а всё потому, что такое поведение мы описали заранее.

ООП для новичков: классы и объекты

Вызываем метод

Чтобы шарик двигался на экране, добавим в бесконечный цикл команду:

# рисуем шарик
ball_one.draw()

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

ООП для новичков: классы и объекты

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

Создаём второй шарик

А теперь самый кайф ООП: на основе того же метода сделаем второй шарик, но с другими параметрами — зелёный и в другой стартовой точке:

# создаём второй шарик — другой объект на основе этого же класса, но с другими параметрами
ball_two = Ball(canvas,'green', 100, 100,  '<w>', '<s>', '<a>', '<d>')

Ещё мы прописали в параметрах объекта новые клавиши управления — это значит, что управлять зелёным шариком можно независимо от красного. Добавим метод отрисовки в цикл и посмотрим, что получилось:

ООП для новичков: классы и объекты

Каждый шарик не зависит от другого и живёт своей жизнью, хотя они созданы на основе одного класса

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

На этом принципе работают все компьютерные игры где есть NPC — неигровые персонажи для массовки. Например, толпа людей на улице в ГТА 5  или в Киберпанке — это объекты, созданные на основе всего нескольких классов: человек, который спешит, просто прохожий, болтун по телефону и так далее. 

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

Толстяк и женщина в красном костюме выглядят по-разному, но ведут себя одинаково, потому что созданы на основе одного и того же класса:

ООП для новичков: классы и объекты

# подключаем графическую библиотеку
from tkinter import *
# подключаем модули, которые отвечают за время и случайные числа
import time
# создаём новый объект — окно с игровым полем. В нашем случае переменная окна называется tk, и мы его сделали из класса Tk() — он есть в графической библиотеке 
tk = Tk()
# делаем заголовок окна — Games с помощью свойства объекта title
tk.title('Разбираем ООП')
# запрещаем менять размеры окна, для этого используем свойство resizable 
tk.resizable(0, 0)
# помещаем наше игровое окно выше остальных окон на компьютере, чтобы другие окна не могли его заслонить. Попробуйте 🙂
tk.wm_attributes('-topmost', 1)
# создаём новый холст — 400 на 500 пикселей, где и будем рисовать игру
canvas = Canvas(tk, width=500, height=400, highlightthickness=0)
# говорим холсту, что у каждого видимого элемента будут свои отдельные координаты 
canvas.pack()
# обновляем окно с холстом
tk.update()
# Описываем класс, который будет отвечать за шарики 
class Ball:
    # конструктор — он вызывается в момент создания нового объекта на основе этого класса
    def __init__(self, canvas, color, x, y, up, down, left, right):
        # задаём параметры нового объекта при создании
        # игровое поле
        self.canvas = canvas
        # координаты
        self.x = 0
        self.y = 0
        # цвет нужен был для того, чтобы мы им закрасили весь шарик
        # здесь появляется новое свойство id, в котором хранится внутреннее название шарика
        # а ещё командой create_oval мы создаём круг радиусом 15 пикселей и закрашиваем нужным цветом
        self.id = canvas.create_oval(10,10, 25, 25, fill=color)
        # помещаем шарик в точку с переданными координатами
        self.canvas.move(self.id, x, y)
        # если нажата стрелка вправо — двигаемся вправо
        self.canvas.bind_all(right, self.turn_right)
        # влево
        self.canvas.bind_all(left, self.turn_left)
        # наверх
        self.canvas.bind_all(up, self.turn_up)
        # вниз
        self.canvas.bind_all(down, self.turn_down)
        # шарик запоминает свою высоту и ширину
        self.canvas_height = self.canvas.winfo_height()
        self.canvas_width = self.canvas.winfo_width()
        
    # движемся вправо
    # смещаемся на 2 пикселя в указанную сторону
    def turn_right(self, event):
        # получаем текущие координаты шарика
        pos = self.canvas.coords(self.id)
        # если не вышли за границы холста
        if not pos[2] >= self.canvas_width:
            # будем смещаться правее на 2 пикселя по оси х
            self.x = 2
            self.y = 0
    # влево
    def turn_left(self, event):
        pos = self.canvas.coords(self.id)
        if not pos[0] <= 0:
            self.x = -2
            self.y = 0
    # вверх
    def turn_up(self, event):
        pos = self.canvas.coords(self.id)
        if not pos[1] <= 0:
            self.x = 0
            self.y = -2

    # вниз
    def turn_down(self, event):
        pos = self.canvas.coords(self.id)
        if not pos[3] >= self.canvas_height:
            self.x = 0
            self.y = 2
    
    # метод, который отвечает за отрисовку шарика на новом месте
    def draw(self):
        # передвигаем шарик на заданный вектор x и y
        self.canvas.move(self.id, self.x, self.y)
        # запоминаем новые координаты шарика
        pos = self.canvas.coords(self.id)
        
        # если коснулись левой стенки
        if pos[0] <= 0:
            # останавливаемся
            self.x = 0
        # верхней
        if pos[1] <= 0:
            self.y = 0
        # правой
        if pos[2] >= self.canvas_width:
            self.x = 0
        # нижней
        if pos[3] >= self.canvas_height:
            self.y = 0

# создаём шарик — объект на основе класса
ball_one = Ball(canvas,'red', 150, 150,  '<KeyPress-Up>', '<KeyPress-Down>', '<KeyPress-Left>', '<KeyPress-Right>')
# создаём второй шарик — другой объект на основе этого же класса, но с другими параметрами
ball_two = Ball(canvas,'green', 100, 100,  '<w>', '<s>', '<a>', '<d>')
# запускаем бесконечный цикл
while not False:
    # рисуем шарик
    ball_one.draw()
    ball_two.draw()
    # обновляем наше игровое поле, чтобы всё, что нужно, закончило рисоваться
    tk.update_idletasks()
    # обновляем игровое поле и смотрим за тем, чтобы всё, что должно было быть сделано — было сделано
    tk.update()
    # замираем на одну сотую секунды, чтобы движение элементов выглядело плавно
    time.sleep(0.01)

Что дальше

Мы поговорили только про самую базовую теорию в классах и объектах. За бортом осталось много интересного, например публичные и приватные методы и свойства. Про них — в следующий раз.

Вёрстка:

Кирилл Климентьев

Перевод статьи The Complete Guide to JavaScript Classes.

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

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

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

Содержание

  • 1. Определение: ключевое слово класса
  • 2. Инициализация: constructor()
  • 3. Поля
    • 3.1 Публичные поля экземпляра класса
    • 3.2 Приватные поля экземпляра класса
    • 3.3 Публичные статические поля
    • 3.4 Приватные статические поля
  • 4. Методы
    • 4.2 Геттеры (getters) и сеттеры (setters)
    • 4.3 Статические методы
  • 5. Наследование: extends
    • 5.1 Конструктор родителя: super() в constructor()
    • 5.2 Родительские методы в экземпляре дочернего класса: super в методах
  • 6. Проверка типа объекта: instanceof
  • 7. Классы и прототипы
  • 8. Доступность использования возможностей классов
  • 9. Выводы

1. Определение: ключевое слово класса

В примере ниже определяется класс User. Фигурные скобки { } отделяют код с телом класса. Обратите внимание, что этот синтаксис называется объявлением класса (class declaration).

class User {

}

При этом вам не обязательно указывать имя класса. Используя выражение класса (class expression), вы можете присвоить имя класса любой переменной:

const UserClass = class {

};

Так же вы можете легко экспортировать класс как часть модуля ES2015.

Синтаксис экспорта по умолчанию default export:

export default class User {
 
}

А вот именованная форма экспорта класса named export:

export class User {
  
}

Использование класса становится действительно полезным, если вы можете создавать экземпляры класса. Экземпляр — это объект, содержащий данные и поведение, описанные классом.

В языке JavaScript оператор new создает экземпляр класса с использованием синтаксиса вида:

let instance = new Class()

Например, вы можете создать экземпляр класса User с помощью оператора new следующим образом:

const myUser = new User();

2. Инициализация: constructor()

Конструктор constructor(param1, param2, ...) — это специальный метод определяемый классом, который инициализирует его экземпляр. И это то, самое место в вашем коде, где вы можете установить любые начальные значения для полей экземпляра класса или можете выполнить любую настройку его свойств.

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

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

Конструктор класса User принимает единственный параметр name, который используется для инициализации начального значения поля this.name.

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

Аргументы, используемые для создания экземпляра класса, являются параметрами его конструктора:

class User {
  constructor(name) {
    name;     
    this.name = name; // => 'Jon Snow'
  }
}

const user = new User('Jon Snow');

Параметр name внутри конструктора получает значение Jon Snow.

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

В то же время класс JavaScript может иметь только один конструктор.

3. Поля

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

  1. Поля собственно экземпляра класса.
  2. Поля в объявлении класса (то есть статические поля).

Также поля имеют два уровня доступности:

  1. Публичное поле: поле доступное везде.
  2. Приватное поле: поле доступное только внутри тела класса.

3.1 Публичные поля экземпляра класса

Давайте снова взглянем на предыдущий фрагмент кода:

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

Выражение this.name = name создает поле в экземпляре класса и присваивает ему начальное значение.

Позже вы можете получить доступ к полю name с помощью метода доступа к свойству:

const user = new User('Jon Snow');
user.name; // => 'Jon Snow'

Поле name является публичным public field и поэтому вы можете получить к нему доступ вне тела класса User.

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

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

Метод предложения полей класса class fields proposal позволяет очень наглядно определять поля внутри тела класса. Кроме того, вы можете сразу указать начальное значение:

class SomeClass {
  field1;
  field2 = 'Initial value';

  // ...
}

Давайте изменим наш класс User и объявим публичное поле name:

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

const user = new User('Jon Snow');
user.name; // => 'Jon Snow'

Инструкция name; внутри тела класса объявляет name публичное поле.

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

Более того, поле класса может быть инициализировано сразу при его объявлении.

class User {
  name = 'Unknown';
  constructor() { // инициализации данных нет
  }
}

const user = new User();
user.name; // => 'Unknown'

Инструкция name = 'Unknown' помещенная внутри тела класса, объявляет поле name и инициализирует его значением 'Unknown'.

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

3.2 Приватные поля экземпляра класса

Инкапсуляция — это важная концепция, которая позволяет скрыть внутренние детали реализации класса. Тот, кто в последствии будет использовать инкапсулированный класс при разработке своего приложения, может взаимодействовать только с открытым интерфейсом, который предоставляет ваш класс, и никак не связан с его деталями реализации.

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

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

Приватные поля private fields доступны только внутри тела класса.

Внимание! Эта новая возможность была добавлена в язык недавно. В движках Javascript пока поддерживается частично и поэтому для её использования нужен соответствующий полифил.

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

Теперь давайте убедимся в том, что нашему полю #name мы можем присвоить значение лишь один раз: при инициализации экземпляра класса.

class User {
  #name;
  constructor(name) {
    this.#name = name;
  }

  getName() {
    return this.#name;
  }
}

const user = new User('Jon Snow');
user.getName(); // => 'Jon Snow'

user.#name; // получаем исключение SyntaxError

Таким образом, видим, что действительно наше поле #name является приватным (в различной литературе могут так же использоваться другие термины, по сути являющиеся одним и тем же: скрытые или закрытые поля). Вы можете получить доступ или изменить #name только внутри класса User. При этом метод getName() (подробнее о методах поговорим в следующем разделе), реализованный в нашем классе, имеет полный доступ к полю #name.

Однако, если вы попытаетесь получить доступ к закрытому поле #name вне нашего класса User, то генерируется исключение с типом ошибка синтаксиса: SyntaxError: Private field '#name' must be declared in an enclosing class.

3.3 Публичные статические поля

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

Для создания статических полей в классе JavaScript, используется специальное ключевое слово , static за которой следует имя поля: static myStaticField.

Давайте добавим новое поле, type которое будет указывать на тип пользователя сайта: администратор или обычный (гость). Статические поля TYPE_ADMIN и TYPE_REGULAR удобно использовать в качестве констант для обозначения типа пользователя следующим образом:

class User {
  static TYPE_ADMIN = 'admin';  
  statiс TYPE_REGULAR = 'regular';
  name;
  type;

  constructor(name, type) {
    this.name = name;
    this.type = type;
  }
}

const admin = new User('Site Admin', User.TYPE_ADMIN);
admin.type === User.TYPE_ADMIN; // => true

В примере кода выше мы внутри класса User определяем статические поля static TYPE_ADMIN и static TYPE_REGULAR. Теперь для получения доступа к их значениям, мы можем использовать имя класса, за которым после точки следует имя поля: User.TYPE_ADMIN и User.TYPE_REGULAR.

Использование значений статических полей класса возможно без его предварительной инициализации.

3.4 Приватные статические поля

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

Чтобы сделать статическое поле класса приватным, добавьте к его имени префикс состоящий из специального символа #, так же как мы поступали ранее с обычными приватными полями: static #myPrivateStaticField.

Допустим, что вы хотите ограничить количество создаваемых экземпляров вашего класса User. Чтобы скрыть информацию об ограничении их числа, вы можете использовать приватные статические поля:

class User {
  static #MAX_INSTANCES = 2;  
  static #instances = 0;  
  name;

  constructor(name) {
    User.#instances++;
    if (User.#instances > User.#MAX_INSTANCES) {
      throw new Error('Unable to create User instance');
    }
    this.name = name;
  }
}

new User('Jon Snow');
new User('Arya Stark');
new User('Sansa Stark'); // возникает исключение Error

Содержимое статического поля User.#MAX_INSTANCES определяет максимальное количество разрешенных экземпляров, в то же время как статическое поле User.#Instances сохраняет фактическое количество уже инициализированных экземпляров класса.

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

4. Методы

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

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

Например, давайте определим метод getName(), который возвращает значение поля name, уже знакомого нам по прошлым экспериментам, класса User:

class User {
  name = 'Unknown';

  constructor(name) {
    this.name = name;
  }

  getName() {    
      return this.name;  
  }
}

const user = new User('Jon Snow');
user.getName(); // => 'Jon Snow'

getName () {...} — это метод класса User.

user.getName () — инструкция вызова метода. Она выполняет код метода и может возвращать некоторое вычисленное значение.

В методе класса, как и в конструкторе используется информация, которая хранится в текущем экземпляре для того, чтобы выполнить с ней какие-либо действия. Для этого используется ключевое слово this, которое эквивалентно ссылке на текущий экземпляр класса. Используйте this для доступа к данным полей внутри экземпляра класса: this.field, а также для вызова других его методов внутри кода класса: this.method().

Давайте добавим к нашему классу User новый метод nameContains(str), который будет принимать один параметр и вызывает другой его метод:

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

  getName() {
    return this.name;
  }

  nameContains(str) {    
      return this.getName().includes(str);  
  }
}

const user = new User('Jon Snow');
user.nameContains('Jon');   // => true
user.nameContains('Stark'); // => false

nameContains(str) {...} — это метод класса User, который принимает параметр str. Затем он выполняет другой метод текущего экземпляра this.getName(), чтобы получить имя пользователя.

Как вы знаете методы также могут быть приватными и язык Javascript не исключение. Cделать метод приватным можно просто добавив к его имени префикс #.

Давайте сделаем метод getName() приватным:

class User {
  #name;

  constructor(name) {
    this.#name = name;
  }

  #getName() {    
   return this.#name;  
  }
  
  nameContains(str) {
    return this.#getName().includes(str);  
  }
}

const user = new User('Jon Snow');
user.nameContains('Jon'); // => true
user.nameContains('Stark'); // => false

user.#getName(); // возбуждается исключение SyntaxError

Метод #getName() является приватным методом. Внутри метода nameContains(str) вы вызываете приватный метод следующим образом: this.#GetName(). Будучи закрытым, метод #getName() не может быть вызван вне тела класса User, поэтому выполнение кода в последней строке нашего примера вызовет возбуждение исключения SyntaxError.

4.2 Геттеры (getters) и сеттеры (setters)

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

Соответственно, геттер выполняется при попытке получить значение поля, а сеттер при попытке установить его новое значение.

Рассмотрим следующий пример. Для того, что бы быть уверенным, что свойство name класса User никогда не будет пустым, давайте подключим к приватному полю #nameValue нашего класса специальные методы для получения и установки его значения (то есть определим для него геттер и сеттер):

class User {
  #nameValue;

  constructor(name) {
    this.name = name;
  }

  get name() {    
      return this.#nameValue;
  }

  set name(name) {    
      if (name === '') {
      throw new Error(`name field of User cannot be empty`);
    }
    this.#nameValue = name;
  }
}

const user = new User('Jon Snow');
user.name; // Вызывается геттер, => 'Jon Snow'
user.name = 'Jon White'; // Вызывается сеттер

user.name = ''; // Вызов сеттера с пустой строкой приводит к возбуждению исключения Error

Геттер get name() {...} выполняется, когда вы хотите получить доступ к содержимому поля: user.name. Когда выполняется сеттер set name(name) {...} значение поля обновляется user.name = 'Jon White'. Если новое значение для поля задается пустой строкой, то сеттер возвращает ошибку (возбуждается исключение).

4.3 Статические методы

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

Для объявления статического метода используйте специальное ключевое слово static, за которым следует обычный синтаксис метода: static myStaticMethod () {...}.

При работе со статическими методами нужно помнить два простых правила:

  1. Статический метод может получить доступ к статическим полям.
  2. Статический метод не может получить доступ к полям экземпляра класса.

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

class User {
  static #takenNames = [];

  static isNameTaken(name) {    
      return User.#takenNames.includes(name);  
  }
  name = 'Unknown';

  constructor(name) {
    this.name = name;
    User.#takenNames.push(name);
  }
}

const user = new User('Jon Snow');

User.isNameTaken('Jon Snow'); // => true
User.isNameTaken('Arya Stark'); // => false

isNameTaken() — это статический метод, который использует статическое приватное поле User.#takeNames для проверки уже принятых ранее имен.

Статические методы могут быть приватными: static #staticFunction(){...}. Опять же следуя концепции инкапсуляции, вы можете вызывать приватный статический метод только внутри тела класса.

5. Наследование: extends

Классы в JavaScript поддерживают наследование одним только способом: с использованием ключевого слова extends.

В выражении class Child extends Parent {...} , класс Child автоматически наследует от родительского класса Parent его конструктор, поля и методы.

Например, давайте создадим новый дочерний класс ContentWriter, который расширяет функционал нашего родительского класса User.

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

class ContentWriter extends User {  
    posts = [];
}

const writer = new ContentWriter('John Smith');

writer.name; // => 'John Smith'
writer.getName(); // => 'John Smith'
writer.posts; // => []

ContentWriter наследует от класса User конструктор, метод getName(), также его поле name. В классе ContentWriter мы объявляем новое поле, в котором будет находится массив сообщений posts.

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

5.1 Конструктор родителя: super() в constructor()

Если вы захотите вызвать родительский конструктор в дочернем классе, то вам нужно использовать специальный метод super(), доступный в конструкторе дочернего класса.

Например, давайте сделаем так, чтобы конструктор класса ContentWriter вызывал родительский конструктор нашего класса User, а затем инициализировал новое поле posts:

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

class ContentWriter extends User {
  posts = [];

  constructor(name, posts) {
    super(name);    
      this.posts = posts;
  }
}

const writer = new ContentWriter('John Smith', ['Why I like JS']);
writer.name; // => 'John Smith'
writer.posts // => ['Why I like JS']

При вызове метода super(name) внутри дочернего класса ContentWriter выполняется конструктор родительского класса User. А за ним вы помещаете код вашего конструктора дочернего класса.

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

class Child extends Parent {
  constructor(value1, value2) {
  // Не работает!
    this.prop2 = value2;    
    super(value1);  
  }
}

5.2 Родительские методы в экземпляре дочернего класса: super в методах

Для того, чтобы получить доступ к методу родительского класса внутри метода дочернего, вы должны использовать специальную ссылку на экземпляр родительского класса: super.

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

class ContentWriter extends User {
  posts = [];

  constructor(name, posts) {
    super(name);
    this.posts = posts;
  }

  getName() {
    const name = super.getName();    
      if (name === '') {
      return 'Unknwon';
    }
    return name;
  }
}

const writer = new ContentWriter('', ['Why I like JS']);
writer.getName(); // => 'Unknwon'

Как видно из примера выше, метод getName() дочернего класса ContentWriter обращается к методу super.getName(), который был реализован в родительского классе User. То есть в дочернем классе мы заменяем родительский метод getName() на свой с тем же именем. Этот механизм объектно-ориентированного программирования называется переопределением метода .

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

6. Проверка типа объекта: instanceof

Инструкция вида object instanceof Class определяет, является ли object экземпляром Class.

Давайте посмотрим на оператор instanceof в действии:

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

const user = new User('Jon Snow');
const obj = {};

user instanceof User; // => true
obj instanceof User; // => false

Как видим, объект user является экземпляром класса User, поэтому результат выполнения инструкции user instanceof User определяется как true.

Пустой объект {} не является экземпляром User, соответственно , obj instanceof User возвращает false.

Оператор instanceof является полиморфным: он определяет экземпляр дочернего класса как экземпляр родительского.

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

class ContentWriter extends User {
  posts = [];

  constructor(name, posts) {
    super(name);
    this.posts = posts;
  }
}

const writer = new ContentWriter('John Smith', ['Why I like JS']);

writer instanceof ContentWriter; // => true
writer instanceof User; // => true

writer является экземпляром дочернего класса ContentWriter. Поэтому инструкция writer instanceof ContentWriter возвращает true.

В то же время ContentWriter это дочерний класс User. Поэтому writer instanceof User также возвращает true.

Как же быть если вы хотите определить точно экземпляром какого класса является ваш объект? Для этого вы можете использовать свойство constructor вашего экземпляра класса, значение которого вы сравнивается с именем класса:

writer.constructor === ContentWriter; // => true
writer.constructor === User; // => false

7. Классы и прототипы

Как мы могли уже убедиться, синтаксис класса в JavaScript отлично справляется с абстрагированием от прототипного наследования. Для описания синтаксиса определения классов с использованием ключевого слова class я даже не использовал термин прототип.

Но тем не менее, как нам известно, классы в Javascript все таки построены на основе прототипного наследования. То есть каждый класс по сути является функцией и создает его экземпляр при вызове в качестве конструктора.

Следующие два фрагмента кода эквивалентны.

Версия класса с использованием ключевого слова class:

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

  getName() {
    return this.name;
  }
}

const user = new User('John');

user.getName(); // => 'John Snow'
user instanceof User; // => true

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

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

User.prototype.getName = function() {
  return this.name;
}

const user = new User('John');

user.getName(); // => 'John Snow'
user instanceof User; // => true

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

8. Доступность использования возможностей классов

Новые возможности использования классов, представленные в этом посте, отражены в стандарте ES2015 и предложениям, закладываемым в него на 3 этапе.

В конце 2019 года все, рассмотренные нами в посте, функциональные возможности классов представлены:

  • Публичные и приватные поля экземпляра класса являются частью Class fields proposal;
  • Приватные методы экземпляра, а также средства доступа (геттеры и сеттеры) являются частью Class private methods proposal;
  • Публичные и приватные статические поля, а также приватные статические методы являются частью Class static features proposal;
  • Все остальное является частью стандарта ES2015.

9. Выводы

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

Механизм наследования реализуется с использованием ключевого слова extends, с помощью которого вы можете легко создать дочерний класс от родительского. Ключевое слово super используется для доступа к полям и методам родительского класса из дочернего.

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

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

А что вы думаете об использовании префикса # при определении приватных свойств классов?

Всем привет!  Объекты очень важная вещь в программировании, которая может облегчить решения многих задач. Например нужно вести дневник пользователя: год рождения, имя, фамилия, местожительство, все это можно продолжать еще очень долго. Поэтому, как и другие языки программирования C++ обзавелся — классами.

  • Что такое класс
  • ООП в программировании
  • Доступы public и private
  • Карточка работника
  • Массив объектов 
  • Отделение методов от логики
  • Инициализация объектов с помощью указателей
  • Конструктор и деструктор

Как создать класс

Чтобы объявить класс нужно использовать данную конструкцию:

Обычно <имя класса> прописывают с заглавной буквы. Также в конце обязательно должна присутствовать точка с запятой (;).

Что такое класс

Это абстрактный тип данных. Он сочетает в себе два функционала:

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

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

класс примеры c объектами

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

Так свойства класса Worker (рабочий) может иметь — имя, производительность (полезность работы) за 6 месяцев, среднюю производительность.

class Worker {

  public:  // об этом ниже

    string name; // имя

    // производительность в течении 6 месяцев

    int academic_performance[6];

    // средняя производительность

    int avarage_AP;

};

Методы — это обычные функции, в функционале которых можно использовать свойства.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

class Worker {

  public:  // об этом ниже

    // функция для вычисления средней производительности

    void discover_avarage_AP () {

      double answer = 0;  

      for (int i = 0; i < 6; i++) {

        answer += academic_performance[i];

      }

      avarage_AP = answer / 6; // вычисляем с.п.

    }

    string name; // имя

    // производительность в течении 6 месяцев

    int academic_performance[6];

    // средняя успеваемость

    int avarage_AP;

};

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

<имя класса>.<название свойства или метода>;

Что такое ООП

Раньше программистам приходилось весь функционал программы записывать в одном файле. Что в будущем неизбежно приводило к путанице из-за нескольких сотен и даже тысяч строк. А с приходом классов появилась возможность отделять любую часть программы в отдельный файл.

Например один файл отвечает за инициализацию введенных данных, другой за считывание производительности. Таким образом стала возможным структурировать программу.

В ООП входит такие свойства:

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

    SquarePlis() {

    square = a * a;  // для квадрата

    square = 1 / 2 * h * a;  // для треугольника

    square = 3.14 * r * r;  // для круга

    }

  • Абстракция — это возможность выбирать только те свойства или функции, которые нам необходимы. Например, при создании класса про работника понадобится указать его имя, возраст, образование, но никак его цвет волос, глаз, рост и тому подобное.

Спецификаторы доступа public и private

Для разграничение содержимого класса, например которое пользователю лучше не трогать, были добавлены спецификаторы доступа public, private и protected (о нем пойдет речь в следующем уроке про наследование). Это и есть инкапсуляция, которую мы упоминали выше.

  • public — дает публичный доступ, содержимому, которое в нем указано. Так можно обратится к любой переменной или функции из любой части программы.
  • private — запрещает обращаться к свойствам вне класса. Поэтому под крылом этого доступа часто находятся именно объявления переменных, массивов, а также прототипов функций.Оперировать его содержимым можно только из методов класса. Получается все преобразования: добавление, вычитание нужно будет делать в функции (их еще называют set и get функциями).

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

class Worker {

  public:

    void discover_avarage_AP () {

      // …

      set_avarage_AP(answer);

      // вместо avarage_AP = answer / 6;

    }

    void set_avarage_AP (double score) {

      avarage_AP = score / 6;

    }

    double get_avarage_AP () {

      return avarage_AP;

    }

  private:

    string name; // имя

    // успеваемость за 6 месяцев

    int academic_performance[6];

    // средняя успеваемость

    int avarage_AP;

};

  • В строке 5: используем функцию set_avarage_AP() для установки нового значения avarage_AP.
  • В строке 13: находится функция get_avarage_AP(), которая передает значения avarage_AP.

Если отсутствуют права доступа, то по умолчанию будет стоять доступ private.

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

Функции get и set классов

При создании класса обычно создают функции в названии которых присутствуют слова set и get.

  • set — в ней инициализируют свойства класса.

    set_number() {    // set и имя переменной

    cin >> number;  // которую инициализируют

    }

    private:

      int number;  // наша переменная

  • get — выводит свойства конечному пользователю.

    get_number() {    // get и переменная

    return number;  // которую возвращают

    }

    private:

      int number;  // наша переменная

Пример использования классов

Давайте используем созданный класс на практике создав карточку об одном работнике, например Иване. Класс разместим в файле workers.h, который подключим к главному файлу main.cpp таким образом #include "workers.h".

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

// workers.h

#include <string>

using namespace std;

class Worker {

  public:

    void discover_avarage_AP () {

      double answer = 0;

      for (int i = 0; i < 6; i++) {

        answer += academic_performance[i];

      }

      set_avarage_AP(answer);

      // вместо avarage_AP = answer / 6;

    }

    void set_avarage_AP (double score) {

      avarage_AP = score / 6;

    }

    // здесь находятся set и get функции

    double get_avarage_AP () {

      return avarage_AP;

    }

    void set_name(string a) {

      // считываем имя

      name = a;

    }

    void  set_academic_performance (vector  v) {

      // заполняем 6 месячныю успеваемость

      for (int i = 0; i < 6; i++) { academic_performance[i] = v[i];

      }

    }

    string get_name () {

      // выводим имя

      return name;

    }

    // конец set и get функций

  private:

    // средняя успеваемость

    int avarage_AP;

    string name; // имя

    // успеваемость за 6 месяцев

    int academic_performance[6];

};

В строках 19-34: находятся set и get функции для инициализации наших свойств. Вот какие именно:

  • get_name() — считывает имя работника.
  • get_academic_perfomence() — считывает успеваемость на работе за шесть месяцев.

Функции set имеют такое же название, только вместо get — set.

А вот как выглядит main.cpp

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

// main.cpp

#include <iostream>

#include <vector>

#include «workers.h»

using namespace std;

int main() {

  Worker employee;

  string name;

  vector <int> average_balls;

  cout << «Your name: «; cin >> name;

  cout << «Your academic performance for 6 months: « << endl;

  for (int i = 0; i < 6; i++) {

    int one_score;

    cout << i + 1 << «) «; cin >> one_score;

    average_balls.push_back(one_score);

  }

  employee.set_name(name);

  employee.set_academic_performance(average_balls);

  employee.discover_avarage_AP();

  cout << endl << employee.get_name() << endl;

  cout << «Avarage academic performance: « << employee.get_avarage_AP() << endl;

  return 0;

}

Для создания объекта employee мы указали класс Worker.

  • В строках 14 — 21: считываем пользовательские данные.
  • В строках 23- 24: отсылаем полученные данные классу (функциями set).
  • Вычисляем среднюю успеваемость вызвав функцию discover_avarage_AP() в строке 26.
  • В строках 28 — 29: выводим все свойства: имя, фамилию, возраст, средний балл.

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

Your name: Иван

Your academic performance for 6 months:

1) 3

2) 4

3) 5

4) 5

5) 3

6) 4

Иван

Avarage academic performance: 4

Process returned 0 (0x0) execution time : 0.010 s

Press any key to continue.

Как создать массив объектов

Кроме создания единичного объекта класса можно создать целый массив объектов. Нужно всего лишь в конец создаваемого объекта добавить его количество ([n]).

Worker employee[5];

cout << «Name for second employee : «; cin >> name;

employee[2].set_name(name);

Здесь мы просим пользователя ввести имя для второго работника. Далее вводим введенное имя в объект employee[2] с помощью функции set_name().

 Вынесение методов от логики

Давайте отделим реализацию всех методов отдельный файл.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

// main.cpp

#include <iostream>

#include «Worker.h»

void Worker::discover_avarage_AP () {

  double answer = 0;

  for (int i = 0; i < 6; i++) {

    answer += academic_performance[i];

  }

  set_avarage_AP(answer);

}

void Worker::set_avarage_AP (double score) {

  avarage_AP = score / 6;

}

// set — get функции

double Worker::get_avarage_AP () {

  return avarage_AP;

}

void Worker::set_name(string a) {

  // считываем имя

  name = a;

}

void Worker::set_academic_performance (vector v) {

  // заполняем 6 месячныю успеваемость

  for (int i = 0; i < 6; i++) {

      academic_performance[i] = v[i];

  }

}

string Worker::get_name () {

  // выводим имя

  return name;

}

А вот файл с логикой класса.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

class Worker {

  public:

    // высчитывание среднего балла

    void discover_avarage_AP ();

    void set_avarage_AP (double score);

    // вывод средней успеваемости

    double get_avarage_AP ();

    // получение и вывод имени

    void set_name(string a);

    string get_name ();

    // получение баллов за шесть месяцев

    void set_academic_performance (vector v);

  private:

    // средняя успеваемость

    int avarage_AP;

    string name; // имя

    // успеваемость за 6 месяцев

    int academic_performance[6];

};

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

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

Поэтому перед каждой функцией стоит данный префикс.

Инициализация объектов с помощью указателей

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

Чтобы создать объект с помощью указателя необходимо использовать конструкцию ниже:

<название класса> *<имя объекта> = new <название класса>

Для освобождения памяти используется оператор delete:

delete <название объекта>;

При обращении к свойствам и методам объекта применяется такая конструкция (->):

<имя объекта>-><название свойства или метода>

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

// main.cpp

#include <iostream>

#include <vector>

#include workers.h

using namespace std;

int main() {

  Worker *employee = new Worker;

  string name;

  vector <int> average_balls;

  cout << «Your name: «; cin >> name;

  cout << «Your academic performance for 6 months: « << endl;

  for (int i = 0; i < 6; i++) {

    int one_score;

    cout << i + 1 << «) «; cin >> one_score;

    average_balls.push_back(one_score);

  }

  employee->set_name(name);

  employee->set_academic_performance(average_balls);

  employee->discover_avarage_AP();

  cout << endl << employee->get_name() << endl;

  cout << «Avarage academic performance: « << employee->get_avarage_AP() ;

  return 0;

}

Конструктор и деструктор класса

Конструктор — это метод, который вызывается во время создания класса. Также он имеет несколько условий для существования:

  1. Он должен называться также, как класс.
  2. У него не должно быть типа функции (bool, void …).

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

class Worker {

    public:

        // Конструктор для класса Worker

        Worker (string my_name_is, string my_last_name_is)

        {

            name = string my_name_is;

            last_name = my_last_name_is;

        }

    private:

        string name;

        string last_name;

};

int main()

{

    Worker employee («Ваня», «Шарапов»);

    // вот как будет выглядеть, если мы создаем через указатель

    Worker *employee_CocaCola = new Worker(«Дмитрий»,«Талтонов»);

    return 0;

}

Деструктор — тоже функция, только уже вызывается после удаления класса. Кроме условий, которые имеет конструктор, деструктор еще должен начинаться с тильды (~).

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

class Workers {

    public:

        // Деструктор класса Workers

        ~Workers()

        {

            std::cout << «I’m sorry, my creator(« << std::endl;

        }

};

int main()

{

    Workers *employee = new Workers;

    Worker employee_CocaCola;

    // Удаление объекта

    delete student;  // после этого сработает деструктор employee

    return 0;

}

// А вот где сработает деструктор employee_CocaCola

Классы в C++

Пожалуйста, подождите пока страница загрузится полностью.
Если эта надпись не исчезает долгое время, попробуйте обновить страницу. Этот тест использует javascript. Пожалуйста, влкючите javascript в вашем браузере.

If loading fails, click here to try again

Пройдите тест и проверьте уровень усвоения материала. Напишите в комментарии сколько вы набрали очков, посмотрим кто лучший!

Оригинальная статья: Dmitri Pavlutin – The Complete Guide to JavaScript Classes

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

Но через прототипное наследования можно эмулировать классическое наследование классов. Чтобы привести традиционные классы в JavaScript, в стандарте ES2015 было введено классовый синтаксис, то есть появилось ключевое слово class: которое является синтаксическим сахаром над прототипным наследованием.

Этот пост знакомит вас с классами в JavaScript: как определить класс, инициализировать экземпляр, определить поля и методы, рассматриваются такие понятия как приватные поля, публичные поля, статические поля и методы.

Содержание

  • 1. Определение: ключевое слово class
  • 2. Инициализация: constructor()
  • 3. Поля
    • 3.1 Публичные поля экземпляра
    • 3.2 Приватные поля экземпляра
    • 3.3 Публичные статические поля
    • 3.4 Приватные статические поля
  • 4. Методы
    • 4.1 Методы экземпляра
    • 4.2 Getters и setters
    • 4.3 Статические методы
  • 5. Наследование: extends
    • 5.1 Родительский конструктор: super() в constructor()
    • 5.2 Экземпляр родителя: super в методах
  • 6. Проверка типа объекта: instanceof
  • 7. Классы и прототипы
  • 8. Доступность возможностей классов
  • 9. Заключение

1. Определение: ключевое слово class

Специальное ключевое слово class определяет класс в JavaScript:

class User {
  // The body of class
}

Приведенный выше код определяет класс User. Фигурные скобки {} определяют тело класса. Обратите внимание, что такой синтаксис называется объявлением класса.

Вы не обязаны указывать название класса. Используя выражение класса, вы можете назначить класс переменной:

const UserClass = class {
  // The body of class
};

Вы можете легко экспортировать класс как часть модуля ES2015.

Вот синтаксис для экспорта по умолчанию:

export default class User {
 // The body of class
}

И именной экспорт:

export class User {
  // The body of class
}

Класс становится полезным, когда вы создаете экземпляр класса. Экземпляр – это объект, содержащий данные и поведение, описанные классом.

Оператор new создает экземпляр класса в JavaScript таким образом: instance = new Class().

Например, вы можете создать экземпляр класса User с помощью оператора new:

const myUser = new User();

new User() создает экземпляр класса User.

2. Инициализация: constructor()

constructor(param1, param2, …) это специальный метод в теле класса, который инициализирует экземпляр. Это место, где вы можете установить начальные значения для полей или выполнить любые настройки объектов.

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

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

constructor класса User использует один параметр name, который используется для установки начального значения поля this.name.

Внутри конструктора значение this равно вновь созданному экземпляру.

Аргументы, используемые для создания экземпляра класса, становятся параметрами конструктора:

class User {
  constructor(name) {
    name; // => 'Jon Snow'
    this.name = name;
  }
}

const user = new User('Jon Snow');

Параметр name внутри конструктора имеет значение ‘Jon Snow’.

Если вы не определяете конструктор для класса, создается конструктор по умолчанию. Конструктор по умолчанию является пустой функцией, которая не изменяет экземпляр.

В то же время класс JavaScript может иметь до одного конструктора.

3. Поля

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

  1. Поля на экземпляре класса
  2. Поля на самом классе (в этом случае он называется статическим полем)

Поля также имеют 2 уровня доступности:

  1. Публичное: поле становиться везде доступным
  2. Приватное: поле доступно только внутри тела класса

3.1 Публичные поля экземпляра

Давайте снова посмотрим на предыдущий фрагмент кода:

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

Выражение this.name = name создает поля name экземпляра и присваивает ему начальное значение.

Позже вы можете получить доступ к полю name с помощью метода доступа к свойству:

const user = new User('Jon Snow');
user.name; // => 'Jon Snow'

name является публичным полем, поэтому вы можете получить к нему доступ вне тела класса User.

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

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

Предложение (proposal) TC39 о полях класса позволяет определять поля внутри тела класса. Кроме того, вы можете сразу указать начальное значение:

class SomeClass {
  field1;
  field2 = 'Initial value';
  // ...
}

Давайте изменим класс User и объявим публичное поле name:

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

const user = new User('Jon Snow');
user.name; // => 'Jon Snow'

name внутри тела класса объявляется как публичное поле.

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

Более того, поле класса может быть инициализировано сразу при объявлении.

class User {
  name = 'Unknown';
  constructor() {
    // No initialization
  }
}

const user = new User();
user.name; // => 'Unknown'

name = ‘Unknown’ внутри тела класса объявляет поля name и инициализирует его значением ‘Unknown’.

3.2 Приватные поля экземпляра

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

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

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

Приватные поля доступны только внутри тела класса.

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

Давайте удостоверимся, что поле #name будет приватным:

class User {
  #name;
  constructor(name) {
    this.#name = name;
  }

  getName() {
    return this.#name;
  }
}

const user = new User('Jon Snow');
user.getName(); // => 'Jon Snow'

user.#name;     // SyntaxError is thrown

Теперь #name это приватное поле. Вы можете получить доступ и изменить #name только в теле класса User. Метод getName() (подробнее о методах в следующем разделе) может получить доступ к закрытому полю #name.

Но если вы пытаетесь получить доступ к закрытому полю #name вне тела класса User, возникает синтаксическая ошибка: SyntaxError: Private field ‘#name’ must be declared in an enclosing class.

3.3 Публичные статические поля

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

Чтобы создать статические поля в классе JavaScript, используйте специальное ключевое слово static, за которым следует имя поля, например: static myStaticField.

Давайте добавим новый тип поля, который указывает тип пользователя: admin или regular. Статические поля TYPE_ADMIN и TYPE_REGULAR являются удобными константами для различения пользовательских типов:

class User {
  static TYPE_ADMIN = 'admin';
  static TYPE_REGULAR = 'regular';
  name;
  type;

  constructor(name, type) {
    this.name = name;
    this.type = type;
  }
}

const admin = new User('Site Admin', User.TYPE_ADMIN);
admin.type === User.TYPE_ADMIN; // => true

static TYPE_ADMIN и static TYPE_REGULAR определяют статические переменные внутри класса User. Чтобы получить доступ к статическим полям, вы должны использовать класс, за которым следует имя поля: User.TYPE_ADMIN и User.TYPE_REGULAR.

3.4 Приватные статические поля

Иногда даже статические поля – это детали реализации, которые вы хотели бы скрыть. В связи с этим вы можете сделать статические поля приватными.

Чтобы сделать статическое поле приватным, добавьте к имени поля специальный символ #, например: static #myPrivateStaticField.

Допустим, вы хотите ограничить количество экземпляров класса User. Чтобы скрыть подробности об ограничениях экземпляров, вы можете создать приватное статические поле:

class User {
  static #MAX_INSTANCES = 2;
  static #instances = 0;  
  name;

  constructor(name) {
    User.#instances++;
    if (User.#instances > User.#MAX_INSTANCES) {
      throw new Error('Unable to create User instance');
    }
    this.name = name;
  }
}

new User('Jon Snow');
new User('Arya Stark');
new User('Sansa Stark'); // throws Error

Статическое поле User.#MAX_INSTANCES устанавливает максимальное количество разрешенных экземпляров, в то время как статическое поле User.#instances подсчитывает фактическое количество экземпляров.

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

4. Методы

Поля содержат данные. Но возможность изменять данные выполняют специальные функции, которые являются частью класса: методы.

Классы JavaScript поддерживают как экземпляры, так и статические методы.

4.1 Методы экземпляра

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

Например, давайте определим метод getName(), который возвращает имя в классе User:

class User {
  name = 'Unknown';

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

const user = new User('Jon Snow');
user.getName(); // => 'Jon Snow'

getName() {…} – это метод внутри класса User. А user.getName() – это вызов метода: он выполняет метод и возвращает вычисленное значение, если оно есть.

В методе класса, как и в конструкторе, значение this равно экземпляру класса. Используйте this для доступа к данным экземпляра, например: this.field или даже для вызова других методов, например: this.method().

Давайте добавим новый метод nameContains(str), который имеет один параметр и вызывает другой метод:

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }

  nameContains(str) {
    return this.getName().includes(str);
  }
}

const user = new User('Jon Snow');
user.nameContains('Jon');   // => true
user.nameContains('Stark'); // => false

nameContains(str) {…} – это метод класса User, который принимает один параметр str. Более того, он выполняет другой метод экземпляра this.getName(), чтобы получить имя пользователя.

Метод также может быть закрытым. Чтобы сделать метод приватным, добавьте к его имени префикс #.

Давайте сделаем метод getName() приватным:

class User {
  #name;

  constructor(name) {
    this.#name = name;
  }

  #getName() {
    return this.#name;
  }
  nameContains(str) {
    return this.#getName().includes(str);
  }
}

const user = new User('Jon Snow');
user.nameContains('Jon');   // => true
user.nameContains('Stark'); // => false

user.#getName(); // SyntaxError is thrown

#getName() является приватным методом. Внутри метода nameContains(str) вызывается приватный метод this.#getName().

Будучи приватным, #getName() не может быть вызван вне тела класса User.

4.2 Getters и setters

getter и setter имитируют обычное поле, но с большим контролем над тем, как поле доступно.

getter выполняется при попытке получить значение поля, а setter при попытке установить значение.

Чтобы убедиться, что свойство name пользователя не может быть пустым, давайте обернем приватное поле #nameValue в методы getter и setter:

class User {
  #nameValue;

  constructor(name) {
    this.name = name;
  }

  get name() {    
    return this.#nameValue;
  }

  set name(name) {    
    if (name === '') {
      throw new Error(`name field of User cannot be empty`);
    }
    this.#nameValue = name;
  }
}

const user = new User('Jon Snow');
user.name; // The getter is invoked, => 'Jon Snow'
user.name = 'Jon White'; // The setter is invoked

user.name = ''; // The setter throws an Error

get name () {…} getter выполняется при доступе к значению поля user.name.

В то время как set name(name) {…}  выполняется при обновлении поля user.name = ‘Jon White’. setter выдаст ошибку, если новое значение будет пустой строкой.

4.3 Статические методы

Статические методы – это функции, прикрепленные непосредственно к классу. Они содержат логику, связанную с классом, а не с экземпляром класса.

Для создания статического метода используйте специальное ключевое слово static, за которым следует обычный синтаксис метода: static myStaticMethod () {…}.

При работе со статическими методами нужно помнить 2 простых правила:

  1. Статический метод может получить доступ к статическим полям
  2. Статический метод не может получить доступ к полям экземпляра.

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

class User {
  static #takenNames = [];

  static isNameTaken(name) {    
    return User.#takenNames.includes(name);  
  }
  name = 'Unknown';

  constructor(name) {
    this.name = name;
    User.#takenNames.push(name);
  }
}

const user = new User('Jon Snow');

User.isNameTaken('Jon Snow');   // => true
User.isNameTaken('Arya Stark'); // => false

isNameTaken() – это статический метод, который использует статическое приватное поле User.#takeNames для проверки принятого значения name.

Статические методы могут быть приватными: static #staticFunction() {…}. Опять же, они следуют правилам конфиденциальности: вы можете вызывать приватный статический метод только внутри тела класса.

5. Наследование: extends

Классы в JavaScript поддерживают одиночное наследование с использованием ключевого слова extends.

Выражение вида class Child extends Parent { }, означает что класс Child наследует от класса Parent его конструктор, поля и методы.

Например, давайте создадим новый дочерний класс ContentWriter, который расширяет родительский класс User.

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

class ContentWriter extends User {
  posts = [];
}

const writer = new ContentWriter('John Smith');

writer.name;      // => 'John Smith'
writer.getName(); // => 'John Smith'
writer.posts;     // => []

ContentWriter наследует от пользователя конструктор, метод getName() и поле name. Также, класс ContentWriter объявляет новое поле posts.

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

5.1 Родительский конструктор: super() в constructor()

Если вы хотите вызвать родительский конструктор в дочернем классе, вам нужно использовать специальную функцию super(), доступную в дочернем конструкторе.

Например, давайте сделаем так, чтобы конструктор ContentWriter вызывал родительский конструктор User, а также инициализировал поле posts:

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

class ContentWriter extends User {
  posts = [];

  constructor(name, posts) {
    super(name);
    this.posts = posts;
  }
}

const writer = new ContentWriter('John Smith', ['Why I like JS']);
writer.name; // => 'John Smith'
writer.posts // => ['Why I like JS']

super(name) внутри дочернего класса ContentWriter выполняет конструктор родительского класса User.

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

class Child extends Parent {
  constructor(value1, value2) {
    // Does not work!
    this.prop2 = value2;    
    super(value1);  
  }
}

5.2 Экземпляр родителя: super в методах

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

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

class ContentWriter extends User {
  posts = [];

  constructor(name, posts) {
    super(name);
    this.posts = posts;
  }

  getName() {
    const name = super.getName();    
    if (name === '') {
      return 'Unknown';
    }
    return name;
  }
}

const writer = new ContentWriter('', ['Why I like JS']);
writer.getName(); // => 'Unknown'

getName() дочернего класса ContentWriter обращается к методу super.getName() напрямую из родительского класса User.

Эта функция называется переопределением (overriding) метода.

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

6. Проверка типа объекта: instanceof

object instanceof Class – оператор, который определяет, является ли object экземпляром Class.

Давайте посмотрим оператора instanceof в действии:

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

const user = new User('Jon Snow');
const obj = {};

user instanceof User; // => true
obj instanceof User; // => false

user является экземпляром класса User, поэтому user instanceof User оценивается как true.

Пустой объект {} не является экземпляром User, соответственно obj instanceof User равен false.

instanceof является полиморфным: оператор обнаруживает дочерний элемент как экземпляр родительского класса.

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

class ContentWriter extends User {
  posts = [];

  constructor(name, posts) {
    super(name);
    this.posts = posts;
  }
}

const writer = new ContentWriter('John Smith', ['Why I like JS']);

writer instanceof ContentWriter; // => true
writer instanceof User;          // => true

Writer является экземпляром дочернего класса ContentWriter. Оператор writer instanceof ContentWriter оценивается как true.

В то же время ContentWriter является дочерним классом User. Таким образом, writer instanceof User также оценивает как true.

Что если вы хотите определить точный класс экземпляра? Вы можете использовать свойство constructor и сравнить его непосредственно с классом:

writer.constructor === ContentWriter; // => true
writer.constructor === User;          // => false

7. Классы и прототипы

Я должен сказать, что синтаксис класса в JavaScript отлично справляется с абстрагированием от прототипного наследования. Для описания синтаксиса class я даже не использовал термин prototype.

Но внутри классы построены на основе прототипного наследования. Каждый класс является функцией и создает экземпляр при вызове в качестве конструктора.

Следующие два фрагмента кода эквивалентны.

Версия с классом:

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

  getName() {
    return this.name;
  }
}

const user = new User('John');

user.getName();       // => 'John Snow'
user instanceof User; // => true

Версия с прототипом:

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

User.prototype.getName = function() {
  return this.name;
}

const user = new User('John');

user.getName();       // => 'John Snow'
user instanceof User; // => true

Синтаксис класса намного проще в работе, если вы знакомы с классическим механизмом наследования языков Java или Swift.

В любом случае, даже если вы используете синтаксис класса в JavaScript, я рекомендую вам хорошо разбираться с прототипным наследованием (prototypal inheritance).

8. Наличие возможностей класса

Свойство классов, рассмотрены в этом посте, были внедрены в ES2015 а так же новыми предложениям (proposals) находящимися на этапе 3.

В конце 2019 года функции класса разделяются на:

  • Публичные и приватные поля экземпляра являются частью предложения TC39 о полях класса Class fields proposal
  • Приватные методы экземпляра и инструменты доступа являются частью предложения TC39 о приватных методов класса Class private methods proposal
  • Публичные и приватные статические поля и приватные статические методы являются частью предложения TC39 о статических функций класса Class static features proposal
  • Остальное является частью стандарта ES2015.

9. Заключение

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

Наследование реализуется с помощью ключевого слова extends: вы можете легко создать дочерний класс из родительского. Ключевое слово super используется для доступа к родительскому классу из дочернего класса.

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

Классы в JavaScript становятся все более удобными в использовании.

Была ли вам полезна эта статья?

Понравилась статья? Поделить с друзьями:
  • Инструкция по от для бухгалтера на предприятии
  • Руководство по ремонту комацу рс 300
  • Лекарственный препарат вобэнзим инструкция по применению
  • К службам руководство деятельностью которых осуществляет президент российской федерации относятся
  • Кавинтон ампулы инструкция по применению цена отзывы аналоги