Tutorial

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

DevelopmentJavaScript

Введение

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);
Output
x {} constructor: ƒ ()

Это также относится и к классам.

const y = class {}

// Initialize a constructor from a class
const constructorFromClass = new y();

console.log(constructorFromClass);
Output
y {} constructor: class

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

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

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

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

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

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

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

При использовании классов этот синтаксис упрощается, и метод можно добавить напрямую в класс. Благодаря концепции заблаговременного определения методов, введенной в 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), мы более подробно увидим. что происходит при инициализации класса.

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

В результатах мы видим, что функции constructor() и greet() functions были применены к прототипу __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 на базе конструктора.

Output
Mage {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 на консоли и посмотрим результат.

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

Creative Commons License