0

Starting from an object literal {} or new Object(), is there any way to modify the instance such that it behaves like an Array exotic object?

Special behaviours of Array exotics:

const a = [];
console.log(a instanceof Array); // true
console.log(a.__proto__ === Array.prototype); // true
console.log(a.length); // 0

a.push(true);
console.log(a.length); // length increases to 1
console.log(Object.hasOwnProperty(a, 'length')); // false
console.log('length' in a); // true

a[1] = true;
console.log(a.length); // length increases to 2 to fit element [1]
console.log(a); // visualized as Array [ true, true ]
console.log(JSON.stringify(a)); // serialized as "[true,true]"

a.length = 0;
console.log(a[1]); // element [1] removed because length decreased to 0

We can achieve some Array behaviours by prototypically inheriting from Array:

const o = {};
Object.setPrototypeOf(o, Array.prototype);
console.log(o instanceof Array); // true
console.log(o.__proto__ === Array.prototype); // true
console.log(o.length); // 0

o.push(true);
console.log(o.length); // length increases to 1
console.log(Object.hasOwnProperty(o, 'length')); // false
console.log('length' in o); // true

o[1] = true;
console.log(o.length); // length remains at 1
console.log(o); // visualized as [true, 1: true]
console.log(JSON.stringify(o)); // serialized as {"0":true,"1":true,"length":1}

o.length = 0;
console.log(o[1]); // element [1] remains despite length increase

Clearly, Object.setPrototypeOf() gives us some of the functionality of Arrays, but not all its invariants are maintained and it's logged and serialized differently. Are there further tweaks we can do to our object instance to make it behave even more like an Array exotic object?

Creating an instance of a class which extends Array gives much better results:

const c = new class extends Array {};
console.log(c instanceof Array); // true
console.log(c.__proto__ === Array.prototype); // false
console.log(c.__proto__.__proto__ === Array.prototype); // true
console.log(c.length); // 0

c.push(true);
console.log(c.length); // length increases to 1
console.log(Object.hasOwnProperty(c, 'length')); // false
console.log('length' in c); // true

c[1] = true;
console.log(c.length); // length increases to 2 to fit element [1]
console.log(c); // visualized as Array [ true, true ]
console.log(JSON.stringify(c)); // serialized as "[true,true]"

c.length = 0;
console.log(c[1]); // element [1] removed because length decreased to 0

Is there something different about how prototypical inheritance works via extends versus via setPrototypeOf? Are there effects that we can apply to an Object instance in addition to setPrototypeOf to get similar results to what we've achieved with extends?

Qwerty
  • 19,992
  • 16
  • 88
  • 107
Birchlabs
  • 5,931
  • 4
  • 28
  • 41
  • You are asking multiple questions. You should focus on one question only. – trincot Feb 13 '21 at 19:58
  • Using `console.log` as a requirement is fuzzy. Different agents have different implementations of `console`, so I would not focus on that. Moreover, these agents host the JavaScript engine, so that they privileged access to the JavaScript internals. – trincot Feb 13 '21 at 20:03

2 Answers2

2

Are there further tweaks we can do to our object instance to make it behave even more like an Array exotic object?

No. The type of an object cannot be changed after the fact. It will never become a function, it will never become a proxy exotic object, it will never become an array exotic object. Array.isArray(obj) will never become true.

To create an array exotic object, you will have to construct it, using new Array, the array literal syntax, or Reflect.construct (which is what super does internally).

To create an object that behaves like an array but isn't one, you could use a Proxy.

To make an object that isn't an array have a .length behaving like that of an array, you could define a (very inefficient) getter/setter that maintains the invariants.

Bergi
  • 513,640
  • 108
  • 821
  • 1,164
1

Is there something different about how prototypical inheritance works via extends versus via setPrototypeOf?

Yes. setPrototypeOf changes the prototype after the fact, so the original value is a "normal" object. With extends Array, the value will actually be an instance of Array, because it is the result of calling the Array constructor.

Are there effects that we can apply to an Object instance in addition to setPrototypeOf to get similar results to what we've achieved with extends?

Yes and no.

Exotic array objects have different implementation for the internal slot [[DefineOwnProperty]] and you cannot directly change an internal slot from "user land" code.

But you could potentially use a Proxy to intercept property assignment and implement the same behavior as [[DefineOwnProperty]].

Felix Kling
  • 705,106
  • 160
  • 1,004
  • 1,072
  • marking this as the answer because it explains how the object's initial prototype is significant in determining which underlying exotic is used. – Birchlabs Feb 14 '21 at 12:54
  • 1
    @Birchlabs: It's the less the prototype, more the constructor. If you have `class X extends Array` then the `Array` constructor will be called first to create the actual instance value (of course the prototype will also be set). – Felix Kling Feb 14 '21 at 19:31