Why are object initializer methods not usable as constructors?

# /#!/JoePea (8 years ago)

I feel like recent changes in the language (ES6+) introduce new features that don't have the flexibility as the pre ES6 language that we're used to.

For example, super is static and inflexible which is not inline with how this works.

Object initializer methods are also limited. Suppose I want to define an object that contains various constructors. I would be inclined to use object-initializer shortcuts:

let classes = {
  Cat() {},
  Dog() {},
  Bird() {}
}

but this doesn't work:

new classes.Dog() // Uncaught TypeError: classes.Dog is not a constructor

So, here's another failure of intuition (super being static was also not intuitive). We can fix this by writing:

function Cat() {}
function Dog() {}
function Bird() {}

let classes = {
  Cat,
  Dog,
  Bird
}

but that's not as convenient. What's the reason why we shouldn't be able to do that? I feel like JavaScript is being restricted in undesirable ways. I love JavaScript because it has always been so flexible, and I would expect the new features to continue being flexible. That's what makes JavaScript great.

# Andrea Giammarchi (8 years ago)

IIRC methods shorthand are the only where super is allowed, at least in Chrome and Firefox.

Accordingly, if you use them as classes, the super won't point at the one you'd expect, it would point to the object hosting those classes as properties.

class Animal { setName(name) { this.name = name; } }
class Cat extends Animal { constructor() { super(); super.setName('cat'); }}
let classes = {
  __proto__: {setName: 'nope'}
  Cat
};

new classes.Cat(); // ?

I guess you cannot have the best from both worlds.

# Allen Wirfs-Brock (8 years ago)

The most common case is the “methods” are designed to be used as constructors. This has always be recognized for built-in methods in the ECMAScript specification which says: "Built-in function objects that are not identified [in this specification] as constructors do not implement the [[Construct]] internal method unless otherwise specified in the description of a particular function.”. When “concise methods” where add as part of Object Initializers and Class Definitinos this default was applied to them.

You can still define constructible properties for Object Initializers, just not by using concise method syntax:

let classes = {
  Cat: function() {},
  Dog: function() {},
  Bird: class {}
};

let [c, d, b] = [new classes.Cat, new classes.Dog, new classes.Bird];
# /#!/JoePea (8 years ago)

What's the good reason that object-initializer methods can't be constructors though? I mean, I get that "that's what spec says", but what's the actual good reason?

The problem here would be solved with a dynamic super.

I feel that ES6 diverges from ES5 in backwards-incompatible ways. Some people say ES6 classes are "syntax sugar" over the ES5 constructor pattern classes, but, not really, since they aren't compatible in the same places. ES6 classes and object-initializer methods are not backwards-incompatible in a bad way because they cannot be treated the same as we are used to in ES5 (f.e. copying methods with underscore's _.extend() doesn't work because of static HomeObjects), and that is backwards-incompatible with previous concepts, ideas, and methodologies that we love JavaScript for.

Some old code might do something like

_.extend(SomeClass.prototype, OtherClass.prototype)

where SomeClass and OtherClass are ES5 constructor-pattern classes. If the classes are updated to be ES6 classes, then that code may fail because of the static HomeObject properties, and that is backwards-incompatible with the paradigms and patterns of ES5.

A dynamic super would remedy this problem.

For now, I am contemplating using a custom and dynamic Object.prototype.super getter in all apps where my code runs so that my ES6 classes remain as flexible as in ES5. The overhead associated with a custom Object.prototype.super would only be due to the fact that it is not possible for us to hook into the native property lookup algorithm, otherwise the overhead of finding a HomeObject would be completely negligible because the native property lookup algorithm already finds the HomeObject where a property or method is located and therefore it would cost next to nothing to use the found HomeObject on a method call by passing HomeObject as an argument in the native side of the JS engine.

# /#!/JoePea (8 years ago)

IIRC methods shorthand are the only where super is allowed, at least in

Chrome and Firefox.

If super were dynamic, then super could be allowed anywhere, not just in Classes or object-initializer shorthand methods.

For what it's worth, Object.assign is a concept promoted into the language by pre-ES6 ideas, like underscore's _.extend and others. So, the mere fact that Object.assign will fail to produce expected results when copying something from an ES6 prototype that uses super means that the direction of the ES6+ language is backwards-incompatible with pre-ES6 JavaScript and that ES6+ features are incompatible with themselves.

If something like Function.prototype.toMethod made it into spec but not a dynamic super, then solution would be half-complete: tools like Object.assign would also need to start using toMethod internally so that results would be intuitive.

Arguably, the best solution would be for super to be dynamic and for Function.prototype.toMethod to exist, following the same patterns as we love about JavaScript's this keyword. This would open up meta-programming possibilities, for example it would then be easy to write a prototype-based implementation of multiple inheritance whereby a single prototype chain is created based on the multiple classes we wish to extend from (this is in constrast to a Proxy-based implementation).

Claude Pache mentioned that he likes for method borrowing to maintain the same HomeObject, but in my opinion that does not stay inline intuitive expectations coming from pre-ES6 with a dynamic this: "I intuitively expect super to be relative to the prototype chain of the current object where a method is called".

If you are technical enough to understand why method borrowing fails due to a static super, then you'd also be technical enough to understand why it works the dynamic way if that ever became reality. Plus, if Function.prototype.toMethod is brought into spec, then you'd also be technical enough to borrow the method and assign the original HomeObject that you wish.

# Andrea Giammarchi (8 years ago)

I hope you're not using Object.assign to copy anything different from basic setup-like or arguments objects: if I were you I would use Object.defineProperties(targetProto, Object.getOwnPropertyDescriptors(superProto)) or it gonna be "bad time".

I think they answered already on the dynamic super, not sure keep raising the issue would help :-(

Best

# Tab Atkins Jr. (8 years ago)

On Wed, Jul 27, 2016 at 11:44 AM, /#!/JoePea <joe at trusktr.io> wrote:

What's the good reason that object-initializer methods can't be constructors though? I mean, I get that "that's what spec says", but what's the actual good reason?

Because they're methods, not functions. The distinction between the two was merely semantic in ES5, but now it's mechanical, due to super(); constructing something intended as a method would make super() behave in confusing and unintuitive ways, so methods just don't have a constructor any more.

There are several very similar ways you can write your example that do achieve what you want. As Allen said, you can just use the non-concise syntax, explicitly typing "function" (or better, "class") for each of the values. This is only a few characters more and achieves exactly what you want.

It's been explained to you already in previous threads why super() is designed the way it is, and how a dynamic super() would add significant cost to some situations. Making this one niche use-case ("I want to define several constructor-only classes inline in an object initializer") require a few characters less is not a sufficiently worthwhile benefit for the cost. Just type the few extra characters (exactly what you would have typed in ES5, so it's not even a new imposition), and you'll be fine.

# /#!/JoePea (8 years ago)

The distinction between the two was merely semantic in ES5, but now it's

mechanical, due to super(); constructing something intended as a method would make super() behave in confusing and unintuitive ways, so methods just don't have a constructor any more.

So, if super were dynamic, then it would be no problem.

It's been explained to you already in previous threads why super() is

designed the way it is, and how a dynamic super() would add significant cost to some situations

Those "some situations" haven't been listed yet (or I don't know where they are listed). Do you know any? As far as I can tell, a dynamic super would perform just fine:

  • For constructor calls, HomeObject can just the .prototype property of the function when new.target is the same as the function being constructed. That's simple.
  • If new.target is not the current constructed function, then the current function was found on the prototype chain of Object.getPrototypeOf(new.target) (i.e. tje .constructor property was found on some HomeObject in the prototype chain) and then that function is called with the found HomeObject. This seems like a simple addition to the property lookup algorithm.
  • Functions called as methods simply have HomeObject passed as the prototype (HomeObject) where they were found. This seems like a simple addition to the property lookup algorithm.
  • What else?

Based on those ideas from my limited knowledge on thetopic, a dynamic super doesn't seem to "costly".

Suppose I write

obj.foo()

Then, in ES5, there is already going to be a property lookup algorithm to find foo on the prototype chain of obj. Therefore, when the propertyfoo is found on a prototype (a HomeObject), it doesn't seem like all that much extra cost to simply pass that found object by reference to the foo method call, since we already found it. That's not very costly.

I may be using the word "HomeObject" wrong, but I think you get what I mean.

# /#!/JoePea (8 years ago)

Because they're methods, not functions. The distinction between the two was merely semantic in ES5, but now it's mechanical, due to super(); constructing something intended as a method would make super() behave in confusing and unintuitive ways, so methods just don't have a constructor any more.

You are correct for app authors, but wrong for library authors. A library author may easily like to make an object that contains ES5 constructors, and writing them like

let ctors = {
  Foo() {},
  Bar() {},
}

is simply simple.

For example, suppose a library author releases a Class function for defining classes, it could be used like this:

const Animal = Class({
  constructor: function() {
    console.log('new Animal')
  }
})

but most JS authors who don't know about these pesky new JavaScript language internals might be inclined to write:

const Animal = Class({
  constructor() {
    console.log('new Animal')
  }
})

If the Class implementation returns that "constructor", then when the user does new Animal they will get an unexpected error, and that is not ideal at all for a dynamic language like JavaScript.

What's even stranger is that the Class implementation can wrap the concise method with a [[Construct]]able function and call the concise method with .call or .apply and it will work! But that is simply just messy and ugly.

So why prevent it from working only sometimes? It would be much better for it to just work all the time, and make restrictions only when super is present in the function body. In the above example, the constructor() {} concise method does not use the keyword super, so treating it like constructor: function constructor() {} would be much more ideal.

We shouldn't limit developer creativity for reasons that don't exist (I haven't heard of any compelling reasons so far).

The new language features cause failures in unexpected ways, and I really don't think the language should be designed in this less-intuitive way.

JavaScript pre-ES6 has a dynamic nature, but ES6 and newer features are less-so in that tradition.

Making this one niche use-case ("I want to define several constructor-only classes inline in an object initializer") require a few characters less is not a sufficiently worthwhile benefit for the cost.

Actually, no, I want to allow end-users of my library to pass in objects containing methods and properties, and I don't want the result to fail in unexpected ways, and I also don't want to write ugly and hacky code to make it work.

Just type the few extra characters (exactly what you would have typed in ES5, so it's not even a new imposition), and you'll be fine.

Like I said, it won't be me typing these things, it will be end users. Yes I can disguise the problem, but if I for example were implementing a Class tool, I wouldn't like to wrap their non-constructable methods in a proxy function just to make it work not only because it is ugly, but because it will show things in the console that are more difficult to debug.

For example, have you ever looked at classes made with Backbone? They are not so nice to inspect because Backbone wraps constructors and prototypes like an onion.

This language "feature" of concise methods that makes them not constructable forces library authors to write ugly code who's output is harder to inspect and debug by end developers. /#!/JoePea

# Jordan Harband (8 years ago)

You seem to be suggesting that ES6 should be making it easier for you to reimplement a core ES6 language feature. If you want class, can you not use class? That's what users who have access to ES6+ environments are likely to do anyways.

It's also worth noting that someone could do constructor: () => {} or constructor: function *() {} and newing them would fail the same way.

# T.J. Crowder (8 years ago)

Methods not being constructors also makes them lighter-weight: They have no prototype property and associated object.

This isn't limiting developer creativity or freedom. If you want to create an object with constructor functions, you have at least two ways to do that:

  1. class:
let ctors = {
    Foo: class { },
    Bar: class { }
};
  1. function:
let ctors = {
    Foo: function { },
    Bar: function { }
};

but most JS authors who don't know about these pesky new JavaScript language internals might be inclined to write:

Then the library author warns them not to do that in the documentation (maybe even a really clear error message in the non-min build), or they figure it out really quickly when they do and it doesn't work. :-)

-- T.J. Crowder

# /#!/JoePea (8 years ago)

/#!/JoePea

On Mon, May 15, 2017 at 6:16 PM, Jordan Harband <ljharb at gmail.com> wrote:

You seem to be suggesting that ES6 should be making it easier for you to reimplement a core ES6 language feature. If you want class, can you not

Yeah, sure, why not? Allow library authors to do awesome things like provide libraries for multiple inheritance that aren't hacks.

import Bar from './Bar'
import Baz from './Baz'
import multiple from 'multiple-inheritance-library-by-some-author'

class Foo extends multiple(Bar, Baz) {}

use class? That's what users who have access to ES6+ environments are likely to do anyways.

ES6 classes don't have protected or private members, but an author's Class tool might.

Not everyone is writing ES6 in a shiny new app. There's large outdated code bases. It'd be convenient for a tool like Class to work on old code, and not fail on new code.

It's also worth noting that someone could do constructor: () => {} or constructor: function *() {} and newing them would fail the same way.

Yes, we can't prevent all the bad usages, but those are obviously not meant to work. Concise methods aren't obviously going to fail, especially considering that they are not really called "concise methods" by most people, but more like "shorthands", and by that terminology the layman is going to expect them to work. Arrow function and generatos are very explicitly different, and you have to actually know what they are in order to use them.

There's always going to be some way to make some library fail, but that doesn't mean we should add more ways to make code fail when we can avoid it. Arrow functions and generators are necessary for a purpose. However, making concise methods that don't use the keyword super non-constructable doesn't really have any great purpose, it only makes certain code fail in needless ways...

# Michael J. Ryan (8 years ago)

But if you're introducing a new Class library, your writing new code... You could almost as really setup a build chain (Babel) to support class syntax, if you can't already target sorted browsers.