Понимание классов в JavaScript

Вступление

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

До недавнего времени трудолюбивые разработчики использовалиconstructor functions для имитации объектно-ориентированного шаблона проектирования в 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() method. Давайте воспользуемся этим для проверки созданного нами пустогоfunction.

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

Мы также можем использовать этот метод для только что созданныхclass.

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

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

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

Вprototypes and inheritance tutorial мы создали пример, основанный на создании персонажа в текстовой ролевой игре. Давайте продолжим с этим примером здесь, чтобы обновить синтаксис от функций к классам.

constructor function инициализируется рядом параметров, которые будут назначены как свойстваthis, относящиеся к самой функции. Первая буква идентификатора будет прописной.

constructor.js

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

Когда мы переводим это в синтаксисclass, показанный ниже, мы видим, что он имеет очень похожую структуру.

class.js

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

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

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

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

Обычной практикой с функциями конструктора является присвоение методов непосредственноprototype вместо инициализации, как показано в методеgreet() ниже.

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.`;
}

С классами этот синтаксис упрощен, и метод может быть добавлен непосредственно в класс. Использованиеmethod definition shorthand, введенного в ES6, делает определение метода еще более кратким.

class.js

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(), а также добавим дополнительное свойство.

constructor.js

// 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 для ссылки на родительский класс.

class.js

// 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.

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

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, часто используют синтаксисclass.