Prototype definition in object literal

The issue is that most users have a really hard time telling the two things apart...

3 Likes

I like that idea, just sadly it doesn't exactly fit well with things like private fields (the part that'd make it most useful). :pensive:

Can you elaborate what the issues are?

How would you integrate it with private data without the use of weak maps?

@claudiameadows - I'm not sure I follow. From what I understand, this is basically a syntax shorthand for Object.create() - you can create an object literal and set its prototype at the same time.

However, I'm still not totally sure I understand the use case for this to begin with.

Hi everyone. I started thinking about this after I read this article. Specifically after getting to know that v8 creates an internal class for each new "object shape" and that adding new properties to an object causes the engine to create a new hidden class.

In many cases what you need is to declare a null object with some properties and pass it somewhere, but currently you are forced to do one of the following:

  • use nasty __proto__ syntax that should not be a part of the language
  • use Object.create(null) and create unwanted internal classes
  • use Object.setPrototypeof({...}, null)

Because of backward compatibility concerns and the fact that by default object literal syntax creates instances of built-in Object class I thought that dedicated syntax would do the job. IMHO the real solution would be if object literal syntax returned objects with null prototype (Yes, I understand how "realistic" it is).

1 Like

For me, the use case would be that we can do "pure" prototype chains and not having to deal with classes/constructor functions. At the moment this can only be done via a magical __proto__ property that looks like a hack and 90% of the time is confused with another deprecated feature, or with Object.create(Prototype) which is either too verbose or too cumbersome.

1 Like

And what value do you see in doing a "pure" prototype chain over, say, the spread operation. e.g.

const someFunctions = { f() { ... }, g() { ... } }
const myInstance = { ...someFunctions, x: 2, y: 3 }

// vs

const someFunctions = { f() { ... }, g() { ... } }
const myInstance = someFunctions <| { x: 2, y: 3 }

I can think of a couple of reasons why prototype inheritance might be preferred:

  • Performance (you don't have to copy as much)
  • It allows instanceof checks to work properly
  • Maybe you find it just feels cleaner if you separate the methods from the data? e.g. if you log out the object, you're going to see a class name and data, but not each individual function.
  • You would be able to shadow functions, and use super() to access them.

Are one or more of those the reason you're wanting this? Or is it something else entirely?

this works ok when simply spreading the prototype:

const x = { data: null, method() { console.log(this.data); } };
const y = { ...x, data: 'test' };

// Object { data: "test", method: method() }

But this doesn't really work:

const x = { 
    data: null, 
    method() { console.log(this.data); }, 
    get allData() { return [this.data, 'more data']; } 
};
const y = { ...x, data: 'test' };

// Object { data: "test", method: method(), allData: Array [ null, "more data" ] }

This is impossible:

const x = { data: null, method() { console.log(this.data); } };
const y = { 
    ...x, 
    data: 'test', 
    method() { console.log('extended...'); super.method(); }
};

y.method();
// Uncaught TypeError: super.method is not a function

And finally, messing up the order has consequences:

var x = { data: null, method() { console.log(this.data); } };
var y = { 
    data: 'test', 
    ...x, 
    method() { 
        console.log('extended...'); 
        super.method(); 
    },
};

// Object { data: null, method: method() }

OK, so you want it for purposes of super() and getters/setters.

Now, what are the issues you see with using existing solutions to these problems, like factory functions or classes. I'm sure I don't have to enumerate the ways to do this using a class, but I will show how it can all be done using factories.

// 1. normal factory

const createThing = ({ data }) => ({ data, method() { console.log(this.data); } });
const y = createThing({ data: 'test' });
// { method: [Function: method], data: 'test' }

// 2. Getters

const createThing = ({ data }) => ({
  data,
  method() { console.log(this.data); }, 
  get allData() { return [this.data, 'more data']; } ,
});
const y = createThing({ data: 'test' });
// { data: 'test', method: [Function: method], allData: [Getter] }

// 3. super()

// Preferably, the factory will be designed to provide hooks, to let you customize
// certain behaviors while making it more difficult to modify other behaviors.

const createThing = ({ data, onMethod = () => {} }) => ({
  data,
  method() { onMethod(); console.log(this.data); },
});
const y = createThing({
  data: 'test',
  onMethod() { console.log('extended...'); }
});

y.method();

// But, you can of course monkey-patch the object as well.
// It's a little tricker to do, but I think that's a good thing, because it discourages this type of behavior.
// It's always preferable to use whatever explicit interfaces the factory has provided for extension
// then paving your own custom route.
// The same logic applies with classes - it's always better when a class provides specific hooks
// to override certain behaviors, rather than expecting you to shadow certain functions and not other ones.
// (This is called the strategy pattern)

const createThing = ({ data }) => ({
  data,
  method() { console.log(this.data); },
});
const originalY = createThing({ data: 'test' });
y.onMethod = function() { console.log('extended...'); originalY.method() }

y.method();

I'm mostly giving all of this for context as a back drop for the following question: What ways do you feel having explicit syntax for setting the prototype of an object literal is better than using a class or a factory? What is it about syntax like MyPrototype <| { x: 2, y: 3 } do you find more appealing than our current solutions?

I personally think that while these solutions work, they are still either very cumbersome when doing inheritance (factory functions) or are inherently flawed due to the fact that they are simply hiding the true nature of the inheritance system (classes / constructor functions). ECMAScript uses prototype based inheritance at it's core, and directly utilizing it is really straight forward and easy to comprehend. So I think it's reasonable to choose the simplest, approach to inheritance. The only issue with directly using prototype chains is the current syntax.

This is also not a new type of inheritance we are talking about, it wouldn't even be a new way to construct objects. It would simply be a different syntax for an existing feature.

I find this question very confusing to answer. Syntax like MyPrototype <| { x: 2, y: 3 } is more appealing than the existing syntax { x: 2, y: 3, __proto__: MyPrototype } because it can not be confused with another deprecated feature. I think it's reasonable to assume that { __proto__: MyPrototype } and object.__proto__ are the same thing, and it's very counterintuitive that they are not… But this has nothing to do with other ways of object construction and inheritance currently available in the language. I think I explained why I prefer direct construction of prototype chains above.

1 Like

Thanks - my previous questions were there mostly to try and figure out what the problem is we're actually trying to solve, so that we can better discuss the problem and see if this proposed solution really is the best fit.

I find this question very confusing to answer.

When I said "current solutions", I was referring to factories and classes. You answered this question in your first couple of paragraphs. I can understand your argument for not wanting to use __proto__ :).


they are still either very cumbersome when doing inheritance (factory functions)

Fine by me. Inheritance gets overused anyways, so I don't have an issue if the syntax is cumbersome enough as to force developers to think twice before using this tool. I actually wish it was a little more difficult to do inheritance with the class-based syntax than a simple extends Whatever.

or are inherently flawed due to the fact that they are simply hiding the true nature of the inheritance system

As for class syntax, I've never really understood the hate towards them. The arguments I usually hear are things like "A Java or C# developer coming into Javascript would be surprised by how classes behave in Javascript, because of reasons such as: An instance can change classes at runtime, or, a class's functions can be dynamically altered after the class has been declared". My issue with the above argument is the fact that those same surprising qualities a Javascript class has also hold for python classes. No one in the python community runs around saying python classes are bad and should not be used, or that Java programmers are having an extremely difficult time understanding how they work. Here's an article I found that closely matches my thoughts on this.

But, you never said that the above argument was your reasoning for your dislike of class syntax. You said it was because it's "hiding the true nature of the inheritance system" - I'll be interested in learning more about your thoughts related to this. Perhaps (hopefully), this can help me understand better where the class-haters are coming from, and maybe the above different-from-java-classes argument is just what gets thrown out, but isn't the true reason for the dislike.

I can see a few potential reasons for you mentioning that class-based inheritance hides the true nature of Javascript's inheritance system:

  1. You feel prototypal inheritance provides useful features that class syntax makes difficult or impossible to use. If this is the case, what are those useful features you want that class syntax does not provide?
  2. You feel that the overall paradigm of using prototypes is superior to that of using classes. Perhaps you only feel that class syntax is a bad thing simply because is subpar to the prototypal paradigm. If so, would you be able to explain what it is about the prototypal paradigm that you find to be better than class-based inheritance, or maybe point me to an article that you find explains your point of view well?
  3. This is related to the previous point - perhaps you feel it's easier to teach and learn prototypal inheritance over inheritance with classes. Maybe part of it is because of the fact that inheritance with classes does make it easy to avoid learning certain aspects of how the inheritance system works. If this is the case, do you think there are certain footguns related to only knowing inheritance via classes and not prototypal inheritance? If so, what are they? If not, what do you feel is the damage of not fully understanding how the underlying inheritance system works? There's lots of language machinery that people often don't fully understand, but that's usually ok unless the misunderstandings often lead to buggy code. (i.e. have you ever looked up the details of how scoping works with c-style for loops? It's pretty complicated, but no one seems to trip up over not knowing the details - check out this entertaining video about it - especially the latter half of it).

(The following paragraph has been edited slightly to clarify) As an aside, I don't think the argument "classes hide the true nature of prototypes" in-and-of-itself is sufficient to avoid classes. A similar thing could be said about async/await - that syntax hides the true nature of callback-based programming and promises, and if someone were advocating for a syntax that encouraged the use of callbacks over promises, I would probably ask them the same questions. 1. Are there useful features callbacks provide that async/await/promises do not? 2. Do you find the overall paradigm of callbacks better than that of async/await/promises? 3. Do you find async/await/promises harder to teach/learn, because it allows developers to get by without really understanding a lot of the details of how Javascript's async system works? If so, do you think there are footguns related to not knowing the true nature of Javascript's async system?

I assume you don't have an issue with async/await/promises - so hopefully this parallel can illustrate my current point-of-view and where I'm coming from, but I'm excited to learn your point of view on all of this.

  1. Are there useful features callbacks provide that async/await w/ promises do not?

callbacks are the only way frontend-devs can setTimeout handlers to cancel/reject ajax-request that take longer than say, 15-30 seconds.

or pretty much any performant nodejs program doing many http-request with expectation a few of them will hang for no reason.

That sounds like an argument in favor of timeouts in general. setTimeout just happens to use a callback-based API, but it could have used a promise-based one instead, and it would be just as powerful. I've edited my previous post to clarify that paragraph a bit, because I think I may have been a little confusing about it.

Note that it was simply meant to be an analogy/illustration, and I'm sure it has some holes in it, especially if you happen to also believe that async/await should never have existed (I've seen some people who fall into that camp here on these forms).

Promises are for when there’s a single future value. Callbacks are one solution for multiple future values, like events.

Ok, yeah, there's holes in that analogy. Maybe it would have been best if I never said it.

This looks quite neat, even though semantically it doesn't make much sense. However, does it have to make sense? Typical argument in conversations concerning class extends null...

Many other features make very little sense, i.e.:

typeof(null) === 'object';

const { foo, bar } = null; // throws

I just want to say that I really like lightmares idea and I think that it is superb. In this version of reality, where objects declared via literal syntax create instances of a built-in Object class, addition of the feature like this could require a new syntax.

Goodthing that lightmare's new syntax idea is fairly "old".

I must admit that I did not expect this much offtop. I'd realy appreciate if you remove what should not be here.

sorry for digressing, but just realized Promise.race can replace callbacks for handling timeouts in http-requests.

below code is much cleaner than how i've handled timeouts in past, so retracting previous statement callbacks are better than async/await for timeouts.

// copy-paste code below into browser-console
(async function () {
    async function fetchWithTimeout({timeout, url}) {
        let response = await Promise.race([
            fetch(url),                         // race to fetch url
            new Promise(function (resolve) {    // race to timeout
                setTimeout(resolve, timeout, new Error(
                    "timeout " + timeout + " ms"
                ));
            })
        ]);
        if (response.message) {
            throw response;
        }
        return response;
    }
    console.log(await fetchWithTimeout({timeout: 15000, url: "/?nonce=1"}));
    console.log(await fetchWithTimeout({timeout: 1, url: "/?nonce=2"}));
}());

/*
// fetch succeeded before timeout
VM60:16 Response {type: "basic", url: "https://example.com/?nonce=1",
redirected: false, status: 200, ok: true, …}

// fetch failed before timeout
VM60:6 Uncaught (in promise) Error: timeout 1 ms
    at <anonymous>:6:46
    at new Promise (<anonymous>)
    at fetchWithTimeout (<anonymous>:5:13)
    at <anonymous>:17:23
*/

1 Like

I think there is a lot to unpack in your comment, but I will try to address as much as possible.

I was not trying to say that I hate it because it's "hiding the true nature of the inheritance system", but that I perceive it as being flawed because of it being built on top of prototype based inheritance. I do not mean that it is completely broken and can not be used, but that it has many small imperfections which make it cumbersome to use at times.

Okay, I will try to explain my thought process here, I hope it can be followed.

When we look at ECMAScript, we see that we do not have any kind of type system. Disregarding primitive types and arrays, there are only objects and functions (yes functions are also objects, but they are still special), nothing else. Users of the language also can not create new types. In order to reuse information and functionality, the language allows us to chain together objects. If one object doesn't contain some of the things we are looking for, we can travel up the chain to find it in the linked object. Objects are not constructed from classes which behave like a building blueprint, there is no such concept in the language. So they have to fit in the space of objects and functions. This requirement makes it hard for classes to be their own thing, they have to be either a function or an object. In the end they are both, a function and an object, and everything they can do has to be fit into one of the two.

This all means that there are limits to what classes can be, I will try to list as many as I can, but I don't claim that they are enough to justify my conclusion or that it's a complete list, it's just what I can think of at the moment.

  1. The primary interface of a class is in fact a function with an implicit return value, which is an object that is linked to the classes prototype. Since it's a function, the author can override the implicit return value, so we have no guarantee that the return value has a detectable relationship with the class.

    class CustomWindow {
       constructor() {
          const custom = Object.create(window);
          custom.newMethod = function(a, b) { ... };
    
          return custom;
       }
    }
    

    The resulting "instance" has nothing to do with the class... We don't have to do this, but there is no guarantee that it won't be done at some point.

  2. As said before, everything we can do in a class has to fit into the function or the prototype object. Methods (and getter/setters too) get put into the prototype object, they shouldn't change and should be share across all instances, that's nice, and we can look at the prototype object, we can go through all properties and inspect them, we can use all the reflection ECMAScript offers. Class fields on the other hand are dynamic and part of the constructor function. Since it's a function, the language can not tell us anything about the contents of said function. The properties of a class are not inspectable until we create an instance of our class. This is weird, it makes sense inside the limits of the language, but it feels counterintuitive, maybe even wrong.

  3. Building onto 2. We can get the class properties from our new instance by calling Object.keys(instance), but wait, if we have a getter defined in our class, then it won't be part of this list. I have to look into Class.prototype for my getters... Knowing how the runtime system works, it seems obvious, Object.keys(...) only operates on the provided object, not it's prototypes. But from the perspective of wanting to obtain a list of all fields on the class this feels cumbersome and overly complicated...

  4. Since there is no type for an object, we also have no way of referring to it. All we can do, is adding a property into our object that refers to the function which created our object. So if we want to create a new object with the same type/class than our current object, we have to it this way new this.constructor().
    Accessing static methods or fields has to go the same route, this.constructor.staticMethod(). It works, but it feels cumbersome.

All these circumstances lead me to the question, do we really need classes? Can't we just use objects and prototype chains? Isn't it simpler if we embrace objects the way they are and just not deal with classes and constructor functions? Is there really a great advantage in using one system that is acceptable at best layered on top of another system?

If we just ignore classes and constructor functions, we still have a usable inheritance system but have less oddities to concern ourself with....

I can't think of anything off the top of my head. Objects which have been constructed via a constructor function or class can still be prototypes, so that doesn't limit me in utilizing features of prototype chains. But I guess it's not so obvious to for example utilize inheritance for working copies, where we want to track which properties in an object have been changed, when someone thinks of inheritance only in terms of classes.

No not really, but I do think that it is also not inferior to that of using classes. Like I tried to state above, I think that when we look at it in the context of ECMAScript, we can reduce complexity and confusion by not trying to apply one paradigm with the help of an other one.

In my experience, people who have prior knowledge of other languages often tend to take heavy shortcuts and jump to conclusions far from reality when trying to understand certain aspects of the language. (This is not related to classes, but apparently all promises are obviously executed in a separate thread). I think ECMAScript classes can contribute to this, but I wouldn't consider them a major cause.

I agree.

Yes, they usually can be called more than once. :grin:

This is just an analogy, but I want to comment on it anyway. In my experience, it has been harder to teach the concept of async / await to people how have no prior experience with async code, without introducing them to callbacks, promises and async behavior in general first.

I also see a major possible footgun in code that uses await syntax like this:

(@kaizhu256 I'm sorry that you provided a convenient example)

These two functions are called in sequence when there is actually no dependency between the two calls, and they could in fact run in parallel, which would reduce the overall time to complete the async function.

I'm not against async/await and I'm all for promises, but there are people who think that they do not have to concern themselves with async behavior, because they can just stick an await in front of it and it's synchronous again.

1 Like

Thank you @TitanNano for that excellent, well thought out response, this is certainly the best rebuttle I've heard against Javascript's class syntax. You've given me a lot to think about, so it may be some time for me to put my thoughts together with a better response.

I especially like how you responded to my question of "Is the overall paradigm of using prototypes superior to that of class-based inheritance?"

I can see that anyone who's coming from this point of view would think of class syntax as pretty useless - so one of the things I'm going to have to try and figure out is exactly what's supposed to be the benefit of the class syntax (I do beleive they were added for more reasons than just "tailoring to those coming from other languages" and to make "javascript a grown-up language", like people like to toute around - but I'll have to sort out my thoughts on that as well and see how many of the benefits actually lie on solid ground, and how many are more opinion oriented).