0

I've asked a similar question yesterday and it was marked as duplicate of
ES6: call class constructor without new keyword

As the above linked document does not give a full answer, I now asked the same question a bit different today ... and provide a possible solution below:

How do I have to implement the ES5 function "extend" (it should just extend a class) in the following demo to also work with ES2015 classes without getting an "Class constructor baseClass cannot be invoked without 'new'" error?

[Edit: The function "extend" will be part of a (transpiled) ES5 library. This library itself may then later be used by someone else who uses either ES5 or >= ES2015 - both cases should work]

// ES5 function (similar to: constr => class extends constr {})
function extend(constr) {
  var ret = function () {
    constr.apply(null, arguments)
  }

  ret.prototype = Object.create(constr.prototype)

  return ret
}


// ES2015 code
const
  baseClass = class {},
  extendedClass = extend(baseClass)

new extendedClass() // <- Error: Class constructor baseClass cannot be invoked without 'new'

See: https://jsfiddle.net/kjf7cheo/

One solution could look like follows (=> using "new Function(...)"). Is there anything wrong/insecure/to be considered with this solution?

var extend = null

if (typeof Proxy === 'function') { // check for ECMAScript version >= 2015
  try {
    extend =
      new Function('baseClass', 'return class extends baseClass {}')
  } catch (e) {
    // ignore
  }
}

if (!extend) {
  extend = function (baseClass) {
    var ret = function () {
      baseClass.apply(this, arguments)
    }

    ret.prototype = Object.create(baseClass.prototype)

    return ret
  }
}

// Result: function "extend"
Natasha
  • 421
  • 1
  • 3
  • 10
  • If you are dealing with classes already why mix constructor types? – MinusFour Aug 23 '18 at 02:27
  • The above code will be part of a (transpiled) ES5 library. This library may then later be used by someone else who uses either ES5 or >= ES2015 – Natasha Aug 23 '18 at 02:31
  • I don't get it how that makes any difference? If you are using a transpiler then you can do classes, if you are not using a transpiler then it won't be a ES5 environment because you have classes. Unless you want to use this to extend transpiled code? – MinusFour Aug 23 '18 at 02:49
  • Let's assume the library has a function decorate(baseClass) that uses "extend" internally that can be used either as (ES2015) decorate(class { ... }) or as (ES5) decorate(someES5Constructor) - both should work – Natasha Aug 23 '18 at 02:54

2 Answers2

1

I'd try using Reflect.construct for this:

let extend = function (constr) {
    let ret;
    if(Reflect && Reflect.construct){
        ret = function(...args){
            let inst = Reflect.construct(constr, args, eval('new.target'));
            return inst;
        }
    } else {
        ret = function () {
        constr.apply(this, arguments)
        }
    }
    ret.prototype = Object.create(constr.prototype);
    ret.prototype.constructor = ret;
    return ret;
}

The eval line just makes it so you don't get a syntax error when you transpile it to ES5. It's necessary if you want to extend the constructors you create with extend otherwise the prototypes will be lost.

MinusFour
  • 13,913
  • 3
  • 30
  • 39
  • Thx for your answer. The problem with this solution is that to following code will output "false" instead of "true": class A {}; const B = extend(A); console.log(new B instanceof B) – Natasha Aug 23 '18 at 03:48
  • @MinusFor: I think it must be "Reflect.construct(constr, args, derived)" in your code snippet above, then it seems to work, correct? – Natasha Aug 23 '18 at 04:05
  • @Natasha oh I was using it incorrectly, it needs an array like object not spreading the arguments. – MinusFour Aug 23 '18 at 04:21
  • Note, you do need `new.target` here so you can actually set the prototype correctly... however that's not valid syntax for ES5. You could wrap it on an `eval` line though – MinusFour Aug 23 '18 at 04:27
  • @MinusFor Thanks, your latest version (=> eval('new.target')) is working great. But now we have the problem that using "eval" is bad, even worse than using "new Function(...)" as I have done in my solution above. Do you have any idea how to replace that "eval('new.target')" thing with something else? – Natasha Aug 23 '18 at 05:43
  • @MinuFour Here's a little demo - maybe that helps: https://jsbin.com/fogolidome/edit?js,console – Natasha Aug 23 '18 at 05:49
0

I think this could be a solution without using "eval(...)" or "new Function(...)" ... what do you think?

Find a demo here: https://jsbin.com/vupefasepa/edit?js,console

var hasReflection =
  typeof Reflect === 'object'
    && Reflect
    && typeof Reflect.construct === 'function'

function extend(baseClass) {
  var ret = function () {
    if (!(this instanceof ret)) {
      throw new Error("Class constructor cannot be invoked without 'new'")
    }

    var type = this.constructor

    return hasReflection
      ? Reflect.construct(baseClass, arguments, type)
      : void(baseClass.apply(this, arguments))
  }

  ret.prototype = Object.create(baseClass.prototype, {
    constructor: {
      value: ret
    }
  })

  return ret
}
Natasha
  • 421
  • 1
  • 3
  • 10
  • 1
    I guess `this.constructor` can work in this case, i can't think of an edge case right now but it seems it works. – MinusFour Aug 23 '18 at 14:36