Супербыстрый доступ к свойствам `super`
super
может быть использован для доступа к свойствам и методам родителя объекта.
Ранее доступ к свойству super
(например, super.x
) выполнялся через вызов времени выполнения. Начиная с V8 v9.0, мы используем систему кеширования inline (IC) в неоптимизированном коде и генерируем правильный оптимизированный код для доступа к свойствам super
, без необходимости перемещаться к выполнению времени.
Как вы можете видеть из графиков ниже, доступ к свойствам super
раньше был на порядок медленнее, чем обычный доступ к свойствам из-за вызова времени выполнения. Теперь разница значительно уменьшилась.
Доступ к свойствам super
сложно оценить, так как он должен происходить внутри функции. Мы не можем измерить отдельные запросы свойств, только крупные блоки работы. Таким образом, накладные расходы на вызов функции включены в измерения. Графики выше несколько недооценивают разницу между доступом к свойствам super
и обычным доступом, но они достаточно точны, чтобы продемонстрировать разницу между старым и новым доступом к свойствам super
.
В неоптимизированном режиме (режим интерпретации) доступ к свойствам super
всегда будет медленнее, чем обычный доступ к свойствам, так как требуется больше операций (чтение home object из контекста и чтение __proto__
из home object). В оптимизированном коде мы уже встраиваем home object как константу, если это возможно. Это можно улучшить, встраивая также его __proto__
как константу.
Прототипное наследование и super
Начнем с основ — что вообще означает доступ к свойствам super
?
class A { }
A.prototype.x = 100;
class B extends A {
m() {
return super.x;
}
}
const b = new B();
b.m();
Теперь A
является суперклассом для B
, а b.m()
возвращает 100
, как и ожидалось.
Реальность прототипного наследования в JavaScript гораздо сложнее:
Нужно четко различать свойства __proto__
и prototype
— они не означают одно и то же! Чтобы сделать это еще более запутанным, объект b.__proto__
часто называют «прототипом объекта b
».
b.__proto__
— это объект, от которого b
наследует свойства. B.prototype
— это объект, который станет __proto__
для объектов, созданных с помощью new B()
, то есть b.__proto__ === B.prototype
.
В свою очередь, у B.prototype
есть собственное свойство __proto__
, равное A.prototype
. Вместе это формирует цепочку прототипов:
b ->
b.__proto__ === B.prototype ->
B.prototype.__proto__ === A.prototype ->
A.prototype.__proto__ === Object.prototype ->
Object.prototype.__proto__ === null
Через эту цепочку b
может получить доступ ко всем свойствам, определенным в любом из этих объектов. Метод m
является свойством B.prototype
— B.prototype.m
, — и поэтому b.m()
работает.
Теперь мы можем определить super.x
внутри m
как поиск свойства, где мы начинаем искать свойство x
в __proto__
home object и движемся вверх по цепочке прототипов, пока не найдем его.
Home object — это объект, в котором определен метод. В данном случае home object для m
— это B.prototype
. Его __proto__
— это A.prototype
, так что именно там мы начинаем искать свойство x
. Мы назовем A.prototype
объектом начала поиска. В данном случае мы находим свойство x
сразу в объекте начала поиска, но в общем случае оно может также находиться выше в цепочке прототипов.
Если у B.prototype
есть свойство x
, мы его игнорируем, так как начинаем искать выше него в цепочке прототипов. Кроме того, в данном случае поиск свойства super
не зависит от получателя — объекта, который является значением this
при вызове метода.
B.prototype.m.call(some_other_object); // все равно возвращает 100
Если у свойства есть геттер, получатель будет передан в геттер в качестве значения this
.
В резюме: в доступе к свойствам super
, super.x
, объектом начала поиска является __proto__
home object, а получатель — это получатель метода, в котором происходит доступ к свойствам super
.
При обычном доступе к свойству, o.x
, мы начинаем поиск свойства x
в объекте o
и перемещаемся вверх по цепочке прототипов. Мы также используем o
в качестве получателя, если у x
есть геттер — объект поиска и получатель являются одним и тем же объектом (o
).
Доступ к свойству через super похож на обычный доступ к свойству, но объект поиска и получатель — разные.
Реализация ускоренного super
Указанный выше вывод также является ключом к реализации быстрого доступа к свойствам через super. V8 уже разработан для обеспечения быстрого доступа к свойствам — теперь мы обобщили его для случая, когда объект поиска и получатель различаются.
Система кэширования V8 с использованием встроенных данных является основной частью для реализации быстрого доступа к свойствам. Вы можете прочитать о ней в высокоуровневом введении, указанном выше, или более подробных описаниях представления объектов V8 и реализации системы кэширования V8 с использованием встроенных данных.
Для ускорения работы с super
мы добавили новый байткод Ignition, LdaNamedPropertyFromSuper
, который позволяет подключиться к системе IC в интерпретируемом режиме, а также создавать оптимизированный код для доступа к свойствам через super.
С помощью нового байткода мы можем добавить новый IC, LoadSuperIC
, для ускорения загрузки свойств через super. Аналогично LoadIC
, который обрабатывает обычную загрузку свойств, LoadSuperIC
отслеживает формы объектов поиска, которые он видел, и запоминает, как загружать свойства из объектов, имеющих одну из этих форм.
LoadSuperIC
повторно использует существующий механизм IC для загрузки свойств, просто с другим объектом поиска. Так как слой IC уже различает объект поиска и получателя, реализация должна была быть легкой. Но так как объект поиска и получатель всегда были одинаковыми, возникали ошибки, когда мы использовали объект поиска, хотя имелся в виду получатель, и наоборот. Эти ошибки были исправлены, и теперь мы правильно поддерживаем случаи, когда объект поиска и получатель различаются.
Оптимизированный код для доступа к свойствам через super создается на этапе JSNativeContextSpecialization
компилятора TurboFan. Реализация обобщает существующий механизм поиска свойств (JSNativeContextSpecialization::ReduceNamedAccess
) для обработки случая, когда получатель и объект поиска различаются.
Оптимизированный код стал еще более эффективным, когда мы переместили домашний объект из JSFunction
, где он хранился. Теперь он хранится в контексте класса, что позволяет TurboFan внедрить его в оптимизированный код как константу, когда это возможно.
Другие использования super
super
внутри методов литералов объектов работает так же, как внутри методов классов, и оптимизируется аналогично.
const myproto = {
__proto__: { 'x': 100 },
m() { return super.x; }
};
const o = { __proto__: myproto };
o.m(); // возвращает 100
Конечно, существуют ограниченные случаи, которые мы не оптимизировали. Например, запись свойств через super (super.x = ...
) не оптимизирована. Кроме того, использование миксинов делает место доступа мегаморфным, что приводит к более медленному доступу к свойствам через super:
function createMixin(base) {
class Mixin extends base {
m() { return super.m() + 1; }
// ^ это место доступа является мегаморфным
}
return Mixin;
}
class Base {
m() { return 0; }
}
const myClass = createMixin(
createMixin(
createMixin(
createMixin(
createMixin(Base)
)
)
)
);
(new myClass()).m();
Потребуется еще работа, чтобы обеспечить максимально быструю работу всех объектно-ориентированных паттернов — следите за дальнейшими оптимизациями!