Почему мутирует [[прототип]] объекта плохого для производительности?

Из документов MDN для стандартной setPrototypeOf функции, а также нестандартного __proto__ свойства:

Мутация [[Prototype]] объекта, независимо от того, как это выполняется, сильно обескуражена, потому что она очень медленная и неизбежно замедляет последующее выполнение в современных реализациях JavaScript.

Использование Function.prototype для добавления свойств - это способ добавления функций-членов в классы javascript. Затем, как показано ниже:

function Foo(){}
function bar(){}

var foo = new Foo();

// This is bad: 
//foo.__proto__.bar = bar;

// But this is okay
Foo.prototype.bar = bar;

// Both cause this to be true: 
console.log(foo.__proto__.bar == bar); // true

Почему foo.__proto__.bar = bar; плохо? Если его плохое не Foo.prototype.bar = bar; так же плохо?

Тогда почему это предупреждение: оно очень медленно и неизбежно замедляет последующее выполнение в современных реализациях JavaScript. Конечно, Foo.prototype.bar = bar; не так уж плохо.

Обновление Возможно, благодаря мутации они означали переназначение. См. Принятый ответ.

Ответ 1

// This is bad: 
//foo.__proto__.bar = bar;

// But this is okay
Foo.prototype.bar = bar;

Нет. Оба делают то же самое (как foo.__proto__ === Foo.prototype), и оба они в порядке. Они просто создают свойство bar объекта Object.getPrototypeOf(foo).

То, о чем говорится в заявлении, это присвоение самому свойству __proto__:

function Employee() {}
var fred = new Employee();

// Assign a new object to __proto__
fred.__proto__ = Object.prototype;
// Or equally:
Object.setPrototypeOf(fred, Object.prototype);

Предупреждение на Object.prototype странице более подробно:

Мутация [[Prototype]] объекта по характеру, как современные JavaScript-движки оптимизируют доступ к ресурсам, очень медленная операция

Они просто заявляют, что изменение цепи прототипов уже существующего объекта убивает оптимизацию. Вместо этого вы должны создать новый объект с другой цепочкой прототипов через Object.create().

Я не мог найти явную ссылку, но если мы рассмотрим, как реализованы скрытые классы V8, мы можем видеть, что может продолжаться Вот. При изменении последовательности прототипов объекта изменяется его внутренний тип - он не просто становится подклассом, как при добавлении свойства, но полностью заменяется. Это означает, что все исправления поиска свойств очищаются, и предварительно скомпилированный код нужно будет отбросить. Или он просто возвращается к неоптимизированному коду.

Некоторые заметные цитаты:

  • Брендан Эйх (вы его знаете) сказал

    Writable __proto__ - это огромная боль для реализации (требуется сериализация для проверки цикла), и она создает всевозможные угрозы путаницы типов.

  • Брайан Хакетт (Mozilla) сказал:

    Разрешающие скрипты мутировать прототип практически любого объекта затрудняют рассуждение о поведении script и делают более сложным и сложным внедрение VM, JIT и анализа. Вывод типа имеет несколько ошибок из-за изменчивого __proto__ и не может поддерживать несколько желательных инвариантов из-за этой функции (т.е. "наборы типов содержат все возможные типы объектов, которые могут реализоваться для var/property" и "JSFunctions имеют типы, которые также являются функциями",).

  • Джефф Уолден сказал:

    Прототипная мутация после создания, с ее неустойчивой дестабилизацией производительности и воздействием на прокси и [[SetInheritance]]

  • Эрик Корри (Google) сказал:

    Я не ожидаю больших выигрышей в производительности от того, чтобы сделать nono-overwritable. В неоптимизированном коде вам нужно проверить цепочку прототипов, если объекты прототипа (а не их личность) были изменены. В случае оптимизированного кода вы можете вернуться к неоптимизированному коду, если кто-то пишет в proto. Таким образом, это не принесло бы такой большой разницы, по крайней мере, в V8-Crankshaft.

  • Эрик Фауст (Mozilla) сказал

    Когда вы устанавливаете __proto__, вы не только разрушаете любые шансы, которые вы могли бы иметь для будущих оптимизаций от Ion на этом объекте, но также заставляете движок обходить все остальные части типа вывода (информация о функции возвращаемые значения или значения свойств), которые думают, что знают об этом объекте, и сообщают им не делать много предположений, что предполагает дальнейшую деоптимизацию и, возможно, недействительность существующего jitcode.
    Изменение прототипа объекта в середине исполнения на самом деле является неприятным кувалдой, и единственный способ избежать ошибок - это безопасно, но безопасно медленнее.

Ответ 2

__proto__/setPrototypeOf не совпадают с назначением прототипа объекта. Например, когда у вас есть функция/объект с назначенными ей членами:

function Constructor(){
    if (!(this instanceof Constructor)){
        return new Constructor();
    } 
}

Constructor.data = 1;

Constructor.staticMember = function(){
    return this.data;
}

Constructor.prototype.instanceMember = function(){
    return this.constructor.data;
}

Constructor.prototype.constructor = Constructor;

// By doing the following, you are almost doing the same as assigning to 
// __proto__, but actually not the same :P
var newObj = Object.create(Constructor);// BUT newObj is now an object and not a 
// function like !!!Constructor!!! 
// (typeof newObj === 'object' !== typeof Constructor === 'function'), and you 
// lost the ability to instantiate it, "new newObj" returns not a constructor, 
// you have .prototype but can't use it. 
newObj = Object.create(Constructor.prototype); 
// now you have access to newObj.instanceMember 
// but staticMember is not available. newObj instanceof Constructor is true

// we can use a function like the original constructor to retain 
// functionality, like self invoking it newObj(), accessing static 
// members, etc, which isn't possible with Object.create
var newObj = function(){
    if (!(this instanceof newObj)){   
        return new newObj();
    }
}; 
newObj.__proto__ = Constructor;
newObj.prototype.__proto__ = Constructor.prototype;
newObj.data = 2;

(new newObj()).instanceMember(); //2
newObj().instanceMember(); // 2
newObj.staticMember(); // 2
newObj() instanceof Constructor; // is true
Constructor.staticMember(); // 1

Кажется, что все фокусируются только на прототипе и забывают, что функции могут иметь назначенные ему члены и создаваться после мутации. В настоящее время нет другого способа сделать это без использования __proto__/setPrototypeOf. Едва ли кто-либо использует конструктор без возможности наследования от родительской конструкторской функции, а Object.create не может служить.

И плюс, что два вызова Object.create, которые в настоящий момент не имеют ошибок в V8 (оба браузера и Node), что делает __proto__ более жизнеспособным выбором

Ответ 3

Да .prototype = так же плохо, поэтому формулировка "независимо от того, как это делается". prototype - псевдообъект для расширения функциональности на уровне класса. Его динамический характер замедляет выполнение script. С другой стороны, добавление функции на уровне экземпляра несет гораздо меньшие накладные расходы.

Ответ 4

Ниже приведен пример использования node v6.11.1

NormalClass: нормальный класс с прототипом без редактирования

PrototypeEdited: класс с отредактированным прототипом (добавлена ​​функция test())

PrototypeReference: класс с добавленной функцией прототипа test(), который ссылается на внешнюю переменную

Результаты:

NormalClass x 71,743,432 ops/sec ±2.28% (75 runs sampled)
PrototypeEdited x 73,433,637 ops/sec ±1.44% (75 runs sampled)
PrototypeReference x 71,337,583 ops/sec ±1.91% (74 runs sampled)

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

Источник:

const Benchmark = require('benchmark')
class NormalClass {
  constructor () {
    this.cat = 0
  }
  test () {
    this.cat = 1
  }
}
class PrototypeEdited {
  constructor () {
    this.cat = 0
  }
}
PrototypeEdited.prototype.test = function () {
  this.cat = 0
}

class PrototypeReference {
  constructor () {
    this.cat = 0
  }
}
var catRef = 5
PrototypeReference.prototype.test = function () {
  this.cat = catRef
}
function normalClass () {
  var tmp = new NormalClass()
  tmp.test()
}
function prototypeEdited () {
  var tmp = new PrototypeEdited()
  tmp.test()
}
function prototypeReference () {
  var tmp = new PrototypeReference()
  tmp.test()
}
var suite = new Benchmark.Suite()
suite.add('NormalClass', normalClass)
.add('PrototypeEdited', prototypeEdited)
.add('PrototypeReference', prototypeReference)
.on('cycle', function (event) {
  console.log(String(event.target))
})
.run()