Наследование в ES6
Создание объекта на основе класса является одной из сторон объектно-ориентированного программирования . Другая сторона это наследование . То есть способность объектов наследовать свойства и методы от других классов.
Все материалы по ES6
Если у вас есть опыт работы с другими объектно ориентированными языками, то вы почувствуете себя в своей тарелке.
Если вы работали только с JavaScript, но при этом понимаете как работают прототипы, то я уверен, что вы будете очень рады увидеть новый синтаксис, который, тем не менее, не вводит новые объектно-ориентированную модель наследование, а внутри используются все те же прототипы.
Для начала, в папке src я создам файл inheritance.js , что в переводе с английского означает наследование.
Далее, я запущу Babel, который будет следить за файлами и при их изменении сгенерирует свежие версии .
Открою папку в командной строке и наберу команду:
npm run watch.
Последние версии браузеров поддерживает новый синтаксис без транспиляции и мой браузер входит в число, но тем не менее в конце мы посмотрим на транспонированный код.
Также, необходимо указать этот файл в index.html
<script src="src/inheritance.js"></script>
Продолжая тему задач, я создам класс Task и в конструкторе выведу сообщение - “создание задачи”.
class Task { constructor() { console.log("Создание задачи"); } }
Далее, создадим ещё один класс, который будет представлять из себя подзадачу.
Класс SubTask - подзадачи является задачей, но также будет иметь родительскую задачу . Для того чтобы указать что SubTask является подклассом, используется ключевое слово
extends
которое по-русски означает "расширяет".
После названия, я ставлю пробел и напишу
extends
и далее название класса который мы расширяем.
Это всё что нужно для того чтобы определить наследование между двумя классами.
class SubTask extends Task { }
После классов я создам объект, который будет представителем класса Task и Объект, который будет представителем класса SubTask.
let task = new Task(); let subtask = new SubTask();
Давайте посмотрим на оба объекта в консоли браузера и используя оператор
instanceof
проверим - является ли объект subtask представителем обоих классов?
console.log(subtask instanceof Task); console.log(subtask instanceof SubTask);
Откроем файл в браузере и посмотрим в консоли.
Мы видим два сообщения о создании задачи.
Далее мы видим два объекта и два сообщения
true
Тот факт, что мы видим два сообщения о создании задачи означает, что SubTask не имея конструктора, использует конструктор родителя.
Таким образом получается:
Если у подкласса нет конструктора то он будет тебя использовать родительский конструктор!
А сообщение
true
означает, что subtask. является представителем класса SubTask.
и также subtask является представителем класса Task.
Наследование свойств
Как мы знаем из прошлого поста ES6:Классы , классы могут иметь свойства.Ограничимся двумя title - заголовок и done - выполнена задача или нет.
Конструктор может принимать параметры, которые далее могут быть использованы для создания свойств.
При создании объектов укажем заголовок задачи
В качестве задачи отправим -”Изучить JavaScript” , а подзадачи отправим - “ Изучить ES6”
Далее, конструктор будет принимать параметры title, которому мы присвоим свойство this.title . И инициализируем свойство this.done со значением false То есть задача по умолчанию будет не выполнена.
Заметьте, что мы инициализировали свойство в конструкторе у класса Task, но при этом в SubTask, мы отправляем заголовок.
и пока конструктора у класса SubTask нет.
class Task { constructor(title) { this.title = title; this.done = false; console.log('Создание задачи'); } } class SubTask extends Task { } let task = new Task('Изучить JavaScript'); let subtask = new SubTask('Изучить ES6'); console.log(task); console.log(subtask); console.log(subtask instanceof Task); console.log(subtask instanceof SubTask);
Давайте посмотрим что произойдет.
В консоли мы видим два сообщения о создании задачи. Дальше мы видим два объекта, у которых свойства изучения указаны правильно.
Это означает, что
если у подкладка нет конструктора то он воспользуются конструктора суперкласса или родительского класса
Тот, в свою очередь, правильно инициализирует свойства.
Но это не значит что мы не можем определить классу SubTask свой конструктор.
Давайте это сделаем. Пока не будем указывать параметры и просто посмотрим что произойдет?
class Task {
constructor(title) {
this.title = title;
this.done = false;
console.log('Создание задачи');
}
}
class SubTask extends Task {
constructor() {
}
}
let task = new Task('Изучить JavaScript');
let subtask = new SubTask('Изучить ES6');
console.log(task);
console.log(subtask);
console.log(subtask instanceof Task);
console.log(subtask instanceof SubTask);
Посмотрим в консоли и :
И мы увидим сообщение об ошибке.
Это происходит потому что
если подклассу мы указываем конструктор, то этот конструктор должен вызвать конструктор родительского класса .
Мы можем это сделать используя ещё одно ключевое слово которое называется
super
.
Я просто напишу
super
и поставлю круглые скобки super ();
и супер вызовет конструктор родительского класса.
Давайте посмотрим.
Вроде бы всё сработало но заметьте, что у subtask заголовок исеет значение -
undefined
.
Это происходит потому, что конструктор родительского класса ожидает title в качестве параметра.
Но в данном случае, мы его не отправляет для этого в конструкторе
Я укажу параметр title в конструкторе.
И в качестве аргументов super() Я тоже отправлю title.
class Task { constructor(title) { this.title = title; this.done = false; console.log('Создание задачи'); } } class SubTask extends Task { constructor( title ) { super( title ); } } let task = new Task('Изучить JavaScript'); let subtask = new SubTask('Изучить ES6'); console.log(task); console.log(subtask); console.log(subtask instanceof Task); console.log(subtask instanceof SubTask);
Давайте посмотрим, что у нас получилось.
Теперь мы видим, что всё работает правильно
И вот эти строчки я удалю они нам больше не нужны
console.log(subtask instanceof Task); console.log(subtask instanceof SubTask);
Для того чтобы убедиться, что SubTask использует свой конструктор, давайте выведем сообщение похожие на сообщение родительского класса, только задачу изменим на создание подзадачи.
class SubTask extends Task { constructor(title) { super(title); console.log('Создание подзадачи'); } }
Посмотрим. И теперь мы видим 3 сообщения - создание задачи, создание задачи и создание подзадачи.
Первое сообщение выводится при создании задачи Task
При создании подзадачи мы попадаем в конструктор дальше мы вызываем конструктор родительского класса и здесь выводится сообщение о создании задачи.
Дальше возвращаемся конструктор SubTask и будет сообщение о создание подзадачи.
Именно поэтому видим сообщения 3 раза.
Давайте посмотрим что будет если я конструктор родительского класса как-будто удалил (закомментирую) и вызов родительского класса я тоже пока удалю. И создание объекта task тоже удалю.
И соответственно вывод в консоли тоже удалю.
class Task { // constructor(title) { // this.title = title; // this.done = false; // console.log('Создание задачи'); // } } class SubTask extends Task { constructor(title) { // super(title); console.log('Создание подзадачи'); } } // let task = new Task('Изучить JavaScript'); let subtask = new SubTask('Изучить ES6'); console.log(task); console.log(subtask);
Посмотрим, сможет ли SubTask правильно инициализироваться без конструктора у Task?
И мы видимся ту же ошибку.
Это означает что:
если мы указываем для подкласса конструктор, то этот конструктор должен вызывать конструктор родительского класса.
Этот вызов используют ключевое слово
Этот вызов используют ключевое слово
super
и должен быть первой строчкой до того как мы приступим к присвоению каких-либо свойств.
И в качестве примера, давайте присвоим классу SubTask свойство parent или родитель, в котором мы укажем родительскую задачу.
class Task { constructor(title) { this.title = title; this.done = false; console.log('Создание задачи'); } } class SubTask extends Task { constructor( title, parent ) { super(title); this.parent = parent; console.log('Создание подзадачи'); } } let task = new Task('Изучить JavaScript'); let subtask = new SubTask('Изучить ES6', task); console.log(task); console.log(subtask);
Давайте посмотрим что случилось.
и мы видим? что у объекта subtask появилась свойство - parent, которое содержит в себе задачу.
И это заголовок - “Изучить JavaScript “
Помимо наследования свойств, классы могут наследовать и поведение, то есть - методы.
Наследование методов.
Давайте добавим классу Task метод complete, который мы будем использовать для того, чтобы завершить задачу. И в теле методa мы укажем свойства this.done со назначением true.И в консоли выведен сообщение о том что задача с таким заголовком выполнена.
Для этого я воспользуюсь шаблонной строкой и напишу “задача выполнена” используя местозаполнитель покажу. title - заголовок задачи.
complete() { this.done = true; console.log(`Задача : "${this.title}" выполнена! `); }
В коде, после создания объектов, давайте завершим задачу и также завершим подзадачу:
task.complete(); subtask.complete();
Посмотрим в консоль.
Мы видим сообщение, что задачи выполнены.
Это означает что SubTask унаследовал методы complete. То есть - он ему доступен.
Таким же образом мы можем унаследовать свойства get, set и статические свойства.
То есть все члены класса.
Но что делать если нас не устраивает родительский метод?
Допустим мы хотим его полностью поменять или немного изменить?
Мы можем его перезаписать!
Перезапись родительского метода
Для этого в подклассе, мы объявляем метод с таким же названием как у родительского, в теле которого мы можем сделать всё что угодно.Я пометил, что задача выполнена.
И в консоли выведу сообщение -
console.log(`Подзадача : "${this.title}" выполнена! `);
Код полностью:
class Task {
constructor(title) {
this.title = title;
this.done = false;
console.log('Создание задачи');
}
complete() {
this.done = true;
console.log(`Задача : "${this.title}" выполнена! `);
}
}
class SubTask extends Task {
constructor( title, parent ) {
super(title);
this.parent = parent;
console.log('Создание подзадачи');
}
complete() {
this.done = true;
console.log(`Подзадача : "${this.title}" выполнена! `);
}
}
let task = new Task('Изучить JavaScript');
let subtask = new SubTask('Изучить ES6', task);
task.complete();
subtask.complete();
console.log(task);
console.log(subtask);
Давайте посмотрим, что произошло.
И теперь вместо “задача” мы видим “подзадача”.
Это означает, что SubTask теперь использует свой метод complete(), а не родительский. Но заметьте, что нам пришлось указать
this.done
в двух местах - у родительского класса и у подкласса.
Конечно, мы повторили лишь одну строчку, но тем не менее в настоящем классе методы обычно выполняет чуть больше работы и для того чтобы не нарушать принципы D.R.Y (Don’t Repeat Yourself - не повторяйся) в унаследованном методе, мы можем вызвать метод родительского класса, а потом как либо его дополнить.
this.done = true; у подкласса я удалю использую ключевое слово super далее ставлю точку и выбираю метод родительского класса - в данном случае -
super.complete();
Таким образом, как только вызовите метод complete(); у объекта subtask мы “перепрыгнем” в родительский класс пометим this.done = true;
Далее, мы выведем сообщение “задача изучить ЕS6 выполнено”. После чего мы “перепрыгнем” обратно и выведем сообщение “ подзадача изучить ES6 выполнено!”
Давайте посмотрим.
И действительно, мы видим:
- - сообщение “задача изучить ЕS6 выполнила”.
- - видим сообщение “Подзадача : "Изучить ES6" выполнена! “
- - видим что свойства
done
- имеет значениеtrue
.
То есть - первое, мы успешно перезаписали родительский метод и второе - в этом перезаписанных методе мы использовали метод родительского класса.
Теперь для полной картины, я добавлю к классу Task геттер и сеттер для свойства title.
Также добавлю статическое свойство и статический метод.
Начнем с геттера и сеттера.
Геттер и сеттер.
Для начала названия в title я добавлю нижнее подчеркивание.После конструктора напишу:
get title() { return this._title; } set title( value ) { this._title = value; } }
get title(){}
и в теле метода мы просто вернем. this._title
и set title, который примет параметр value.
Для простоты я не буду проверять параметр и просто присвоили его.
Далее я создам статический метод static getDefaultTitle(), который просто вернет заголовок задачи по умолчанию.
static getDefaultTitle() { return 'Задача'; }
И после класса Task я добавлю статическое свойство, которое будет содержать в себе общее количество созданных задач.
Task.count = 0;
Инициализируем со значением 0. и при создании задачи добавим свойству единицу -
Task.count +=1;
далее, посмотрим как класс Task унаследовал эти методы и свойства.
console.log(SubTask.getDefaultTitle()); - статический метод у класса Task console.log(SubTask.count); - статическое сво-во.
а геттер и сеттер мы сможем увидеть сразу в консоли.
Давайте посмотрим на результат
Мы видим:
- -
Задача
- это вызов статического метода у SubTask. - -видим цифру
2
- это статическое свойство SubTask.count. - - у субкласса есть свойство title помимо свойства _title.
Таким образом мы унаследовали свойства, методы, статические свойства, статические методы ,и геттеры, и сеттеры.
Файл inheritance.js полностью.
class Task { constructor(title) { this._title = title; this.done = false; Task.count +=1; console.log('Создание задачи'); } complete() { this.done = true; console.log(`Задача : "${this.title}" выполнена! `); } get title() { return this._title; } set title( value ) { this._title = value; } static getDefaultTitle() { return 'Задача'; } } Task.count = 0; class SubTask extends Task { constructor( title, parent ) { super(title); this.parent = parent; console.log('Создание подзадачи'); } complete() { super.complete(); console.log(`Подзадача : "${this.title}" выполнена! `); } } let task = new Task('Изучить JavaScript'); let subtask = new SubTask('Изучить ES6', task); console.log(SubTask.getDefaultTitle()); console.log(SubTask.count); task.complete(); subtask.complete(); console.log(task); console.log(subtask);И напоследок давайте взглянем на то, что сгенерировал Babel:
'use strict'; var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Task = function () { function Task(title) { _classCallCheck(this, Task); this._title = title; this.done = false; Task.count += 1; console.log('Создание задачи'); } _createClass(Task, [{ key: 'complete', value: function complete() { this.done = true; console.log('\u0417\u0430\u0434\u0430\u0447\u0430 : "' + this.title + '" \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0430! '); } }, { key: 'title', get: function get() { return this._title; }, set: function set(value) { this._title = value; } }], [{ key: 'getDefaultTitle', value: function getDefaultTitle() { return 'Задача'; } }]); return Task; }(); Task.count = 0; var SubTask = function (_Task) { _inherits(SubTask, _Task); function SubTask(title, parent) { _classCallCheck(this, SubTask); var _this = _possibleConstructorReturn(this, (SubTask.__proto__ || Object.getPrototypeOf(SubTask)).call(this, title)); _this.parent = parent; console.log('Создание подзадачи'); return _this; } _createClass(SubTask, [{ key: 'complete', value: function complete() { _get(SubTask.prototype.__proto__ || Object.getPrototypeOf(SubTask.prototype), 'complete', this).call(this); console.log('\u041F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 : "' + this.title + '" \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0430! '); } }]); return SubTask; }(Task); var task = new Task('Изучить JavaScript'); var subtask = new SubTask('Изучить ES6', task); console.log(SubTask.getDefaultTitle()); console.log(SubTask.count); task.complete(); subtask.complete(); console.log(task); console.log(subtask);
Кода опять много, но тем не менее можно разобрать название класса Task , конструктор, методы геттеры и сеттеры, подкласс ... Ну и так далее . Таким образом, мы теперь разобрались как работают классы и наследование .
В следующем посте мы сравним объектно-ориентированное программирование, как это было в ES5 и как это есть в ES6.
UPDATE
Основные отличия ES6 от ES5.
- Функции в ES6 не поднимаются. То есть нельзя вызвать функцию до её объявления в коде.
- В ES6 переменные “не загрязняют” глобальное пространство имен.
В ES5 все переменные являются производными глобального объекта window и мы всегда можем это проверить простым выводом в консоль:
console.log(window.name_var === name_var); // true
В ES6 это не так.
Объявление функций и классов.
Функции в джаваскрипт можно Объявлять двумя способами:Объявление функции - function declaration
function имя_функции() { что-то делает. }
Выражение функций - function expression.
Для того чтобы объявить функцию нужно в начале объявить переменную и в качестве значения присвоить функцию. Функция может быть безымянной или иметь название. Если указать название то это поможет при отладке.let Task = function Task() { }
Далее мы можем создать новый объект используя функцию Task:
let task = new Task();
Как мы уже знаем, классы являются функциями и поэтому класс мы можем объявить двумя способами.
Class Declaration - объявление класса.
Это самый простой и знакомый нам способ.class Task { constructor(){ console.log(‘Creating a Task); } };
Class Expression - выражение класса.
Для этого создадим переменную и в качестве значения присвоим класс. Этот класс может иметь имя или быть безымянным. Если мы указываем имя то это поможет при дальнейшей отладке. В теле класса укажем конструктор.let Task = class Task { constructor(){ console.log(‘Creating a Task’); } }; let task = new Task();
Если посмотреть этот код в консоли браузера, то мы увидим :
Creating a Task
Даже если мы уберем имя класса
Task
, то в браузере мы увидим то же самое сообщение.
Теперь давайте попробуем создать наследование.
Создадим переменную, которой присвоим в качестве значения - безымянных класс, который является расширением родительского класса.
В теле класса укажем конструктор, который будет вызывать родительский класс (
super()
) и выведем сообщение.
let SybTask = class extends Task { constructor(){ super(); console.log(‘Creating a SubTask’); } }; let subtask = new SubTask();
Если посмотреть результат в браузере, то мы увидим два сообщения о создании задачи и одно сообщение о создании подзадачи.
Комментариев нет:
Отправить комментарий