Почему мутирует [[прототип]] объекта плохого для производительности?
Из документов 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()