2

I was really struggling to make a custom object that could inherit all the properties of an array, but would also behave the same as a normal instance, that is to say, instanceof and constructor would behave like you want them to. I had read that class declarations were just syntactic sugar, so I never turned to them for a solution (I knew very little about them).

Before I had my big breakthrough, I had created this abomination:

function arrayLike() {
    let al = [];

    //make obj.constructor work
    Object.defineProperty(al, 'constructor', {value: arrayLike}); 

    //add methods

    //make (obj instanceof arrayLike) == true
    return new Proxy(al, {
        getPrototypeOf() {
            return arrayLike.prototype;
        },
    })
}

//make (obj instanceof Array) == true
Reflect.setPrototypeOf(arrayLike.prototype, Array.prototype);

It just so happens that I saw a class example very close to what I wanted to do, and then discovered that it was perfectly made for the job:

class arrayLike extends Array {
    //add methods
}

Looking at it in Chrome DevToos, I can see that what I created does not have the same structure as this at all.

If class declarations truly are syntactic sugar, then how the hell do you create this object without it?

Mason
  • 738
  • 7
  • 18
  • 1
    I wouldn't say Class declarations are 100% syntactic sugar. Subclassing special objects with magic properties like `.length` on `Array` are made a lot easier in new browsers using Class syntax. Is there any reason you don't want to use Class syntax? You can always just transpile (and let the transpiler do all the work for you) if you're trying to run in an old environment. – jfriend00 Jun 23 '19 at 06:45
  • @jfriend00 No no, I'm totally fine with using class syntax. I'm asking this question purely out of curiosity. Because what I've read says this construction should be possible without it, and I want to know how. Or I want to know that what I've read is actually wrong and there really is new functionality added with class declarations. – Mason Jun 23 '19 at 06:52
  • 1
    You can read some about it here: https://davidtang.io/2017/09/21/subclassing-arrays-in-es2015.html and http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/ and http://speakingjs.com/es5/ch28.html. – jfriend00 Jun 23 '19 at 07:02
  • 1
    @Mason You can use `Reflect.construct` to create a subclass of `Array` without using `class` syntax. And yes, [they're not just sugar](https://stackoverflow.com/a/48036928/1048572). – Bergi Jun 23 '19 at 10:17

2 Answers2

2

Javascript is a language having a form of inheritance which is called prototypal inheritance.

The idea behind it is that given an object, it has an hidden property called prototype which is a reference to another object, which is said to be the prototype object.

This relationship is important when you ask the javascript engine to give you the value of an object property, let's call it foo just to fix the idea. The javascript engine will first check your object to see if it has a property called foo: if the property is defined on your object its value is returned and the search completes. If, otherwise, your object doesn't have a property called foo then its prototype object is searched and the same process is repeated again.

This procedure is repeated recursively until all the so called prototype chain has been explored. The root of the prototype chain is a built-in javascript object that you can reference with the expression Object.prototype and is the object from which all the other javascript objects derive. Notice that, if the foo property is missing in all the objects composing the entire prototype chain, then the value undefined is returned.

This is the real form of inheritance built into javascript and it's the business which really seats behind the ES6 class keywork which is a convenience which hides this mess and gives you the impression that javascript has a form of class inheritance (class inheritance is more widely known and most of programmers find it easier to think of than prototypal inheritance).

The bare minimum that you can do in order to take an object and decide that it should behave like an array is the following:

const myArray = [];
const myObject = { foo: "bar" }

Object.setPrototypeOf(myObject, myArray);

myObject.push("hello");
myObject.push("world");
console.log(myObject.length); // prints 2

This book is the best reference that I know for the javascript language. This is good too, but nowdays is a bit outdated and it's not as easy to follow along as the previous.

An example a bit more involved than the previous one can be implemented by using a function as a constructor. This is actually the ES5 old-school way to implement class-like inheritance, the thing that you did at the time of ES5 in order to mimic classes:

function SpecialArray(name) {
  this.name = name;
}

SpecialArray.prototype = []; 

// fix the constructor property mess (see the book linked above)
Object.defineProperty(SpecialArray.prototype, "constructor", {
  value: SpecialArray,
  enumerable: false,
  writable: true
});

SpecialArray.prototype.getSalutation = function() {
  return "Hello my name is " + this.name;
};

const mySpecialArray = new SpecialArray("enrico");

// you can call the methods and properties defined on Array.prototype
mySpecialArray.push("hello");
mySpecialArray.push("world");
console.log(mySpecialArray.length); // prints 2

// you can use the methods and properties defined on SpecialArray.prototype
console.log(mySpecialArray.name); // prints enrico
console.log(mySpecialArray.getSalutation()); // prints Hello my name is enrico

// important sanity checks to be sure that everything works as expected
console.log(mySpecialArray instanceof Array); // prints true
console.log(mySpecialArray instanceof SpecialArray); // prints true
console.log(mySpecialArray.constructor === SpecialArray); // prints true

// you can iterate over the special array content
for (item of mySpecialArray){
  console.log(item);
}

// you can read special array entries
console.log(mySpecialArray[1]); // prints world
Enrico Massone
  • 6,464
  • 1
  • 28
  • 56
  • 2
    This code example doesn't actually subclass the array such that you now have a prototype with both the array methods and with your own custom methods on it. It just makes a new object with the same Array prototype. I think you've a bit oversimplified your answer for what the OP was actually asking. – jfriend00 Jun 23 '19 at 07:22
  • 2
    Interesting, using `push` makes `length` actually work. I had tried this approach but unfortunately, if you do `myObject[0] = "hello"`, `myObject.length` still equals 0. – Mason Jun 23 '19 at 07:27
  • 1
    ^That's because the prototype inheritance model only do named property search up the chain. Read/write arbitrary properties like `myObject[whatever]` is out of its scope. For this case, you have to use `Proxy`. When you do `myObject.push` that push is an `array.prototype.push`, so it changes `length`. But when you do `myObject[0] = value`, that assignment is not assigning to array element, is just plain object property assignment, no array-related feature invovled, thus no change to `length`. – hackape Jun 23 '19 at 07:36
  • 1
    @jfriend00 thanks for your comment, I tried to improve my answer to better explain what is behind the class keyword. – Enrico Massone Jun 23 '19 at 07:37
  • 2
    Your new code still doesn't fix the issue I pointed out with the original. – Mason Jun 23 '19 at 07:47
  • @Mason this answer just took care only the prototype chain, but not the constructor. When you do `class Foo extends Array`, the constructor of `Foo` is implicitly setup as `constructor() { super(); /* sth else */ }`. – hackape Jun 23 '19 at 08:23
  • @Mason i think that as pointed out in another comment, the only way to achieve that is by using a Proxy, so that you can modify the length property manually (or maybe delegating it to the prototype object). – Enrico Massone Jun 23 '19 at 09:14
  • @Mason actually I have never tried to achieve the behavior you pointed out in your comment above. You can try to write some stuff with ES6 classes, transpile it down to ES5 with Babel and study the result. I think that the technique I showed above is enough to mimic class inheritance without ES6 magic – Enrico Massone Jun 23 '19 at 09:15
0

Edit: I study the transpiled code of babel and find that one extra touch is needed to correctly extends built-in class like Array, we need to wrap the Array constructor within a normal Wrapper function first, otherwise the prototype chain would be broken at construction.

function _wrapNativeSuper(Class) {
  _wrapNativeSuper = function _wrapNativeSuper(Class) {
    function Wrapper() {
      var instance = Class.apply(this, arguments)
      instance.__proto__ = this.__proto__.constructor.prototype;
      return instance;
    }
    Wrapper.prototype = Object.create(Class.prototype, {
      constructor: {
        value: Wrapper,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });
    Wrapper.__proto__ = Class;
    return Wrapper;
  };
  return _wrapNativeSuper(Class);
}

The class declaration syntax does 3 things.

  1. setup the constructor properly
  2. setup the prototype chain properly
  3. inherit static properties

So in order to replay what class Foo extends Array {} does in old school js, you need to do those 3 things accordingly.

// 0. wrap the native Array constructor
// this step is only required when extending built-in objects like Array
var _Array = _wrapNativeSuper(Array)

// 1. setup the constructor
function Foo() { return _Array.apply(this, arguments) }

// 2. setup prototype chain
function __dummy__() { this.constructor = Foo }
__dummy__.prototype = _Array.prototype
Foo.prototype = new __dummy__()

// 3. inherit static properties
Foo.__proto__ = _Array

Runnable example below:

function _wrapNativeSuper(Class) {
  _wrapNativeSuper = function _wrapNativeSuper(Class) {
    function Wrapper() {
      var instance = Class.apply(this, arguments)
      instance.__proto__ = this.__proto__.constructor.prototype;
      return instance;
    }
    Wrapper.prototype = Object.create(Class.prototype, {
      constructor: {
        value: Wrapper,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });
    Wrapper.__proto__ = Class;
    return Wrapper;
  };
  return _wrapNativeSuper(Class);
}

// 0. wrap the native Array constructor
// this step is only required when extending built-in objects like Array
var _Array = _wrapNativeSuper(Array)

// 1. setup the constructor
function Foo() { return _Array.apply(this, arguments) }

// 2. setup prototype chain
function __dummy__() { this.constructor = Foo }
__dummy__.prototype = _Array.prototype
Foo.prototype = new __dummy__()

// 3. inherit static properties
Foo.__proto__ = _Array


// test
var foo = new Foo;
console.log('instanceof?', foo instanceof Foo);

Foo.prototype.hi = function() { return 'hello' }
console.log('method?', foo.hi());
hackape
  • 18,643
  • 2
  • 29
  • 57
  • Where/how do you add new methods to Foo's prototype without adding them to `Array.prototype`? When I tried `Foo.prototype.add = function(x) {...}`, it did not work. I got "`x.add` is not a function" when I tried `x = new Foo(); x.add("A")`. – jfriend00 Jun 23 '19 at 08:29
  • @jfriend00 you're right. turns out array is quite unique. If you change `Array` in above code to sth else like `funciton Bar() {}`, then the code works. But looks like the `Array.apply(this, arguments)` part has broken the prototype chain at constructing. I need to mod the answer. – hackape Jun 23 '19 at 09:58
  • This does seem to work in my tests. Why do you use `Wrapper.__proto__ = Class;` instead of using `.setPrototypeOf()`? What a mess. Thank goodness for `class Foo extends Array {}`. – jfriend00 Jun 23 '19 at 16:28
  • Keep in mind that I think I've read that this kind of thing only works with the Array in newer browsers, that the magic `.length` property was not compatible with even this type of sub-classing in earlier generations of the browser. – jfriend00 Jun 24 '19 at 05:46
  • Node.js seems to give the helpful error of: "TypeError: clazz is not a constructor" when constructing a new instance of `Foo` – Vix Jul 02 '20 at 04:58