Прототипы (как в func.prototype) используются для имитации классов. Обычно они содержат все методы класса, их __proto__ является «суперклассом», и они не меняются после настройки.

Производительность настройки прототипов важна для времени запуска приложений, поскольку часто бывает, когда настраивается вся иерархия классов.

Переход формы объекта

Основной способ кодирования объектов - отделение скрытого класса (описание) от объекта (содержимого). Когда создаются новые объекты, они создаются с использованием того же начального скрытого класса, что и предыдущие объекты из того же конструктора. По мере добавления свойств объекты переходят из скрытого класса в скрытый, обычно следуя предыдущим переходам в так называемом «дереве переходов». Например, если у нас есть следующий конструктор:

Если мы создаем экземпляр объекта var o = new C(), он начинает с использования начального скрытого класса M0, присоединенного к C без каких-либо свойств. Когда добавляется a, мы переходим от этого скрытого класса к новому скрытому классу M1, который описывает свойство a. А затем, когда мы добавляем b, мы переходим к новому скрытому классу, который описывает как a, так и b.

Если мы сейчас создадим экземпляр второго объекта var o2 = new C(), он будет следовать этим переходам. Он начинается с M0, затем переходит к M1 и, наконец, M2, когда добавляются a и b.

У этого есть 3 основных преимущества:

  1. Несмотря на то, что настройка первого объекта довольно дорога и требует от нас создания всех скрытых классов и переходов, настройка последующих объектов выполняется очень быстро.
  2. Полученные объекты меньше, чем были бы полные словари. Нам нужно хранить только значения в объекте, а не также информацию о свойстве (например, имя).
  3. Теперь у нас есть описание формы, которое мы можем использовать как во встроенных кэшах, так и в оптимизированном коде для быстрого доступа к объектам аналогичной формы в одном месте.

Это очень хорошо работает для форм объектов, которые, как ожидается, будут очень часто повторяться. То же самое происходит внутри объектных литералов: {a:1, b:2} также внутренне будет иметь скрытые классы M0, M1 и M2.

Об этом много написано; см., например, следующее объяснение Ларса Бака:

Прототипы - особые снежинки.

В отличие от экземпляров, созданных из обычных конструкторов, прототипы обычно представляют собой уникальные объекты, не имеющие общей формы с другими объектами. Это меняет расчет для всех трех пунктов выше:

  1. Обычно нет объекта, который выиграет от кэшированных переходов, и настройка дерева переходов - это просто ненужные затраты.
  2. Нет ничего, что могло бы компенсировать накладные расходы на память из-за создания всех переходных скрытых классов. Фактически, до того, как мы это изменили, мы обычно видели большую часть скрытых классов, используемых для отдельных прототипов.
  3. Загрузка из прототипа на самом деле не так распространена, как использование его через цепочку прототипов. Если мы загружаемся из объекта-прототипа через цепочку прототипов, мы не отправляем скрытый класс прототипа, и нам нужен другой способ проверить, действительно ли он действителен.

Чтобы оптимизировать прототипы, V8 отслеживает их форму иначе, чем обычные переходные объекты. Вместо того, чтобы отслеживать дерево переходов, мы адаптируем скрытый класс к объекту-прототипу и всегда делаем это быстро. Например, даже если delete object.property обычно переводит объекты в «медленное» состояние, это не относится к прототипам. Мы всегда будем хранить их в кэше (с некоторыми оговорками, над которыми мы работаем).

Мы также изменили способ создания прототипов. У прототипов есть 2 основных этапа: настройка и использование. Прототипы на этапе настройки кодируются как объекты словаря. Сохранение прототипов в этом состоянии происходит очень быстро, и не обязательно входить в среду выполнения C ++ (переход границы, что довольно дорого). Это огромное улучшение по сравнению с начальной настройкой объекта, которая должна создать переходящий скрытый класс; частично потому, что это нужно делать во время выполнения C ++.

Любой прямой доступ к прототипу или доступ через цепочку прототипов переведет его в состояние use, гарантируя, что с этого момента все такие обращения будут быстрыми. И, как указывалось выше, даже если вы удалите свойство, мы сделаем это снова быстро.

Это прототип?

Чтобы получить выгоду от любого из вышеперечисленного, нам нужно знать, что объект на самом деле используется в качестве прототипа. Из-за природы JS очень сложно анализировать вашу программу во время компиляции. По этой причине мы даже не пытаемся выяснить при создании объекта, станет ли что-то прототипом в данный момент (конечно, со временем это может измениться…). Как только мы увидим объект, установленный в качестве прототипа, мы помечаем его как таковой. Например, если вы это сделаете:

мы не знаем, что o используется в качестве прототипа, до самого конца. Мы создадим объект обычным довольно дорогостоящим способом создания переходов. Однако после установки он помечается как прототип и переходит в состояние настройки. И в фазу использования , как только вы начнете его использовать.

Если вместо этого вы сделаете следующее, мы узнаем, что o является прототипом до того, как будут добавлены какие-либо свойства. Он перейдет на этап настройки перед добавлением свойств, и это будет намного быстрее:

Обратите внимание, что также можно просто использовать var o = func.prototype, поскольку func.prototype всегда создается как нечто, знающее, что это прототип; очевидно ;-).

Как настроить прототипы

Если вы настроите свой прототип следующим образом, вы получите то преимущество, что мы знаем, что func.prototype является прототипом до добавления методов:

Хотя это уже неплохо, на самом деле нам нужно загрузить func.prototype для каждого метода. Несмотря на то, что мы недавно дополнительно оптимизировали, в частности, func.prototype нагрузки, они не нужны и будут хуже для производительности и использования памяти, чем просто прямой доступ к локальным переменным.

Короче говоря, идеальный способ настройки прототипов выглядит следующим образом: