Работа с классами в JavaScript

JavaScript – это основанный на прототипах язык, и каждый объект в JavaScript имеет скрытое внутреннее свойство [[Prototype]], которое может использоваться для расширения свойств и методов объекта.

Читайте также: Прототипы и наследование в JavaScript

До недавнего времени разработчики использовали функции-конструкторы для имитации объектно-ориентированного шаблона проектирования в JavaScript. Спецификация языка ECMAScript 2015, часто называемая ES6, ввела в JavaScript классы. Классы в JavaScript на самом деле не предоставляют дополнительных функций, но предлагают более чистый и понятный синтаксис, чем прототипы и наследование.

Класс как функция

Класс JavaScript – это такой тип функции. Классы объявляются ключевым словом class. Для инициализации функции используется синтаксис выражения функции, а для инициализации класса — синтаксис выражения класса.

// Initializing a function with a function expression
const x = function() {}
// Initializing a class with a class expression
const y = class {}

Получить доступ к [[Prototype]] объекта можно с помощью метода Object.getPrototypeOf(). Используйте его, чтобы протестировать новую пустую функцию.

Object.getPrototypeOf(x);
ƒ () { [native code] }

Также можно применить этот метод на новый класс:

Object.getPrototypeOf(y);
ƒ () { [native code] }

Код, объявленный ключевыми словами function и class, возвращает функцию [[Prototype]]. Благодаря прототипам любая функция может стать экземпляром конструктора с помощью ключевого слова new.

const x = function() {}
// Initialize a constructor from a function
const constructorFromFunction = new x();
console.log(constructorFromFunction);
x {}
constructor: ƒ ()

Это также применимо к классам:

const y = class {}
// Initialize a constructor from a class
const constructorFromClass = new y();
console.log(constructorFromClass);
y {}
constructor: class

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

Определение классов

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

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

// Initializing a constructor function
function Hero(name, level) {
this.name = name;
this.level = level;
}

Если перевести это в синтаксис класса, получится:

// Initializing a class definition
class Hero {
constructor(name, level) {
this.name = name;
this.level = level;
}
}

Единственная разница в синтаксисе инициализации заключается в использовании ключевого слова class вместо function и назначении свойств внутри метода constructor().

Определение методов

В функциях-конструкторах методы чаще не инициализируются, а присваиваются непосредственно прототипу. Это показано в методе greet() ниже.

function Hero(name, level) {
this.name = name;
this.level = level;
}
// Adding a method to the constructor
Hero.prototype.greet = function() {
return `${this.name} says hello.`;
}

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

class Hero {
constructor(name, level) {
this.name = name;
this.level = level;
}
// Adding a method to the constructor
greet() {
return `${this.name} says hello.`;
}
}

Давайте посмотрим на эти свойства и методы в действии. Для этого создайте новый экземпляр Hero, используя ключевое слово new, и присвойте ему значение.

const hero1 = new Hero('Varg', 1);

Если запросить дополнительную информацию о новом объекте с помощью console.log(hero1), вы увидите более подробные данные о том, что происходит с инициализацией класса.

Hero {name: "Varg", level: 1}
__proto__:
▶ constructor: class Hero
▶ greet: ƒ greet()

На выходе можно видеть, что функции constructor() и greet() были применены к __proto__ (или [[Prototype]]) hero1, а не непосредственно как метод объекта hero1. Это понятно при создании функций-конструкторов, но это не очевидно при создании классов. Классы предлагают более простой и сжатый синтаксис, но жертвуют при этом понятностью кода.

Расширение классов

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

Новые функции-конструкторы можно создать на основе родительской функции с помощью метода call(). В приведенном ниже примере мы создадим более специфический класс персонажей Mage и присвоим ему свойства Hero с помощью call(), а также добавим дополнительное свойство.

// Creating a new constructor from the parent
function Mage(name, level, spell) {
// Chain constructor with call
Hero.call(this, name, level);
this.spell = spell;
}

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

Отправляя hero2 на консоль, вы увидите, что новый персонаж Mage основан на конструкторе.

Mage {name: "Lejon", level: 2, spell: "Magic Missile"}
__proto__:
▶ constructor: ƒ Mage(name, level, spell)

В классах ES6 для доступа к родительским функциям вместо call используется ключевое слово super. Используйте extends для обращения к родительскому классу.

// Creating a new class from the parent
class Mage extends Hero {
constructor(name, level, spell) {
// Chain constructor with super
super(name, level);
// Add a new property
this.spell = spell;
}
}

Теперь создайте нового персонажа Mage тем же образом.

const hero2 = new Mage('Lejon', 2, 'Magic Missile');

Выведите hero2 на консоль и просмотрите вывод:

Mage {name: "Lejon", level: 2, spell: "Magic Missile"}
__proto__: Hero
▶ constructor: class Mage

Результат почти точно такой же, за исключением того, что в построении класса [[Prototype]] связан с родителем, в данном случае с Hero.

Ниже приведено сравнение всего процесса инициализации, добавления методов и наследования функции-конструктора и класса.

// файл constructor.js
function Hero(name, level) {
this.name = name;
this.level = level;
}
// Adding a method to the constructor
Hero.prototype.greet = function() {
return `${this.name} says hello.`;
}
// Creating a new constructor from the parent
function Mage(name, level, spell) {
// Chain constructor with call
Hero.call(this, name, level);
this.spell = spell;
}

// файл class.js
// Initializing a class
class Hero {
constructor(name, level) {
this.name = name;
this.level = level;
}
// Adding a method to the constructor
greet() {
return `${this.name} says hello.`;
}
}
// Creating a new class from the parent
class Mage extends Hero {
constructor(name, level, spell) {
// Chain constructor with super
super(name, level);
// Add a new property
this.spell = spell;
}
}

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

Заключение

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

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

Tags: ,