Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using __proto__ in object literals #2901

Closed
vsemozhetbyt opened this issue Aug 8, 2020 · 10 comments
Closed

Using __proto__ in object literals #2901

vsemozhetbyt opened this issue Aug 8, 2020 · 10 comments

Comments

@vsemozhetbyt
Copy link

vsemozhetbyt commented Aug 8, 2020

If I am not mistaken, using Object.setPrototypeOf() and __proto__ to change object prototype is considered deprecated as a "very slow operation". Also, IIRC, this can degrade the performance of objects in V8.

Also, it is usually not recommended to add properties to an object after creation as it can degrade the performance of objects in V8 as well (if I understand correctly explanations like this).

So, if I need to create an object with defined prototype AND properties, I have two options:

  1. Object.create() with both prototype and property descriptors arguments set. This is rather cumbersome way.

  2. Object literal with __proto__ and properties.

So the questions: are these two options equal with regard to performance? Or is the second way is just syntactical sugar for the same postponed Object.setPrototypeOf() operation internally? Has the second way any drawbacks? Are these drawbacks implementation-dependent or common (spec-dependent?)?

(cc maybe @bmeurer?)

@devsnek
Copy link
Member

devsnek commented Aug 8, 2020

Just reading through, it looks like they should be about the same. Also fwiw, modifying the prototype of an object right after you create it is not that bad. Modifying the prototype once the object has been passed around the codebase and such is what can cause slowdowns.

@tjcrowder
Copy link

tjcrowder commented Aug 8, 2020

FWIW:

  • Because it's part of Annex B, __proto__ in object literals (and its cousin Object.prototype.__proto__) are officially only defined for JavaScript engines in web browsers or if the engine "...is required to run the same legacy ECMAScript code that web browsers encounter." I think it would be hard to argue that the Node.js ecosystem doesn't fit the second criterion there, and I can't imagine __proto__ in object literals will disappear from V8 in Node.js. (But I have no special knowledge in this regard.)
  • The way __proto__ in object literals is specified does supposedly change the prototype of the object after it was created. That doesn't mean JavaScript engines can't optimize it, though. It's hard to see what would be in their way — the object being created can't be a proxy, and nothing else will ever have seen it, so nothing else can observe its prototype before it's changed. And the engine knows in advance, from parsing, whether __proto__ is there.
    • For the avoidance of doubt, the above does not apply to the cousin, the Object.prototype.__proto__ accessor property. You can't rely on the accessor being on any given object (the object may have no prototype, or may inherit from a prototype that doesn't inherit from Object.prototype), so using the accessor property is much chancier than using the __proto__ property name in an object literal.

Re your alternative, it's much less cumbersome to use Object.assign than the second argument to Object.create:

const proto = /*...*/;
const obj = Object.assign(Object.create(proto), {
    // Your properties here in the normal way, not as property descriptors
    example: 42
});
console.log(obj.example); // 42

But, that won't work for accessor properties.


On Twitter you asked if __proto__ was deprecated. Fundamentally, yes, it is; but that doesn't mean it's going away. Annex B has a big explanatory note at the beginning. It says

  • All of the language features and behaviours specified in this annex have one or more undesirable characteristics and in the absence of legacy usage would be removed from this specification.

and

  • These features are not considered part of the core ECMAScript language. Programmers should not use or assume the existence of these features and behaviours when writing new ECMAScript code.

and

  • ECMAScript implementations are discouraged from implementing these features unless the implementation is part of a web browser...

but that same sentence continues with

  • ...or is required to run the same legacy ECMAScript code that web browsers encounter.

and elsewhere in the note

  • However, the usage of these features by large numbers of existing web pages means that web browsers must continue to support them. The specifications in this annex define the requirements for interoperable implementations of these legacy features.

So...hopefully that gives you some input into your decision whether to use __proto__ in object literals, and a slightly less cumbersome alternative (Object.assign(Object.create(...), { ... }), except for accessor properties) if you decide not to.

Hope that helps!

@devsnek
Copy link
Member

devsnek commented Aug 8, 2020

@tjcrowder fwiw we actually are working on moving things out of annex b, for example tc39/ecma262#2125

@tjcrowder
Copy link

@devsnek - That's great news (in relation to this item), thanks!! I get leaving the accessor in Annex B, but I've really wanted __proto__ in literals (or some replacement for it) to be part of the main definition of object literals.

@vsemozhetbyt
Copy link
Author

Just for refs: Node.js lib uses some __proto__ in literals:

https://github.com/search?q=__proto__+repo%3Anodejs%2Fnode+path%3Alib&type=Code&ref=advsearch&l=&l=

@bergus
Copy link

bergus commented Aug 8, 2020

@devsnek From what I understood from the discussion in the linked issue, moving stuff out of annex b does not mean un-deprecating it, just making it easier available in the spec document (especially grammar productions). And I agree with @ljharb, __proto__ should stay dead forever. It's not even that useful in object initialisers, as it works only for plain objects there - not for functions or other exotic objects.

@ljharb
Copy link
Member

ljharb commented Aug 8, 2020

To be clear; I’m not advocating for deprecating proto support in object literals; the discussion there is about the legacy ways to define getters and setters.

It would break tons of code if node ever removed it, and i doubt there’s a huge performance impact any more from using 5-10 year old syntax.

@lukastaegert
Copy link

On a side-note, there are other considerations around __proto__, mostly security related, that caused Deno to remove __proto__: denoland/deno#4341. While it may not be important for your use-case, I found it very interesting to read through the corresponding issue for Node: nodejs/node#31951

Also note that at the moment, and after consulting with experts for the same performance considerations raised here, Rollup uses __proto__: null as a concise syntax to generate prototype-free objects. That means that there are likely quite a few libraries out there that implicitly use that feature.

@ljharb
Copy link
Member

ljharb commented Aug 8, 2020

@lukastaegert note that that’s about removing the accessor property on Object.protoype, the syntax in object literals has no security issues and i don’t think anyone has attempted to remove it.

@vsemozhetbyt
Copy link
Author

vsemozhetbyt commented Aug 9, 2020

A naive benchmark for creating and using an object with null prototype and properties (Win 10 x64, Node.js v15.0.0-v8-canary20200603908772e6f4, V8 8.5.93-node.7).

Code
'use strict';

let sum = 0;

{
  const label = 'warm up'.padEnd(25);
  let counter = 100000;
  console.time(label);

  while (counter--) {
    const object = { a1: 1, a2: 1, a3: 1, a4: 1, a5: 1 };
    sum += object.a1 + object.a2 + object.a3 + object.a4 + object.a5;
  }

  console.timeEnd(label);
}

{
  const label = '{ __proto__ }'.padEnd(25);
  let counter = 100000;
  console.time(label);

  while (counter--) {
    const object = { __proto__: null, b1: 1, b2: 1, b3: 1, b4: 1, b5: 1 };
    sum += object.b1 + object.b2 + object.b3 + object.b4 + object.b5;
  }

  console.timeEnd(label);
}

{
  const label = 'setPrototypeOf'.padEnd(25);
  let counter = 100000;
  console.time(label);

  while (counter--) {
    const object = Object.setPrototypeOf({ c1: 1, c2: 1, c3: 1, c4: 1, c5: 1 }, null);
    sum += object.c1 + object.c2 + object.c3 + object.c4 + object.c5;
  }

  console.timeEnd(label);
}

{
  const label = 'create + add'.padEnd(25);
  let counter = 100000;
  console.time(label);

  while (counter--) {
    const object = Object.create(null);
    object.d1 = 1, object.d2 = 1, object.d3 = 1, object.d4 = 1, object.d5 = 1;
    sum += object.d1 + object.d2 + object.d3 + object.d4 + object.d5;
  }

  console.timeEnd(label);
}

{
  const label = 'create + assign'.padEnd(25);
  let counter = 100000;
  console.time(label);

  while (counter--) {
    const object = Object.assign(Object.create(null), { e1: 1, e2: 1, e3: 1, e4: 1, e5: 1 });
    sum += object.e1 + object.e2 + object.e3 + object.e4 + object.e5;
  }

  console.timeEnd(label);
}

{
  const label = 'create with descriptors'.padEnd(25);
  let counter = 100000;
  console.time(label);

  while (counter--) {
    const object = Object.create(
      null,
      {
        f1: { value: 1, configurable: true, enumerable: true, writable: true },
        f2: { value: 1, configurable: true, enumerable: true, writable: true },
        f3: { value: 1, configurable: true, enumerable: true, writable: true },
        f4: { value: 1, configurable: true, enumerable: true, writable: true },
        f5: { value: 1, configurable: true, enumerable: true, writable: true },
      },
    );
    sum += object.f1 + object.f2 + object.f3 + object.f4 + object.f5;
  }

  console.timeEnd(label);
}

console.assert(sum === 100000 * 5 * 6);

Output:

warm up                  : 16.617ms
{ __proto__ }            : 41.176ms
setPrototypeOf           : 44.759ms
create + add             : 84.438ms
create + assign          : 107.115ms
create with descriptors  : 838.706ms

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants