Поиск по сайту:

Понимание классов в 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);
Output
x {} constructor: ƒ ()

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

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

Output
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, а также добавленный нами новый экземпляр.

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

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

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

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.