Классы
Важно
Синтаксический сахар на поверхности прототипной модели наследования.
Инкапсуляция 🕵️♀️
Инкапсуляция есть ни что иное, как реализация приватности. В JavaScript подобная концепция реализуется благодаря функциям и их областям видимости.
1// Функция конструктор2var Person = function(name) {3 // Приватная функция4 var log = function(message) {5 console.log(message);6 };78 // Публичное свойство9 this.name = name;1011 // Публичный метод12 this.logger = function(message) {13 log('Public ' + message);14 };15};
В JavaScript отсутствует возможность явно обозначить приватность, как в других языках программирования с помощью ключевых слов private
, protected
и public
.
Подобная проблема достаточно просто решается с помощью тех же самых функций — создаётся ещё одна обёртка над кодом в виде само вызывающейся анонимной функции, например, при использовании паттерна “модуль”:
Реализация паттерна модуль для калькулятора
1var Peron = (function() {2 // Приватная функция3 var log = function(message) {4 console.log(message);5 };67 var Person = function(name) {8 // Публичное свойство9 this.name = name;10 };1112 // Публичный метод13 Person.prototype.logger = function(message) {14 log('Public ' + message);15 };1617 // Экспорт публичной функции18 return Person;19})();
Наследование 👨👩👧👦
С помощью наследования вы, буквально, говорите: “У меня есть один конструктор/класс и другой конструктор/класс, который точно такой же, как и первый, кроме вот этого и вот этого”.
Чаще всего наследование в JavaScript реализуется с помощью функции Object.create()
, позволяющий создать новый объект с заданным прототипом.
1// функция конструктор2const Person = function(name) {3 this.name = name + ' Doe';4};56// запись метода в прототип7Person.prototype.sayName = function() {8 console.log(this.name);9};1011// Вызов конструктора родителя внутри дочернего12// конструктора для записи всех свойств13const GreatPerson = function(name, phrase) {14 Person.apply(this, arguments);15 this.phrase = phrase;16};1718// Перезапись прототипа дочернего конструктора19GreatPerson.prototype = Object.create(Person.prototype);2021GreatPerson.prototype.sayPhrase = function() {22 console.log(this.name + ' says: "' + this.phrase + '"');23};2425// создание нового объекта26const john = new Person('John');27const jane = new GreatPerson('Jane', 'Hello, World!');2829john.sayName(); // John Doe30jane.sayName(); // Jane Doe31jane.sayPhrase(); // Jane Doe says: "Hello, World!"
Важно
в JavaScript прототипное наследование “динамическое”, можно изменять всё налету, классическое же наследование подобным похвастаться не может: всё, что вы объявили в одном классе останется там навсегда.
Полиморфизм 🤯
Полиморфизм проще всего постичь на примере встроенных конструкторов (String
, Array
, Object
).
Например число 42
и массив [4,2]
являются разными типами, но разделяют определённую часть методов, например, метод toString
, унаследованный от Object
.
Метод toString
можно весьма успешно переназначить, во-первых, в прототипе функции конструктора, и, во-вторых, сразу же для данного конкретного объекта.
1// Наш собственный конструктор2const Person = function(name) {3 this.name = name;4};56// Переназначение метода toString для всех объектов,7// созданных с помощью данного конструктора8Person.prototype.toString = function() {9 return 'Person ' + this.name;10};1112const john = new Person('John');1314// Два массива, второй абсолютно обычный,15// для первого переназначен метод toString16const arr1 = [4, 2];17const arr2 = [5, 3];18arr1.toString = function() {19 return 'Array ' + this.reduce(function(result, item) {20 return result + '' + item;21 });22};2324// В итоге25console.log(john.toString()); // Person John26console.log(arr1.toString()); // Array 4227console.log(arr2.toString()); // 5,3
При использовании метода toString
на каждом из объектов происходит проверка того, какой метод нужно выбрать.
Проверка ведётся следующим образом: изначально мы проверяем существует ли нужное свойство на самом объекте, если существует, то используется именно оно, если же нет,
то проверка продолжается — на наличие свойства проверяется прототип, потом прототип прототипа, потом прототип прототипа прототипа... и так, пока не дойдём до самого конца — null.
Важно
Полиморфизм отвечает за то, чей метод вызвать.
Классы
important
Код внутри тела класса всегда выполняется в strict mode
1class Person {2 constructor(name) {3 this.name = name;4 }56 sayName() {7 console.log(`Person ${this.name} said his name`);8 }9}1011const john = new Person('John');12john.sayName(); // Person John said his name
Пример выше можно записать в стиле функции конструктора:
1var Person = function(name) {2 this.name = name;3};45Person.prototype.sayName = function() {6 console.log('Person ' + this.name + ' said his name');7};89var john = new Person('John');10john.sayName(); // Person John said his name
Что нужно знать про классы:
- Создавая класс, вы пользуетесь блоком кода (всё, что находится между
{
и}
), внутри которого объявляете, всё, что хотите видеть в прототипе. - Запись
class Person
означает, что будет создана функция конструкторPerson
(всё точно так же, как и в ES5) - Свойство
constructor
используется для обозначение того, что будет происходить непосредственно в самом конструкторе. - Все методы для класса используют краткую запись.
- При перечислении методов не надо использовать запятые (на самом деле, они запрещены)
Важно
используя ключевое слово class
стоит помнить, что мы до сих пор работаем с обычными функциями. То есть если проверить тип класса, то обнаружим function
:
1typeof Person === 'function'; // true
Как и при работе с функциями конструкторами мы можем записать “класс” (на самом деле, только конструктор) в переменную. Иногда подобная запись может быть чрезвычайно полезной, например, когда нужно записать конструктор, как свойство объекта.
1const Person = class P { /* делаем свои дела */ }2const obj = {3 Person4};
Конструктор
Person
объявленная через ключевое словоclass
обязана быть вызвана с операторомnew
.Конструктор, созданный с помощью
class
не испытывает на себе поднятия (hoisting
).
extends
ES6 классы также обладают синтаксическим сахаром для реализации прототипного наследования. Для подобных целей используется extends
:
1class GreatPerson extends Person {2 constructor(name, phrase) {3 super(name);4 this.phrase = phrase;5 }6 sayPhrase() {7 console.log(`${this.name} says: "${this.phrase}"`)8 }9}1011const jane = new Person('Jane', 'Hello, World!');12jane.sayName(); // Person Jane said his name13jane.sayPhrase(); // Jane says: "Hello, World!"
super
В примере выше мы использовали super
для вызова конструктора-родителя.
С помощью подобного вызова мы записали свойство name
для текущего объекта.
Другими словами super
при вызове внутри конструктора (свойства constructor
) — вызывает конструктор родителя и записывает в текущий объект (то есть в this
) всё, что от него требуется.
В ES5 для подобных действий приходилось напрямую обращаться к конструктору:
1var GreatPerson = function(name, phrase) {2 // Передача всех аргументов в конструктор родителя3 Person.apply(this, arguments);45 // или только одного6 Person.call(this, name);78 // запись новых свойств9 this.phrase = phrase;10};
Если вы захотите обратиться к любому методу, записанному в прототип родителя внутри метода потомка, то super
и здесь вас сможет выручить.
1class Person {2 constructor(name) {3 this.name = name;4 }5 speak(phrase) {6 return `${this.name} says ${phrase}`;7 }8}910class Speaker extends Person {11 speak(phrase) {12 console.log(`${super.speak(phrase)} very confidently`);13 }14}1516const bob = new Speaker('Bob');17const john = new Person('John');18console.log(john.speak('I don\'t have a lot of money'));19// John says "I don't have a lot of money"20bob.speak('I have a lot of money');21// Bob says "I have a lot of money" very confidently
Без super
нам бы пришлось напрямую обращаться к прототипу конструктора родителя, чтобы получить перезаписанный нами метод:
1var Person = function(name) {2 this.name = name;3};45Person.prototype.speak = function(phrase) {6 return `${this.name} says "${phrase}"`;7};8var Speaker = function(name) {9 Person.call(this, name);10};11Speaker.prototype = Object.create(Person.prototype);12Speaker.prototype.speak = function(phrase) {13 // обращаемся к методу speak из функции родителя14 var originalSpeak = Person.prototype.speak;15 // в добавок ко всему нужно использовать и call16 console.log(originalSpeak.call(this, phrase) + ' very confidently');17};
Таким образом, super
в зависимости от того, где вы его решили использовать будет работать по-разному, но при любом использовании он готов достаточно сильно сократить ваш код и избавить от не самого понятного способа вызова конструктора родителя.
Подводный камень
super
: при реализации наследования с помощью extends
и работе с дочерним конструктором необходимо вызвать super()
перед добавлением любого нового свойства.
1class Pesron {2 constructor(name) {3 this.name = name;4 }5}67// Всё работает хорошо8class GreatPerson extends Person {9 constructor(name, phrase) {10 // Необходимо вызвать super11 super(name);12 this.phrase = phrase;13 }14}1516// А тут ошибка17class GreatPerson extends Person {18 constructor(name, phrase) {19 // Необходимо вызвать super до записи собственных свойств20 this.phrase = phrase;21 super(name);22 }23}
Классы без конструкторов
1class Speaker extends Person {2 speak(phrase) {3 console.log(`${super.speak(phrase)} very confidently`);4 }5}
В примере выше не было использовано свойство constructor
для класса Speaker
, поскольку в нём просто нет необходимости в данном случае.
Но свойство name
всё равно было записано.
Когда вы не указываете явно, что нужно сделать в конструкторе, то всё решается без вашего участия. Можно представить себе данный процесс следующим образом:
1// Эквивалентно созданию класса без конструктора2class GreatPerson extends Person {3 constructor() {4 super(...arguments);5 }6}
static
При работе с конструкторами в ES5 многие привыкли использовать функции, как объекты (они же и есть объекты) и вешать на них служебные функции:
1// Конструктор2var Person = function() {};34// Служебное значение5Person.hello = 'World';6// Служебная функция7Person.speak = function() {8 console.log('I am alive!');9};
Подобные значения не имеют ничего общего с прототипами и доступны только при запросе непосредственно с функции (объекта):
1Person.speak(); // I am alive!2var john = new Person();3john.speak(); // Ошибка: нет такого метода!
При работе с классами запись подобных свойств доступна сразу внутри класса с помощью оператора static
.
Причем все записанные подобным образом свойства при наследовании благополучно переносятся на конструктор потомка:
1class Person {2 static sos() {3 console.log('I really need help!');4 }5}67class Artist extends Person {8 draw(art) {9 console.log(`Artist has just drawn ${art}`);10 }11}1213const artist = new Artist();14Person.sos(); // I really need help!15Artist.sos(); // I really need help!16artist.sos(); // artist.sos is not a function
Практика 👩💻👨💻
Создать Список для чтения
Создать класс BookList
Создать еще один класс Book
BookList
: должен иметь следующие свойства:
- Количество книг, отмеченных как прочитанные
- Количество книг, отмеченных как непрочитанные
- Ссылка на следующую (next) книгу для чтения (book instance)
- Ссылка на текущую(current) читаемую книгу (book instance)
- Ссылка на последнюю(last) прочитанную книгу (book instance)
- Массив всех Книг
Каждая книга должна иметь несколько свойств:
- заглавие
- Жанр
- Автор
- прочитана (
true
/false
) - Дата чтения может быть пустым, иначе должен быть объектDate
В каждом списке книг должно быть несколько методов:
.add(book)
- следует добавить книгу в список книг.
finishCurrentBook()
- должен отметить книгу, которая сейчас читается, как "прочитанную"
- изменить дату чтения на текущую
new Date(Date.now())
- изменить последнюю прочитанную книгу на книгу, которую только что закончили
- изменить текущую книгу, чтобы она была следующей книгой для чтения
- изменить свойство следующей книги для чтения, чтобы она была первой непрочитанной книгой, которую вы найдете в списке книг
Booklist
и Book
может потребоваться больше методов, чем описано.
1class Book {2 constructor(title, genre, author, read = false, date = new Date()){3 this.title = title || "No Title";4 this.genre = genre || "Fiction";5 this.author = author || "No Author";6 this.read = read;7 this.readDate = date8 }9}1011class BookList{1213 constructor(books =[]){14 this.list = books;15 }16}1718const books = [19 new Book("A Smarter Way to Learn JavaScript", "programming", "Mark Myers"),20 new Book(21 "Eloquent JavaScript: A Modern Introduction to Programming",22 "programming",23 "Marjin Haverbeke"24 ),25 new Book("JavaScript: The Good Parts", "programming", "Douglas Crockford"),26 new Book("JavaScript: The Definitive Guide", "Programming", "David Flanagan")27];2829const bookList = new BookList(books);3031console.log("current", bookList.currentBook);32console.log("next", bookList.nextBook);33console.log("last", bookList.lastBook);3435bookList.finishCurrentBook();36console.log("==2==");3738console.log("current", bookList.currentBook);39console.log("next", bookList.nextBook);40console.log("last", bookList.lastBook);4142bookList.finishCurrentBook();43console.log("==3==");4445console.log("current", bookList.currentBook);46console.log("next", bookList.nextBook);47console.log("last", bookList.lastBook);