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

Чтобы имитировать шаблон объектно-ориентированного проектирования в других языках, таких как Java и C#, изобретательные разработчики использовали функции-конструкторы. Это было необходимо до появления классов в спецификации языка ECMAScript 2015, обычно называемых ES6.

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

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

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

// Инициализация функции с помощью функционального выражения

const x = function() {}

// Инициализация класса выражением класса

const y = класс {}

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

Object.getPrototypeOf(x);

Вывод

f () { [собственный код]

Этот метод также можно использовать в только что созданном классе, как показано ниже:

Object.getPrototypeOf(y);

Вывод

f () { [собственный код]

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

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

Тот же сценарий применим и к классам.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

const warrior1 = new warrior('Varg', 1);

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

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

Это интуитивно понятный вывод, показывающий, что функции constructor() и greet() были применены к _proto_, или [[ Прототип]]объекта warrior1, а не непосредственно как метод объекта warrior1. Это очевидно при создании функций-конструкторов, но не при создании классов. Классы освобождают место для более простого и лаконичного синтаксиса, жертвуя ясностью процесса. Они являются обычным явлением в реальном коде, таком как приложения, размещенные на www.discountdomains.co.nz, которые используют объектно-ориентированную парадигму.

Если вы разработчик JavaScript и ищете домены премиум-класса по отличным ценам, не ищите дальше. Проверьте самые лучшие сделки сегодня.

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

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

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

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

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

const warrior2 = new Rage('Melon', 2, 'Magic Missile');

Просмотрев warrior2 через консоль, мы видим, что создали новый Rage на основе конструктора:

Output
Rage {name: "Melon", level: 2, spell: "Magic Missile"}
__proto__:
constructor: ƒ Rage(name, level, spell)

Классы в ES6 также позволяют использовать ключевое слово super вместо call() для доступа к родительским функциям. Мы будем использовать extends для ссылки на родительский класс.

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

Чтобы таким же образом создать новый экземпляр Rage,

const warrior2 = new Rage('Melon', 2, 'Magic Missile');

Вывод warrior2 на консоль и просмотр вывода, мы видим это:

Output
Rage {name: "Melon", level: 2, spell: "Magic Missile"}
__proto__: warrior
constructor: class Rage

Этот вывод практически такой же, как и предыдущий, за исключением того, что в конструкции класса [[Prototype]] связан с родителем, в данном случае воином. Ниже мы сравниваем полную последовательность инициализации, добавления методов и наследования от функции-конструктора и класс.

// Initializing a constructor function
warrior(name, level) {
    this.name = name;
    this.level = level;
}
// Adding a method to the constructor
warrior.prototype.greet = function() {
    return `${this.name} says hello.`;
}
// Creating a new constructor from the parent
function Rage(name, level, spell) {
    // Chain constructor with call
    warrior.call(this, name, level);
    this.spell = spell;
}
// Initializing a class
class warrior {
    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 Rage extends warrior {
    constructor(name, level, spell) {
        // Chain constructor with super
        super(name, level);
        // Add a new property
        this.spell = spell;
    }
}

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

Заключение

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

Полное понимание наследования прототипов имеет решающее значение для вашей эффективности как разработчика JavaScript. Знакомство с классами очень полезно, особенно с учетом того, что библиотеки JavaScript, такие как React, широко используют синтаксис класса.