2

I am refactoring some code from an older ES5 codebase, where I was doing the following:

function ObjectCreatorFunction() {
  this.someProperty= {};
}

/*
 * Static method, to extend the object passed as a parameter with the
 * ObjectCreatorFunction instance properties and methods
 */
ObjectCreatorFunction.extend = function extend(object) {
  var key;

  ObjectCreatorFunction.call(object);
  for (key in ObjectCreatorFunction.prototype) {
    if (ObjectCreatorFunction.prototype.hasOwnProperty(key)) {
      if (!object.hasOwnProperty(key)) {
        object[key] = ObjectCreatorFunction.prototype[key];
      }
    }
  }

  return object;
};

ObjectCreatorFunction.prototype.someMethod = function someMethod() {...}
//etc

I am trying to do the same with an ES6 rewrite, so I a have this

class ClassName{
  constructor() {
    this.someProperty= {};
  }

  static extend(object) {
    let key;

    ClassName.constructor.call(object);
    for (key in ClassName.prototype) {
      if (ClassName.prototype.hasOwnProperty(key)) {
        if (!object.hasOwnProperty(key)) {
          object[key] = ClassName.prototype[key];
        }
      }
    }

    return object;
  }

  someMethod() {...}
  //etc
}

My problem is that the line ClassName.constructor.call(object); does not work as intended, i.e. The passed object does not get the instance properties of the class.

I have tried s few ways to rewrite this (even some unorthodox ones) to no avail.

How do I extend an object with a class' instance properties, using ES6?

DISCLAIMER:

My code is passed through a transpiling process, with babel and webpack. In case it has any effect to how classes work internally.

Dimitris Karagiannis
  • 8,942
  • 8
  • 38
  • 63
  • 1
    "*The passed object does not get the instance properties of the class*" - really, is that your only observation? You don't get any exception? – Bergi Sep 18 '17 at 14:37
  • 2
    If you want to write mixins, I recommend not to use `class` syntax for them. `function` is still just fine. – Bergi Sep 18 '17 at 14:38
  • @Bergi The way I have this written, no I don't get any exceptions. The passed object simply returns the same as it was when it got in. If I write it in any other way, e.g. `ClassName.call(object)` or `new className.call(object)` I am getting `Cannot call a class as a function` and `className.call is not a constructor` respectively – Dimitris Karagiannis Sep 18 '17 at 14:41
  • Yes, those are definitely what I had expected. – Bergi Sep 18 '17 at 14:43
  • Hint: `ClassName.constructor` is `Function`. `ClassName.call(object)` is the actual equivalent to your ES5 code, and it doesn't work because of `class` syntax. – Bergi Sep 18 '17 at 14:55
  • *ClassName.call(object) is the actual equivalent to your ES5 code* that was my understanding as well, that's how I had it written initially, but replaced it with something that does not throw. What I do not understand is why the `class` syntax is affecting it, since they are supposed to be equivalents. Because, in the end, the `class` syntax is just sugar – Dimitris Karagiannis Sep 18 '17 at 14:57
  • 1
    Because constructors of classes are [a bit more](https://stackoverflow.com/a/32458960/1048572) than just syntactic sugar. Also the distinction between constructible-only (requiring `new`) functions and callable-only (forbidding `new`) is new in ES6 - with `class` constructors and methods as examples. The prototypical inheritance stayed the same, but the initialisation of instances works differently now especially for inherited classes. – Bergi Sep 18 '17 at 15:08
  • I can accept the last comment, with any extra relevant information, as an answer if you want to write it as one. – Dimitris Karagiannis Sep 18 '17 at 15:12

1 Answers1

3

No, this does not work with class syntax. It's a bit more than just syntactic sugar. The prototypical inheritance stayed the same, but the initialisation of instances works differently now especially for inherited classes, and you cannot invoke a constructor without new to not create a new instance.

I would recommend to be explicit about your mixin, and give it an init method:

class Mixin {
    constructor(methods) {
        this.descriptors = Object.getOwnPropertyDescriptors(methods);
    }
    extend(object) {
        for (const p in this.descriptors)) {
            if (Object.prototype.hasOwnProperty.call(object, p)) {
                if (process.env.NODE_ENV !== 'production') {
                    console.warn(`Object already has property "${p}"`);
                }
            } else {
                Object.defineProperty(object, p, this.descriptors[p]);
            }
        }
    }
}
// define a mixin:
const xy = new Mixin({
    initXY() {
        this.someProperty= {};
    },
    someMethod() { … }
});
// and use it:
class ClassName {
    constructor() {
        this.initXY();
    }
}
xy.extend(ClassName.prototype);
jcubic
  • 61,973
  • 54
  • 229
  • 402
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I appreciate your time writing this, but this introduces a whole new level of "abstraction", where I just wanted to know if there was a straightforward way to achieve the effect of the ES5 code with classes. If it is not possible, that's a valid answer but I'd like to understand why. P.s. my code is passed through a transpiling process, with babel and webpack, not sure if it affects it – Dimitris Karagiannis Sep 18 '17 at 15:01
  • You can also drop the abstraction of the `Mixin` class and dump it all into one object literal. But yes, you cannot use a `class` to create mixins. – Bergi Sep 18 '17 at 15:10