Пару недель назад (статья написана в августе — прим. перев.) мы описывали новую систему классов в ES6 в тривиальных случаях создания конструктора объекта. Мы показали, как можно писать код типа такого:
К сожалению (как некоторые заметили), у нас не было тогда времени чтобы поговорить о всей мощи ES6 классов. Как и в традиционных системах классов (а-ля Java и С++), в ES6 возможно наследование, когда один класс берет за базу другой и расширяет его.
Давайте посмотрим поближе на возможности этой фичи.
До того, как мы пойдем дальше, будет полезно потратить немного времени на рассмотрение особенностей наследования свойств и dynamic prototype chain (динамической цепочки прототипов).
Когда мы создаем объект, мы можем положить в него свойства (properties), также он наследует свойства своих прототипов. Javascript-программисты знакомы с существующим
Далее, когда мы добавляем свойства в
Памятуя об этом, мы можем увидеть, как соединить цепь прототипов объекта, созданного классом. Помните, что когда мы создаем класс, мы создаем новую функцию, соответствующую
Это достаточно дремучее объяснение. Давайте попробуем показать на примере, как можно связать объекты без нового синтаксиса, а потом сделаем тривиальное дополнение, чтобы сделать вещи эстетически приятнее.
Продолжая предыдущий пример, допустим что у нас есть класс
Когда мы пишем код таким образом, мы сталкиваемся с той же проблемой что и раньше со
Это достаточно уродливо. Мы добавили синтакс классов для инкапсуляции всей логики в одном месте, чтобы не нужно было делать никакого «связывания» после. Java, Ruby и остальные ОО-языки имеют синтаксис для декларирования одного класса как подкласса другого, поэтому должен и Javascript. Мы используем ключевое слово
Вы можете поставить любое выражение после
Можно использовать null, чтобы не наследоваться от
Итак, мы можем создавать подклассы, наследовать свойства и иногда наши методы перекрывают (shadow) методы родителя. А что если мы хотим обойти механизм перекрывания?
Допустим, мы хотим написать подкласс
Заметьте, что геттер свойства
Доступы к super-свойствам (
От переводчика: для удобства перевода (и чтобы посмотреть, насколько тема актуальна для сообщества) опять разделю статью на 2 примерно равные части.
class Circle {
constructor(radius) {
this.radius = radius;
Circle.circlesMade++;
};
static draw(circle, canvas) {
// Canvas drawing code
};
static get circlesMade() {
return !this._count ? 0 : this._count;
};
static set circlesMade(val) {
this._count = val;
};
area() {
return Math.pow(this.radius, 2) * Math.PI;
};
get radius() {
return this._radius;
};
set radius(radius) {
if (!Number.isInteger(radius))
throw new Error("Circle radius must be an integer.");
this._radius = radius;
};
}
К сожалению (как некоторые заметили), у нас не было тогда времени чтобы поговорить о всей мощи ES6 классов. Как и в традиционных системах классов (а-ля Java и С++), в ES6 возможно наследование, когда один класс берет за базу другой и расширяет его.
Давайте посмотрим поближе на возможности этой фичи.
До того, как мы пойдем дальше, будет полезно потратить немного времени на рассмотрение особенностей наследования свойств и dynamic prototype chain (динамической цепочки прототипов).
Наследование в Javascript
Когда мы создаем объект, мы можем положить в него свойства (properties), также он наследует свойства своих прототипов. Javascript-программисты знакомы с существующим
Object.create
API, который позволяет легко делать что-нибудь вроде такого:var proto = {
value: 4,
method() { return 14; }
}
var obj = Object.create(proto);
obj.value; // 4
obj.method(); // 14
Далее, когда мы добавляем свойства в
obj
с таким же именем как и в proto
, они перекрывают (shadow) изначальные.obj.value = 5;
obj.value; // 5
proto.value; // 4
Основы наследования
Памятуя об этом, мы можем увидеть, как соединить цепь прототипов объекта, созданного классом. Помните, что когда мы создаем класс, мы создаем новую функцию, соответствующую
constructor
методу в дефиниции (определении) класса, содержащую все статические методы. Также мы создаем объект, который будет свойством property
у созданной нами функции, и который будет содержать все методы инстанса (экземпляра) класса. Чтобы создать новый класс, который наследует все статические свойства мы должны заставить новый объект-функцию наследоваться от объекта-функции суперкласса. Похожим образом, мы должны заставить объект prototype
новой функции наследоваться от prototype
суперкласса, чтобы обеспечить наследование методов инстанции класса. Это достаточно дремучее объяснение. Давайте попробуем показать на примере, как можно связать объекты без нового синтаксиса, а потом сделаем тривиальное дополнение, чтобы сделать вещи эстетически приятнее.
Продолжая предыдущий пример, допустим что у нас есть класс
Shape
, для которого мы хотим создать подкласс (subclass):class Shape {
get color() {
return this._color;
}
set color(c) {
this._color = parseColorAsRGB(c);
this.markChanged(); // перерисуем canvas позже
}
}
Когда мы пишем код таким образом, мы сталкиваемся с той же проблемой что и раньше со
static
свойствами: у нас нет синтаксического способа изменить прототип функции при дефиницировании (определении). Хотя это можно обойти с помощью Object.setPrototypeOf
, этот подход в целом менее быстродейственный и имеет меньшие возможности по оптимизации для JS-движков, нежели когда у нас есть возможность задать прототип функции при ее создании. class Circle {
// См. выше
}
// Связываем свойства инстанса
Object.setPrototypeOf(Circle.prototype, Shape.prototype);
// Связываем статические свойства
Object.setPrototypeOf(Circle, Shape);
Это достаточно уродливо. Мы добавили синтакс классов для инкапсуляции всей логики в одном месте, чтобы не нужно было делать никакого «связывания» после. Java, Ruby и остальные ОО-языки имеют синтаксис для декларирования одного класса как подкласса другого, поэтому должен и Javascript. Мы используем ключевое слово
extends
чтобы мы могли писать вроде:class Circle extends Shape {
// См. выше
}
Вы можете поставить любое выражение после
extends
, пока у него есть валидный конструктор со свойством prototype
. Например, подойдут:- Другой класс
- Классоподобные функции с различными механизмами наследования
- Обычная функция
- Переменная, содержащая функцию или класс
- Доступ к свойству объекта (A property access on an object)
- Вызов функции
Можно использовать null, чтобы не наследоваться от
Object.prototype
.Super-свойства
Итак, мы можем создавать подклассы, наследовать свойства и иногда наши методы перекрывают (shadow) методы родителя. А что если мы хотим обойти механизм перекрывания?
Допустим, мы хотим написать подкласс
Circle
который хэндлит (обрабатывает) скалирование круга на заданное число. Чтобы достигнуть этого, мы можем написать достаточно неестественный класс:class ScalableCircle extends Circle {
get radius() {
return this.scalingFactor * super.radius;
}
set radius() {
throw new Error("ScalableCircle radius is constant." +
"Set scaling factor instead.");
}
// Код, где хэндлится scalingFactor
}
Заметьте, что геттер свойства
radius
использует super.radius
. Это новое ключевое слово super
позволяет нам искать свойство начиная с прототипа, игнорируя любое перекрытие (shadowing), которое у нас может быть.Доступы к super-свойствам (
super[expr]
, кстати, тоже работает) могут быть использованы в любой функции, которая объявлена с помощью синтаксиса дефинирования (определения) методов. Хотя эти функции могут быть вытащены из изначального объекта, доступы привязаны к тому объекту, где метод был дефинирован (определен) первый раз. Это значит, что «вытаскивая» метод в локальную переменную (ох уж этот Javascript — прим. перев.), мы не изменим поведение super-доступа.var obj = {
toString() {
return "MyObject: " + super.toString();
}
}
obj.toString(); // MyObject: [object Object]
var a = obj.toString;
a(); // MyObject: [object Object]
От переводчика: для удобства перевода (и чтобы посмотреть, насколько тема актуальна для сообщества) опять разделю статью на 2 примерно равные части.
Arilas
По поводу последнего(сохранение привязки), откуда оригинальный автор это взял? В стандарте я чего-то этого не нашел, и если бы это было, то это потеря обратной совместимости. В том же Babel принудительно методы из инстанса отвязывают от контекста с помощью (0, improtedObject.method)(...args). Всегда контекст теряется, не важно, из класса или из объекта.
P.S. Да, там Undefined с большой.
Ununtrium
Только что в хроме попробовал,
MyObject: [object Object]
иMyObject: [object global]
соответственно. А в FF super не работает :(rock
Оба результата верны, разгадка в strict mode.
Arilas
Не совсем, this — теряется 100%(и с 'use strict' и без), сам прототип нет, вот пример:
То-есть оно оставляет привязанным прототип, но никак не this.
Если бы он оставлял одинаковый this — это было бы ошибкой, так как теряется совместимость со старыми версиями. (Если нужно оставить this — юзайте bind)
В стандарте я такой вещи не видел, хотя просматривал много ее (особенно когда еще стандарт был в драфте)
rock
Я что-то говорил про привязанный
this
? :) Смотрите комментарий ниже. Babel эмулирует окружение модулей, у него строгий режим исполнения по умолчанию. Топикостартер же запустил код без'use strict'
.rock
Домашний объект биндится при создании метода,
super
получается как его прототип на рантайме, а контекстом получения свойства (возможный вызов геттеров) и исполнения метода становится текущийthis
. То есть дляsuper.toString();
в данном случае полноценным дешугарингом является: