Birchlabs
Birchlabs

Reputation: 8066

Can Javascript be tricked into believing an Object is an Array?

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?

Upvotes: 0

Views: 85

Answers (2)

Bergi
Bergi

Reputation: 665527

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.

Upvotes: 2

Felix Kling
Felix Kling

Reputation: 817198

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]].

Upvotes: 1

Related Questions