ES6 Class extends Array: workaround for ES5 Babel

2019-08-12 06:55发布

问题:

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.

回答1:

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


回答2:

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



回答3:

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



回答4:

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.