1

I have some ES6 class inherited from Array:

class Cache extends Array {
  add(item) {
    if(!item.doNotRemove)
      this.push(item)
  }
  printLast() {
    if(this.length > 0)
      console.log(this[this.length - 1].text)
  }
}

The following code works fine

const myCache = new Cache()
myCache.add({text: 'hello'})
myCache.add({text: 'world'})
myCache.add({text: '!!!', doNotRemove: true})
myCache.printLast() // world

But I can't transpile it to ES5 with Babel (I know there is an issue), and currently as a workaround I apply the following approach:

const CacheProto = {
  add(item) {
    if(!item.doNotRemove)
      this.push(item)
  },
  printLast() {
    if(this.length > 0)
      console.log(this[this.length - 1].text)
  }
}
function Cache() {
  return Object.assign(Object.create(Array.prototype), CacheProto)
}

This satisfies the code above (myCache = new Cache() etc). But as you can see, it's just an Array instance extending.

The question

Is it possible to have a workaround with original class? Of course, without extends Array. Is it possible to have add and printLast methods and all Array.prototype methods on the prototype chain, not on instance?

I have made a little plunker for possible research.

dhilt
  • 18,707
  • 8
  • 70
  • 85
  • 1
    Do you actually need the fancy behavior of `Array`? None of your code actually looks like extending `Array` is needed. – loganfsmyth Oct 23 '17 at 19:58
  • @loganfsmyth I actually do. It's just a cut sample. – dhilt Oct 23 '17 at 20:51
  • 1
    @dhilt: What exactly do you need? – Felix Kling Oct 23 '17 at 20:54
  • See [this post](https://stackoverflow.com/q/27985546/1048572) for the correct approach of extending arrays – Bergi Oct 23 '17 at 20:59
  • @felix-kling I need to have an access to `myCache.reduce()` and others Array.prototype methods together with `myCache.add()` and other Cache class methods. And it would be better to have `.add` not in instance but on prototype (current answers satisfy this requirement). But which is more important, I would like to have es6 class as a foundation of "cache" functionality. – dhilt Oct 23 '17 at 20:59
  • But do you actually want instances of `Cache` be used as arrays? Do you want "callers" to be able to do `cache[42] = 21;`? Which arrays methods do you want to expose? As I said in my answer, exposing them is not an issue. – Felix Kling Oct 23 '17 at 21:01
  • Actually I have faced with this bug today. And I wonder that why tsc or Babel doesn't give any warning or any error? This error looks like known error for a long time. – mustafa kemal tuna Jan 31 '22 at 08:23

4 Answers4

2

You only really have to extend Array if you want to the magic .length-affecting property assignment (arr[42] = 21;) behavior or most of the array methods. If you don't need that, using an array as internal data structure seems to be the simplest (and most compatible) solution:

class Cache {
  constructor() {
    this._data = [];
  }

  add(item) {
    if(!item.doNotRemove)
      this._data.push(item)
  }

  printLast() {
    if(this.length > 0)
      console.log(this._data[this._data.length - 1].text)
  }
}

You can easily expose .length and other methods.


An easy way to pull in multiple methods from Array.prototype would be:

['reduce', 'filter', 'find', ...].forEach(method => {
  Cache.prototype[method] = function(...args) {
    return this._data[method](...args);
  };
});

// Or if you want them to be non-enumerable
// (like they would if they were defined via `class` syntax)
Object.defineProperties(
  Cache.prototype,
  ['reduce', 'filter', 'find', ...].reduce((obj, method) => {
    obj[method] = {
      value: function(...args) { return this._data[method](...args); },
      enumerable: false,
      configurable: true,
      writeable: true,
    };
    return obj;
  }, {})
);
Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • Maybe you are right, and the best way is just to count all necessary Array.prototype methods as a Cache class methods in a way like `reduce(cb, init) { return this._data.reduce(cb, init); }`... but I wonder if this could be done programmatically – dhilt Oct 23 '17 at 21:12
  • Of course one problem here is things like `instanceOf` , `typeof` etc are not going to work. – Keith Oct 23 '17 at 21:19
  • @Keith: `instanceof Array` wouldn't work, yes. `typeof array` returns `"object"` anyway. – Felix Kling Oct 23 '17 at 21:20
  • Sorry, yes.. I meant `Array.isArray`.. Anyway, you get my point. – Keith Oct 23 '17 at 21:21
  • @FelixKling Thanks! The last question... about this white list. Could it be created programmatically? I know that `Object.keys(Array.prototype)` and other would not work. – dhilt Oct 23 '17 at 21:50
  • @dhilt: You could use `Object.getOwnPropertyDescriptors`, but that's probably not supported everywhere. If you actually look at `Array.prototype` there aren't that many methods and it's not like new methods get added all the time. I wouldn't worry about maintaining that list (for now). – Felix Kling Oct 23 '17 at 21:52
  • 1
    I got it! `Object.getOwnPropertyNames(Array.prototype)` – and it is more compatible against `Object.getOwnPropertyDescriptors`. Thanks a lot! – dhilt Oct 23 '17 at 21:54
1

You can manipulate the prototype directly using __proto__, it's also now kind of been standardised for backward compatibility reasons so should be safe to use.

More info here -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto

edit: Like @Bergi pointed out, using a shim with Object.setPrototypeOf would future proof this technique too.

function Cache() {
  var arr = [];
  arr.push.apply(arr, arguments);
  arr.__proto__ = Cache.prototype;
  //if using a shim, (better option).
  //Object.setPrototypeOf(arr, Cache.prototype);
  return arr;
}
Cache.prototype = new Array;
Cache.prototype.printLast = function () {
  if(this.length > 0)
    console.log(this[this.length - 1].text)
}
Cache.prototype.add = function (item) {
  if(!item.doNotRemove)
    this.push(item)
}



const myCache = new Cache()
myCache.add({text: 'hello'})
myCache.add({text: 'world'})
myCache.add({text: '!!!', doNotRemove: true})
myCache.printLast() // world

myCache.forEach(function (item) { console.log(item); });

console.log("is Array = " + Array.isArray(myCache));
Keith
  • 22,005
  • 2
  • 27
  • 44
  • It's standardized *and deprecated*. Please don't use `__proto__` - use `Object.setPrototypeOf` (and a shim for it when necessary). – Bergi Oct 23 '17 at 20:53
  • @Bergi Yes, using with a shim would be better. Showing the `__proto__` as that's what the shim is likely to use. – Keith Oct 23 '17 at 21:18
  • @Keith This surely works, but what about class syntax? I'd like to keep Cache functionality in a class. Felix Kling suggested [here](https://stackoverflow.com/a/46898347/3211932) to encapsulate the array into the Cache instance property (this._data = []) and then extend Cache prototype with Array prototype methods. But if it were possible to avoid this._data and provide a reverse extending, like it is in your variant but based on es6 class methods (and not on separate functions as it is in your try and not on some object methods like in my initial workaround)... – dhilt Oct 24 '17 at 01:03
  • @Keith Looks like I did it: https://stackoverflow.com/a/46900998/3211932. That would not have been possible without your post, thanks! – dhilt Oct 24 '17 at 01:52
0

This is a little hacky, but I'm pretty sure it does what you are looking for.

function Cache() {}
Cache.prototype = new Array;
Cache.prototype.add = function(item) {
  if (!item.doNotRemove) {
    this.push(item);
  }
};
Cache.prototype.printLast = function() {
  if (this.length <= 0) { return }
  console.log(this[this.length - 1].text);
}

let test = new Cache();
test.add({foo:'bar', text: 'cake' });
test.add({baz:'bat', doNotRemove: true});
test.add({free:'hugs', text: 'hello'});
test.printLast();
console.log(test);
D Lowther
  • 1,609
  • 1
  • 9
  • 16
  • 1
    [No, it doesn't](http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/) – Bergi Oct 23 '17 at 20:54
  • @Bergi On your link... Is it possible to have [this approach](http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/#wrappers_prototype_chain_injection) but keeping class syntax for base cache functionality? – dhilt Oct 23 '17 at 21:28
  • @dhilt Yes: `class CustomArray extends Array { constructor() { return Object.setPrototypeOf([], new.target.prototype); } … }` – Bergi Oct 23 '17 at 21:32
  • @Bergi Thanks for participation! Looks like I can't finalize your idea, may I ask you to fix [this new plunker](https://plnkr.co/edit/6Hf8i16niGECGYoH76Zy?p=preview)? – dhilt Oct 23 '17 at 21:40
  • @dhilt You forgot `extends Array`, otherwise `Cache.prototype` doesn't inherit the Array methods – Bergi Oct 23 '17 at 22:20
  • @Bergi After transpiling with Babel, there are no Cache class methods on new Cache instance (as it is in initial issue). – dhilt Oct 23 '17 at 22:44
  • @dhilt What does it transpile to? This seems to vary a lot between Babel versions and settings. I'll guess that `new.target` doesn't work right, you can try `Object.setPrototypeOf([], Cache.prototype)` instead. – Bergi Oct 23 '17 at 22:53
  • @Bergi `new.target` works fine, `extends Array` doesn't work. Babel can't simulate extending of native entities (like Array) provided via `class extends` syntax ([link](https://stackoverflow.com/a/33837088/3211932)). – dhilt Oct 23 '17 at 23:08
  • @dhilt It doesn't need to construct the array (which cannot be simulated indeed), we do that ourselves with `Object.setPrototypeOf([], …)`. All it needs to do is let `Cache.prototype` inherit from `Array.prototype`. – Bergi Oct 23 '17 at 23:11
  • @Bergi I understand prototype chaining approach, but I definitely can't apply it with `class Cache` syntax. `setPrototypeOf` doesn't work for prototype as a first argument, while calling `setPrototypeOf` with instance as a first argument just overrides the prototype of instance and so it could be Cache or Array, not Cache inherited from Array. – dhilt Oct 23 '17 at 23:29
  • 1
    @dhilt Yes, it definitely should be `[]` inheriting from `Cache.prototype` inheriting from `Array.prototype`. Otherwise it won't work. – Bergi Oct 23 '17 at 23:33
  • @Bergi According to your vision ([] --> Cache.prototype --> Array.prototype) I built a prototype chain, where `Cache.prototype` is being extended with necessary functionality taken from ES6 class prototype (`CacheProto.prototype`): https://stackoverflow.com/a/46900998/3211932. Do you find it appropriate? – dhilt Oct 24 '17 at 02:12
0

After some discussions here I was able to build a solution satisfied both of the requirements: keep original ES6 class as a state for new functionality and have this new functionality on the prototype as well as it is for the Array.prototype methods.

class CacheProto {
  add(item) {
    if(!item.doNotRemove)
      this.push(item)
  }
  printLast() {
    if(this.length > 0)
      console.log(this[this.length - 1].text)
  }
}

function Cache() {
  const instance = [];
  instance.push.apply(instance, arguments);
  Object.setPrototypeOf(instance, Cache.prototype);
  return instance;
}
Cache.prototype = Object.create(Array.prototype);
Object.getOwnPropertyNames(CacheProto.prototype).forEach(methodName =>
  Cache.prototype[methodName] = CacheProto.prototype[methodName]
);

The only difference between this CacheProto and the original class from the Question is that the CacheProto class does not extend the Array.

The end plunker could be obtained here. It contains this solution and all intermediate variants.

dhilt
  • 18,707
  • 8
  • 70
  • 85
  • There is no reason not to have `Cache` and `CacheProto` be the same object (constructor function). And there's no reason not to use `extends Array`, even if the default constructor doesn't work. – Bergi Oct 24 '17 at 02:43