babel: Native extends breaks HTMLELement, Array, and others

Current Babel transform, when it comes to call the parent

function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  }
  return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

It’s a way too poor implementation.

If we take the current basic ES6 compat syntax:

class List extends Array {}

We’ll realize babel does a bad job.

var l = new List();
l instanceof List; // false

The reason is simple: Babel replace the returns and exit, without caring about userland expectations.

This is how above basic extend should desugar:

function List() {
  return Object.setPrototypeOf(
    Array.apply(this, arguments),
    List.prototype
  );
}

It’s a very ad-hoc case for the initial example that currently fails, but it’s good enough to understand that inheriting the prototype is the least of the problem.

Indeed, we have 3 ways to do that within a transpiled code:

// losing the initial prototype
// for polyfilled ES5+ or lower
List.prototype = Object.create(
  Array.prototype,
  {constructor: {
    configurable: true,
    writable: true,
    value: List.prototype
  }}
);

// using a cleaner way
// for ES5+ compliant targets
Object.setPrototypeOf(
  List.prototype,
  Array.prototype
);

// using a cleaner way
// that works well with
// partially polyfilled .setPrototypeOf
List.prototype = Object.setPrototypeOf(
  List.prototype,
  Array.prototype
);

Solved the inheritance bit, considering Babel also set the prototype of each constructor, we need to address cases where a super call might “upgrade” the current context, like it is for HTMLELement or exotic native objects.

// native ES6 static example
class List extends Array {
  constructor(a, b, c) {
    super(a, b);
    this.push(c);
  }
}

Above case should desugar to something like the follwoing:

function List(a, b, c) {
  // the super bit
  var self = Object.setPrototypeOf(
    Array.call(this, a, b),
    List.prototype
  );
  // the rest with swapped context
  self.push(c);
  // at the end
  return self;
}

which is also ad-hoc example code for the previous example.

Considering a transpiler will get the arguments part easily right, this is how previous case could be generically transpiled for arguments used in both constructors:

// to make it as generic as possible
function Child() {

  // the super call bit desugar to ...
  var
    instance = Object.getPrototypeOf(Child)
        .apply(this, arguments),
    type = instance ? typeof instance : '',
    // if Parent overwrote its default return
    self = (type === 'object' || type === 'function') ?
      // upgrade the instance to reflect Child constructor
      Object.setPrototypeOf(instance, Child.prototype) :
      // otherwise use the current context as is
      this
  ;

  // at this point, the rest of the constructor
  // should use `self` instead of `this`
  self.push(c);

  // and return `self` reference at the end
  return self;
}

The last problem is that modern syntax would use Reflect to create any sort of object, instead of old, ES3 friendly, .call or .apply way.

Following a past, present, and even future proof approach:

var
  reConstruct = typeof Reflect === 'object' ?
    Reflect.construct :
    function (Parent, args, Child) {
      return Parent.apply(this, args);
    }
;

function Child() {

  // the super call bit
  var
    instance = reConstruct.call(
      this,
      Object.getPrototypeOf(Child),
      arguments,
      Child
    ),
    type = instance ? typeof instance : '',
    self = (type === 'object' || type === 'function') ?
      Object.setPrototypeOf(instance, Child.prototype) :
      this
  ;

  // the rest of the constructor body
  self.push(c);

  // at the end or instead of empty returns
  return self;
}

Above solution would work with userland, exotic, or DOM constructors, and in both native and transpiled engines.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 18
  • Comments: 40 (10 by maintainers)

Commits related to this issue

Most upvoted comments

@smalluban yes, it’s related, and Custom Elements extends are indeed the reason I came here.

Right … so, I’ve done some test and played around with the AST explorer. I’d like to announce here my new, and first ever, babel-plugin-transform-builtin-classes transformer.

It is based on the following HELPER:

var HELPER = (function (O) {
  var
    gOPD = O.getOwnPropertyDescriptor,
    gPO = O.getPrototypeOf || function (o) { return o.__proto__; },
    sPO = O.setPrototypeOf || function (o, p) { o.__proto__ = p; return o; },
    construct = typeof Reflect === 'object' ?
      Reflect.construct :
      function (Parent, args, Class) {
        var Constructor, a = [null];
        a.push.apply(a, args);
        Constructor = Parent.bind.apply(Parent, a);
        return sPO(new Constructor, Class.prototype);
      }
  ;
  return function fixBabelExtend(Class) {
    var Parent = gPO(Class);
    return sPO(
      Class,
      sPO(
        function Super() {
          return construct(Parent, arguments, Class);
        },
        Parent
      )
    );
  };
}(Object));

And it uses .babelrc to understand which native/global class you’d like to extend.

The repository provides also a default one you could copy and paste around: https://github.com/WebReflection/babel-plugin-transform-builtin-classes/blob/master/.babelrc

It has all the native classes exported in the global window in Chrome, but you could use just few of them.

Everything else will be untouched.

You might want to use this together with the preset-es2015 or bring in at least the classes related bit.

If you’d like to have this mainstream/by default please let me know what should I do, thanks.

To whom this might concern, I’ve published a 40 LOC module that patches only classes that needs to be patched at distance.

https://github.com/WebReflection/fix-babel-class

Whenever you want to fix the issue you invoke fixBabelClass(YourClassThatExtendsANativeOne).

That’s pretty much it.

I am not sure if this is directly connected to this issue, but Babel class transpilation breaks using native customElements API. HTMLElement constructor has to be called with new operator, so in transpiled code Reflect has to be used.

I have found an article The JS super (and transpilers) problem with possible solution (last code example).

I created two codepen examples.

Babel transpiled class (http://codepen.io/smalluban/pen/wzgoON?editors=0012):

  • Chrome 54: "Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function."
  • Firefox 48: "created element"
  • IE 11: "created element"

Firefox/IE uses customElements polyfill, so it works well - there is no problem with target.new, but Chrome requires that.

Custom transpiled class (http://codepen.io/smalluban/pen/JREbaW?editors=0012):

  • Chrome 54: "created element"
  • Firefox 48: "created element"
  • IE 11: "created element"

In this example if Reflect API is present, it’s used to call parent constructor, and then creating custom element works in both native and polyfilled versions.

I am not sure how this will work with “normal” class definition where special behavior is not required.

FYI the plugin has been updated after a fix related to N level inheritance. There are also tests now to validate it works on NodeJS. https://github.com/WebReflection/babel-plugin-transform-builtin-classes

Custom Elements also seem to work without any issue with or without my polyfill.

I believe all my tests are using the Reflect.construct available in all targets I am checking, but if you could confirm it’s OK even on older browsers/engines that’d be awesome.

Best Regards

@zloirock So is there a fix coming for this? It basically makes web components / custom elements unusable. 😢

It looks like we all agree on this point: https://github.com/w3c/webcomponents/issues/587#issuecomment-254348659

Not even browser vendors developers can workaround the current broken state when it comes to super.

I won’t have time soon to solve this issue, I’d like to understand if it has any priority though so at least I can better reply to people asking me why my polyfill is broken ( when the issue is them using Babel 😦 )

Thanks for any sort of ETA / outcome

Thanks everyone for making this happen - and of course @WebReflection in particular for figuring this out in the first place and staying on the ball.

For users who aren’t familiar with Babel’s internals, AFAICT the fix in #7020 will be included by default with the upcoming Babel v7.0? (If you’re using something like @babel/preset-es2015 or @babel/preset-env, that is - at least those presets seem to depend on @babel/plugin-transform-classes.)

We should at least add a link to the babel-plugin-transform-classes which suggests the users your plugin. Anyway, if you are ok with it, I can work on merging the two plugins.

@zloirock have you seen my proposed changes ? https://github.com/babel/babel/issues/4480#issuecomment-256619501

You can compare them directly with the current output generated by

class List extends Array {
  constructor(a, b, c) {
    super(a, b);
    this.push(c);
  }
}

Current

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var List = function (_Array) {
  _inherits(List, _Array);

  function List(a, b, c) {
    _classCallCheck(this, List);

    var _this = _possibleConstructorReturn(this, (List.__proto__ || Object.getPrototypeOf(List)).call(this, a, b));

    _this.push(c);
    return _this;
  }

  return List;
}(Array);

Improved

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var List = function (_Array) {
  _inherits(List, _Array);

  var _retrieveThis = Object.getOwnPropertyDescriptor(_Array, 'prototype').writable ?
    // user defined class or function
    function (Parent, a, b) {
        return _possibleConstructorReturn(this, Parent.call(this, a, b));
    } :
    // native class
    function (Parent, a, b) {
        // eventually it needs a Reflect and Object.setPrototypeOf polyfill upfront
        return Object.setPrototypeOf(Reflect.construct(Parent, [a, b], List), List.prototype);
    };

  function List(a, b, c) {
    _classCallCheck(this, List);

    var _this = _retrieveThis.call(this, (List.__proto__ || Object.getPrototypeOf(List)), a, b);

    _this.push(c);
    return _this;
  }

  return List;
}(Array);

The only overhead for non native constructors is during class definition time (so once per Application lifecycle) through:

Object.getOwnPropertyDescriptor(_Array, 'prototype').writable

Hopefully, that’s practically irrelevant for real-world projects.

console.time('gOPD');
for(let i = 0; i < 1000; i++)
  Object.getOwnPropertyDescriptor(Array, 'prototype').writable;
console.timeEnd('gOPD');
// gOPD: 0.582ms for thousand classes

@stephanbureau remember in V1 there are changes so that you need to specify upfront attributes to listen to.

class MyDom extends HTMLElement {
  static get observedAttributes() {
    return ['country'];
  }
  attributeChangedCallback(name, oldValue, newValue) {
    // react to changes for name
    alert(name + ':' + newValue);
  }
}

var md = new MyDom();
md.setAttribute('test', 'nope');
md.setAttribute('country', 'UK'); // country: UK

Hi,

As a workaround on my side, I use github:WebReflection/document-register-element as polyfill to import. Then, the only issue I still encountered is attributeChangedCallback which is not called when changing attributes values, not perfect I know. connectedCallback or disconnectedCallback methods are correctly called though.

For info, I am using jspm 0.17.0-beta.31 with babel 6.16.0.

Here’s an example with a basic class:

in ES6

class Test extends HTMLElement {
    static get TAG() {
        return 'my-test';
    }
    connectedCallback() {
        this.innerHTML = 'content';
    }
}

Transpilled with babel runtime

Test = function (_HTMLElement) {
    _inherits(Test, _HTMLElement);

    function Test () {
        _classCallCheck(this, Test);

        return _possibleConstructorReturn(this, Object.getPrototypeOf(Test).apply(this, arguments));
    }

    _createClass(Test, [{
        key: 'connectedCallback',
        value: function connectedCallback() {
            this.innerHTML = 'content';
        }
    }], [{
        key: 'TAG',
        get: function get() {
            return 'my-test';
        }
    }]);

    return Test;
}(HTMLElement);

Hope it helps a bit.