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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With