Search code examples
javascriptarraysobjectprototypal-inheritance

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?


Solution

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