Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why inheriting from Array is difficult to implement in ES5?

With prototype inheritance in ES5, it looks not trivial to inherit from Array and get the expected behavior, like updating .length automatically when adding item to Array (see below code). ES5 creates object of the derived function (MyArray) then pass it base type to do initialization, why is this model hard to get the expected behavior in this model?

ES6 changed the behavior and creating object in base constructor, and constructor of derived class initialize it after that (after call to super()), wondering why this solved the problem.

function MyArray(){}
MyArray.prototype = Object.create(Array.prototype);

var myArr = new MyArray();
myArr[0] = 'first';
console.log(myArr.length); // expect '1', but got '0' in output
like image 289
Thomson Avatar asked Dec 05 '16 03:12

Thomson


1 Answers

The key thing about Array is that a real array object is an Array Exotic Object. An exotic object is an object that has behavior that could not be achived using standard JS language features, though in ES6 Proxy allows much more ability for user code to create exotic-like objects.

When subclassing a constructor that returns an exotic object like Array, the subclassing method needs to be done in such a way that the object created is actually an exotic object. When you do something like

function ArraySubclass(){}
ArraySubclass.prototype = Object.create(Array.prototype);

then

(new ArraySubclass()) instanceof Array

because the prototype matches up, but the object returned by new ArraySubclass is just a normal object that happens to have Array.prototype in its prototype chain. But you'll notice that

Array.isArray(new ArraySubclass()); // false

because the object isn't a real exotic. In this case

new ArraySubclass()

is identical to doing

var obj = Object.create(ArraySubclass.prototype);
ArraySubclass.call(obj);

So in ES5 how do you extend Array? You need to create an exotic object, but you also need to ensure that the exotic object has your ArraySubclass.prototype object in its prototype chain. That is where ES5 hit it issues, because in vanilla ES5, there is no way to change an existing object's prototype. With the __proto__ extension that many engines added you could get the correct Array subclassing behavior with code like

var obj = new Array();
obj.__proto__ = ArraySubclass.prototype;
ArraySubclass.call(obj);

Say you wanted to generalize the pattern above, how would you do it?

function makeSubclass(baseConstructor, childConstructor){
    var obj = new baseConstructor();
    obj.__proto__ = childConstructor.prototype;
    return obj;
}

function ArraySubclass(){
    var arr = makeSubclass(Array, ArraySubclass); 

    // do initialization stuff and use 'arr' like 'this'

    return arr;
}
ArraySubclass.prototype = Object.create(Array.prototype);

so that works in ES5 + __proto__, but what about as things get more complicated? What if you want to subclass ArraySubclass? You'd have to be able to change the second the second parameter of makeSubclass. But how do we do that? What is the actual goal here? When you do something like

new ArraySubclass()

it is the value passed to new that we care about as that second parameter, and it is that constructor's prototype that should be getting passed along. There is no nice avenue in ES5 to accomplish this.

This is where ES6 classes have a benefit.

class ArraySubclass extends Array {
  constructor(){
    super();
  }
}

The key thing is that when super() runs, it knows that ArraySubclass is the child class. When super() calls new Array, it also passes along an extra hidden parameter that says "hey, when you create this array, set its prototype to ArraySubclass.prototype. If there are many levels of inheritance, it will pass along the child-most prototype so that the returned exotic object is a real exotic while also making sure it has the correct prototype.

Not only does this mean that things are constructed properly, but it means that engines can create the object with the correct prototype value up front. Mutating an object's __proto__ value after creating is a well-known deoptimization point because of the ways engines process and track objects.

like image 194
loganfsmyth Avatar answered Sep 21 '22 17:09

loganfsmyth