Prototype definition in object literal

i suspect that is the reason, and many js-programmers (even to this day), feel inferior to programmers of "grown-up languages" -- to the point of defering to latter for language-design, even if they have no clue how frontend-programming works.

yes i'm noob to async/await ^^;;;

Perhaps.

But I generally try to be extra warry of these types of arguments. There's always pushback to any language change, and often one of the first things that people shout out is something like "They just don't know what they're doing", or, "They're just trying to copycat xyz, and didn't think through how it would fit into the current language", etc.

The common thread between all of these arguments is that they're all based on the idea that "they" don't know something that we know. And the common issue with all of these arguments is the fact that if "we" don't know something that they know, then we would still be able to come to the exact same conclusion. Plus, they're arguments that don't have much substance - they attack the reasoning behind a feature addition, and not the value of the feature addition itself.

I'd rather air on the side that there's still something I need to learn about the opposing point of view, even if I might be dead wrong.

Regardless of the motivation, using class is the safest and simplest way to do any kind of inheritance in JS - it sets up the expected contracts and conventions properly, in a way that was almost universally messed up prior to class syntax becoming available.

Constructing inheritance hierarchies otherwise - using __proto__ or Object.create - is quite rare, and arguably quite unidiomatic in the ecosystem.

but why does a frontend-programmer even need to inherit anything, when every piece of data he/she comes comes across is either a string or json-object that gets immediately serialized and sent-over-the-wire?

examples of poorly-designed, class-based api's for common frontend-tasks:

  1. forcing frontend-dev to create artificially-stateful/inheritable Temporal-objects, when the context is to statelessly message-pass isostrings between dom-elements <-> databases.

  2. forcing frontend-dev to create artificially-stateful/inheritable TextEncoder/TextDecoder objects when the context is to statelessly message-pass blob/utf8-string/arraybuffers across realms.

  3. forcing frontend-devs to create artificially-stateful/inheritable Intl-objects when the context is to statelessly message-pass javascript-number / human-readable-currency between database <-> presentation-layer.

  4. forcing nodejs-devs to create artificially-stateful/inheritable crypto-objects when the context is to statelessly message-pass plaintext / ciphertext / jwk-strings between external-endpoints.

the above ux examples and more would be better if tc39, whatwg, node-tsc provided static-functions instead (like fetch()) that directly transformed incoming local-isostring / blob / number / plaintext to output-ready utc-isostring / utf8-string / human-readable-currency / ciphertext that can be immediatedly sent-over-the-wire.

instead of wasting frontend-developer's time creating error-prone, intemediate, artificially-stateful class-objects, in the ultimately stateless message-passing context.

Because your premise is false. As we've discussed many times before, frontend devs simply do not solely deal with string or JSON, immediately serialized or otherwise.

Frontend applications potentially have literally every single kind of complexity that serverside ones have, and also, JS runs on the server - so "frontend doesn't need X" is irrelevant if the backend does need it.

@kaizhu256 - I've actually started a separate thread over here that discusses the importance of inheritance in Javascript. I do beleive in the current state of Javascript, there are certain scenarios where inheritance can be useful, but I also think it does get overused often, and there's potentially different ways to model the language in it's future so that we don't need inheritance at all. Certainly, some of your examples would fall into the overused cases. If you have thoughts related to the overall value of inheritance, you could add to the discussion there. But, like @ljharb said, we've got to keep in mind that Javascript is used by people for all sorts of purposes. It's not simply used by front-end devs to add a tiny bit of scripting to an almost static page - highly interactive webpages, webapps, servers, scripts, electron apps, etc, all require a different level of complexity, and the language has to support all of these use cases. Those who only need the most basic features from Javascript can cherry-pick those out and use them, but for everyone else who needs a little more power, the committee is committed to providing that kind of power for everyone.

For the purposes of this post, it's probably best to just assume that is the occasional valid use case for inheritance (what those are can be discussed in the other thread).

Do you just think classes are simpler and safer than constructor functions? Because I don't think we have to argue about that, they definitely are. Or are you also saying that classes are safer and simpler than using __proto__ in an object literal?

If a syntax like MyPrototype <| { ... } would become available, would you still consider the usage of classes the simplest way?

I agree that this is the case at the moment, but isn't this at least supported by the fact that the __proto__ syntax is obscure and Object.create(...) is really laborious to use?

To your first question, yes, I also think it's far far safer and simpler than using __proto__ inside an object literal.

Regarding the __proto__ syntax, it exists, so any aesthetic concerns are irrelevant and adding a second syntax that does the same thing doesn't make sense; it's obscure not because of the underscores but because the capability is generally considered to be something to avoid. Adding new, cleaner syntax for it wouldn't change that.

The concerns are not only aesthetic. It is obscure because it has different meanings in JS and JSON, and it makes zero sense that {"__proto__":X} does something different than {["__proto__"]:X}

2 Likes

Can you elaborate why?

Because there's more to inheritance than just the prototype chain. People expect a constructor, they expect instanceof, they expect to be able to class extends that constructor, they expect a .constructor property on the prototype, they expect common functionality is always shared.

It's much harder with class to accidentally have own functions and duplicated logic, and it deprives you of the easy ability to have private instance fields - encapsulation is very important.

I wouldn't say those are expected of function-based constructors, and most code dealing with arbitrary non-primitive objects are able to handle even Object.create(null).

It's also worth noting that a constructor property will necessarily exist on any object that inherits from Object.prototype and that even from the standpoint of built-in objects, you can't rely on constructor being an own property of the prototype (think: array iterators). So there is actually some precedent even within the standard library.

Edit: I am willing to be proven wrong here, but I'm pretty skeptical absent data as my experience seems to suggest otherwise.

Alright - I've had some time to process everything. I do see valid concerns and ideas with what you've said, which I'll address lower down, but I'm first going to start with some rebuttals to some of your points.

I'm going to go back to comparing against Python. As mentioned previously, classes in python are generally not seen as a bad thing to use, which makes it a good measuring stick to test the strength of different arguments against Javascript class syntax. If that argument also goes against python syntax, then it must not hold that much power, as python classes still stand tall against that argument. It also means the argument isn't intrinsically one about prototypal inheritance, because python doesn't use prototypal inheritance.


  1. You mentioned that, because of Javascript's prototypal model, a Javascript "class" can return anything, even if it's not an instance of that class. The same is actually true in python as well, and it's not that difficult to do:
class MyClass:
  def __new__(*args):
    return 2

print(MyClass()) // 2

This is normally seen as a bad practice in both python and javascript, as it goes against what you would expect, but it's easy to do it in both languages.

  1. You talked about how class fields can't be inspected until an instance is created.

I'll actually defer to C++ for this one. Reading this stackoverflow Q&A it sounds like C++ doesn't provide any way to get the name of class fields either (it doesn't have a reflection mechanism), and getting other kinds of information can only be done through all sorts of hackery nonsense that C++ allows because, well, it's C++.

  1. You mentioned functions like Object.keys(instance) only gets you the keys of your instance fields, it doesn't also provide the keys from the prototype, like a getter.

There isn't really a one-to-one comparison of this behavior in python. But, the core of this problem seems to be the fact that it's important to know which fields end up on the prototype and which ones don't, otherwise, you'll be surprised by certain behaviors. The more general variation of this issue is something that python exhibits.

Take this code snippet for example:

class Stuff:
  data = []

obj1 = Stuff()
obj2 = Stuff()
obj1.data.append('x')
print(obj2.data) # ['x']

This is a common pitfall that trips developers up. Unlike in javascript, the python member fields are all static members of the class that are available to be looked up on all instances. This means each instance will share the same member fields. This isn't a problem when you only use primitives, because those are immutable, and reassigning (e.g. via obj.x += 1) will just cause a new primitive to be placed on the instance that shadows the shared one on the class.

It's important to understand the underlying machinery of how inheritance works in python, and where in the hierarchy chain the different fields go, in order to not get tripped up by seemingly odd behaviors like this.

  1. A Javascript instance doesn't have a reliable link to a type - the best we've got is instance.constructor.

Same with python - absolutely everything is observable, reflectable, and modifiable at runtime (even data that a function has closed over can be accessed and modified! - The one source of privateness we have in javascript). This also means that an instance in python does not have a reliable "type" or link back to whoever created it.


Ok, my rebuttals are now over. You did say that you didn't claim this list to be comprehensive or to be strong enough to warrant your point of view, and so I don't expect these counterarguments to do anything to change your point of view on this matter. But, perhaps there is something to learn from all of this.

I'm really coming to believe that Javascript class syntax being built on top of prototypal inheritance does not give it any more oddities than that of a language like python. In fact, I'm going to go as far as to say that, because of the class syntax, Javascript has classical inheritance, just as much as python or java have it. I know, I know, it's mostly syntax sugar that's built on top of prototypal inheritance. But, what's important is the fact that the class syntax emulates classical inheritance, and that emulation is good enough to make it real. Just like assembly/binary might not natively have functions, but we say C++ has them. In reality, C++ is just providing function syntax that emulates function-like behavior, but at any point, you can dig into the raw assembly and expose the real truth - it's just labels and gotos. Even at runtime you can do fancy memory tricks to get behind the facade and cause the truth to be revealed.

Perhaps as a better example: It can be said that python classes are just "syntax surgar" for their built-in type function (no one says it though...). With the type function, you can dynamically create new classes at runtime, give them a name, an inheritance link, properties, etc. Does that mean python doesn't actually have classes, because it's just syntax surgar for type()? Or is this emulation enough to grant python the badge of "I have classes"?

I'll concede a bit though - the point of this was mostly to show that everything's not so black-and-white, and classes merely being "mostly syntax surgar" isn't actually a great argument for why it might not have a certain feature. So, when people say "Javascript does not have classical inheritance", well, they're correct in some ways, but it's also not completely wrong to say "Javascript has classical inheritance" - we're in a bit of a fuzzy space.


I will acknowledge some other good points I'm seeing in everything you said. In particular, I'm going to prod at a couple of specific quotes that I found to be really golden, and see if they can be mashed together into a conclusion that perhaps we both can agree on.

Perhaps a core problem here isn't the fact that class syntax creates a lot of oddities when built on top of prototypal inheritance (as I just mentioned, I do not believe Javascript classes have any more oddities than python ones), perhaps a core problem is simply the fact that we now conceptually have two forms of inheritance in Javascript: prototypal inheritance, and classical inheritance. Class syntax, in any language, is already a big syntax construct that takes time to learn and understand. And, even though the syntax looks similar from language to language, the specific details of what that syntax does vary widely (like, where a public field value is stored - on the class or on the instance). In Javascript, not only do developers have to learn and understand their version of the class syntax, they also have to learn and understand prototypal inheritance.

So perhaps, from where you stand, you would rather just take a simpler route, ignore Javascript classes, and just use prototypal inheritance. If others libraries give you classes, that's fine, you can still understand and use those classes in terms of prototypes. Prototypes are simple. Classes are complicated. And maybe you even feel that the safety benefits that classes provide, which @ljharb described, are simply not worth the extra complexity and overhead that class syntax provides.

In other words, perhaps the core issue isn't the fact that classes are more confusing and odd because they're built on top of prototypes, rather, in addition to the complexities that any class syntax provides (from any language), Javascript also has prototypes, and that's just too much. There's an easy way to ignore the class-syntax side of Javascript, but it's impossible to completely ignore the prototype side.

Ok, now I'm just putting words in your mouth - but this sounds like a reasonable conclusion to me at least. I still prefer factory functions or class syntax, but I can understand if someone else were to come to a conclusion like this, and would rather have a simpler developing experience than to work with the complexities of dealing with both prototypal inheritance and classical inheritance. But, maybe you completely disagree with everything I said, and still believe that classes built on prototypes just cause all sorts of unwanted oddities :man_shrugging:.

2 Likes

This is a great conclusion, and I don't really have anything to add anymore. :grinning_face_with_smiling_eyes: I agree that it shouldn't be seen so black-and-white, there are different trade-offs to everything.

The confusion between the deprecated myObject.__proto__ property and the special case of { __proto__: ... } still seems highly problematic to me. But if this thread does not provide enough reason to add better syntax for specifying the prototype of a new object, then I think I do not have anything left to provide a better reason.

1 Like

Note that when doing it that way the [[HomeObject]] of any methods will be incorrect, and therefore the super keyword will not work like we want (super.method() calls will not work).

This is how to keep super working:

const obj = {
  __proto__: thePrototype,

  someMethod() {
    super.someMethod()
  },
}

obj.someMethod() // ok

Otherwise with the Object.assign approach we'll get an error like undefined is not a function on the super.someMethod() call:

const obj = Object.assign(Object.create(thePrototype), {
  someMethod() {
    super.someMethod() // Error, undefined is not a function
  },
}))

obj.someMethod() // error
2 Likes

That's interesting - didn't know that.

It seems in general, super() was mainly designed for use with class syntax, but it occasionally works outside of the class syntax as well. I found this thread talking a little more about it, and how super() doesn't work great with the original form of inheritance either (the way we inherited using functions and prototypes, before the class syntax came around).

There's a handful of newer features that are only designed to work within class syntax (like private fields). super() is a little special in that it occasionally works outside as well.

Yeah. It's useful for people that like to make alternative patterns but still want super to work. F.e. I made a Class utility (unit tests) that works like the following, where the super calls work thanks to [[HomeObject]]:

export default
Class("Foo").extends(SomeOtherClass, ({Protected, Private}) = ({
  public: {
    publicMethod() {
      super.publicMethod()
      Protected(this).protectedMethod()
      Private(this).privateMethod()
    }
  },
  protected: {
    protectedMethod() {
      super.protectedMethod()
      Private(this).privateMethod()
    },
  },
  private: {
    privateMethod() {
      Protected(this).protectedMethod()
    }
  },
}))
import Foo from './Foo.js'

const f = new Foo
f.publicMethod() // ok
f.protectedMethod() // Error: can't call undefined (protected is not public)
f.privateMethod() // Error: can't call undefined (private is not public)

const Bar = Class("Bar").extends(Foo, ({Protected, Private}) => ({
  // without using `public: {}` the top level is public
  publicMethod() {
    super.publicMethod() // ok
    Protected(this).protectedMethod() // ok
    Private(this).privateMethod() // error
  }
})

new Bar().publicMethod()

On the side, the privacy I implemented here, if it were converted to a syntax proposal, would have both protected and private, and in my opinion it would be better than #private fields currently are.

1 Like