Понимание классов в JavaScript
Введение
JavaScript — это язык, основанный на прототипах, и каждый объект в JavaScript имеет скрытое внутреннее свойство, называемое [[Prototype]]
, которое можно использовать для расширения свойств и методов объекта. Вы можете узнать больше о прототипах в нашем учебнике «Понимание прототипов и наследования в JavaScript».
До недавнего времени трудолюбивые разработчики использовали функции конструктора для имитации объектно-ориентированного шаблона проектирования в JavaScript. Спецификация языка ECMAScript 2015, часто называемая ES6, ввела классы в язык JavaScript. Классы в 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);
Outputƒ () { [native code] }
Мы также можем использовать этот метод в только что созданном классе.
Object.getPrototypeOf(y);
Outputƒ () { [native code] }
Код, объявленный с помощью function
и class
, возвращает функцию [[Prototype]]
. С прототипами любая функция может стать экземпляром конструктора с помощью ключевого слова new
.
const x = function() {}
// Initialize a constructor from a function
const constructorFromFunction = new x();
console.log(constructorFromFunction);
Outputx {}
constructor: ƒ ()
Это относится и к классам.
const y = class {}
// Initialize a constructor from a class
const constructorFromClass = new y();
console.log(constructorFromClass);
Outputy {}
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
более прямо передает цель нашей функции.
Единственное отличие в синтаксисе инициализации заключается в использовании ключевого слова class
вместо function
и назначении свойств внутри метода constructor()
.
Определение методов
Обычная практика с функциями-конструкторами заключается в назначении методов непосредственно prototype
, а не при инициализации, как показано в методе 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)
, мы сможем увидеть больше подробностей о том, что происходит с инициализацией класса.
OutputHero {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
, а также добавленный нами новый экземпляр.
const hero2 = new Mage('Lejon', 2, 'Magic Missile');
Отправив hero2
в консоль, мы видим, что создали нового Mage
на основе конструктора.
OutputMage {name: "Lejon", level: 2, spell: "Magic Missile"}
__proto__:
▶ constructor: ƒ Mage(name, level, spell)
В классах ES6 ключевое слово super
используется вместо call
для доступа к родительским функциям. Мы будем использовать 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
в консоль и просмотрим вывод.
OutputMage {name: "Lejon", level: 2, spell: "Magic Missile"}
__proto__: Hero
▶ constructor: class Mage
Результат почти такой же, за исключением того, что в конструкции класса [[Prototype]]
связан с родителем, в данном случае Hero
.
Ниже представлено параллельное сравнение всего процесса инициализации, добавления методов и наследования функции-конструктора и класса.
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;
}
// 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, часто используют синтаксис class
.