简体   繁体   中英

ES6 Class extends Array: workaround for ES5 Babel transpile

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.

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;
  }, {})
);

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)); 

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); 

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM