Hacker News new | past | comments | ask | show | jobs | submit login
How to rewrite classes using closures in JavaScript (gaurangtandon.com)
25 points by gaurang_tandon on Oct 22, 2023 | hide | past | favorite | 57 comments



The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."

Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.

On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.

https://wiki.c2.com/?ClosuresAndObjectsAreEquivalent


Yeah, I'm losing my edge. The kids are coming up from behind. I'm losing my edge. I'm losing my edge to the kids from Stanford and from CMU.

But I was there. I was there in 2008. I was there at the first Expo show in IRC. I'm losing my edge to the kids whose fingertips I hear when they get on the decks. I'm losing my edge to the Internet seekers who can tell me every member of every JS design committee from 2008 to 2018.

I'm losing my edge. To all the kids in typescript and wasm. I'm losing my edge to the bootcamp Brooklynites in little jackets and borrowed nostalgia for the unremembered nineties.

I was there. But I was there. I can hear the fingers every night on the decks. I was there in 2008 at the first search engine collapse. I was working on MVC frameworks with much patience. I was there when Ruby started up test-driven development. I told them, "Don't do it that way. You'll never make a dime."

I was there. I was the first guy playing dynamic dispatch to the php kids. I played it at phpclasses. Everybody thought I was crazy. We all know. I was there.

I've never been wrong. I used to work in the dev store. I had everything before anyone. I was there in the C# linq booth with Anders. I was there in Glasgow during the FP clashes. I woke up typing on the beach in frontpage in 1998.

But I'm losing my edge to t-shit clad people with proprietary ideas and more investment. And they're actually really, really nice.

I'm losing my edge. I heard you have a compilation of every good quine ever written by anybody. Every great patch by Linus Torvalds. All the flamewar hits. All the sourceforge tracks. I heard you have a floppy of every 90s asm rpg on German import. I heard that you have a white label of every seminal Gang-of-Four techno hit - 1995, '96, '97. I heard that you have a CD compilation of every good '60s fortran book and another box set from the '70s.

I hear you're learning vim and lifetimes and are throwing your c compiler out the window because you want to make something safe. You want to make a web app.

I hear that you and your team have deleted your .NET repos and installed npm packages. I hear that you and your band have deleted your npm packages and installed F#.

I hear everybody that you know is more relevant than everybody that I know. But have you seen my records? Wirth, Peyton-Jones, Dijkstra, Fortran, C, C++, D, R, Haskell, PHP, Python, Scala, Java, C, ... malloc, gcc, #include, while(*s++), ...

You don't know what you really want


Nice. I want to see this set to the music and put on YouTube.


It’s especially nice because now every instance has to carry around every copy of every method, type testing is impossible, and if you’re lucky you’re even making every callsite megamorphic and defeating all those pesky optimisations we all hate.


There are several points in the article which are just plain wrong:

- no private properties (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...)

- no readonly properties: You can use a getter. Or define the property as non-writable, the syntax isn't nice but in my opinion it's still much nicer than what the article proposes.

Some of the points are just a matter of taste ("this" is awkward)

There's the implementation itself which forces you to use non-idiomatic code for no good reason or benefit (Class.init instead of new, just why? You can absolutely return a constructor function there while preserving everything else)

Doing what the article proposes also destroys the ability to do instanceof checks because the prototype of instances is not set (can be fixed), and inheritance is severely limited if you want to preserve any of the purported benefits. You might say that inheritance is actually not a good thing and so it's a feature, not a bug, but if that's your opinion why are you trying to mimic a class?


Agreed. I spent a small epoch of my career trying to make js have "classes" in the heady days of OOP. It took a bit to realize that idiomatic js didn't need them. Besides being sold so hard on OOP as a concept, failing to understand prototypes was one of my biggest mistakes. By the time they landed, I was actually pretty disappointed to see the language get classes.


> By the time they landed, I was actually pretty disappointed to see the language get classes.

TBF classes are just a layer of syntactic sugar over the (awful) prototypal system (which somewhat sadly few were interested in making better let alone good).

Behind the scenes, they pretty much just create a ctor function and prototype function, except you don't have to mess with the terrible `Object.create` and `.prototype =` nonsense.

Abandoning constructors and going full-on with prototypes would be an other thing entirely, but the reality is that javascript is not very good at it so it's not very fun (except as a proof of concept / investigation of prototype based OO), and pretty much nobody is interested.


Yeah, that's fair. I guess I don't really use prototypes anymore either. I find modules and closures are intuitive and powerful, though very different conceptually from classes/prototypes, tools for handling things like private state. Really OOP is what I've grown away from over the years in most languages. I even enjoy writing C++ in this style since its capabilities around things like closures has improved, anonymous namespaces make for a similar "module" scope to what you find in js.


Lord, no, please, please don't do this in any codebase that anyone else has to actually look at/contribute to.

Other commenters have pointed out some of the technical issues with this implementation (all instances get a copy of every method, no prototype-based type checking, non-standard use of "init" vs new, etc.) But the much more important issue is that you did not make life easier for other developers looking at your code. Instead, you just forced them to learn one additional, non-standard way of doing things with very little, if any, real benefit.

Developers need to learn to be extremely judicious whenever you use unusual/non-standard patterns in code. Everyone already needs to learn the standard way of doing things, so there should be a giant bar in terms of things like productivity enhancements or performance improvements if you're forcing everyone to learn your non-standard pattern. Avoiding "this" doesn't cut it.


> non-standard way of doing things

Fwiw this exact pattern was the idiomatic defacto standard in JS before classes were added to the ECMA specs. This will be extremely familiar to anyone with a reasonable number of years of experience in JS.

That's not a good defense in itself: things should ideally be familiar to beginner devs, but that's more of a debate on what's being taught in current JS education than on what's "better".

i.e.: if this were a better pattern (it's not), lack of familiarity wouldn't be a good argument against it.

> with very little, if any, real benefit.

In fairness the article spends most of its time laying out the proposed benefits, so stating that there's no benefit without addressing its arguments is a little lazy.

Some of the arguments are inaccurate (private fields have been widely supported since 2021) but it's true that ECMA's class implementation is riddled with issues & exploring alternative patterns is worthwhile.

Even before ECMA added classes, I never liked this pattern: it seems a clumsy attempt to shoehorn classical inheritance -esque arrangements onto a prototypical language without ever putting forward reasoned arguments for the benefits of doing so. This article at least tries to put forward arguments about why fake shoehorned classical inheritance pattern is better than the "standardised" shoehorned classical inheritance pattern, but in reality both are bad.


> Fwiw this exact pattern was the idiomatic defacto standard in JS before classes were added to the ECMA specs.

No it wasn’t. Some people used it, to be sure, but more used the prototype chain properly, storing data in `this` and all that.


Totally agree with the other response. This absolutely was not standard before class syntax was introduced. Use of constructors, new, this, etc. was the standard - class was essentially just a back port, adding syntactic sugar on top of the prototype design that was inherent to JS from the beginning. The only time I saw something like this was just from people that didn't really "get" or understand inheritance or OOP.


I'll never work with someone like the author in a team.

Just because of your personal opinions and certain things that don't matter in the end, you want to ruin everybody's development experience.

The proposed solution is way uglier than the standard syntax, and you'll be lucky to get any intellisense or type checking out of it.


Except that classes are more memory efficient and better optimized in the VM.

This will package a copy of the functions with every instance you make, the class variant will only create member variables for each instance.

Maybe save a few bytes down the wire with a different developer experience for fundamentally more bloated code.


The good old let-over-lambda http://www.csci.viu.ca/~wesselsd/courses/csci330/slides/lol.... . Now, in JS AFAIK this causes high memory usage, as "methods" are separate for each instance.


Love that book. Particularly the fact that the method-to-function mapping doesn't have to be one-to-one. You can always implement multiple methods in a single function (and for prototyping with closures this is usually a good idea).


Oh look, OP reinvented the revealing module pattern used before the class keyword was a thing.

And anyway, all of the stated problems either are none or can be solved by using TS.


Seems very odd to hate classes and still insist on doing OOP. If you just break the data and the functionality apart you don’t have to worry about any of this.


please dont do this in any codebase other people are working on


> The this prefix is mandatory for every instance property, which increases code bloat.

what does "bloat" mean in this sentence? people use "bloat" liberally nowadays.


The author might have meant “boilerplate.”


They mean bloat as in source code bloat as in the source is larger because you have to prefix every method call with “this.”

It’s… a take, for sure.


There are private properties in classes

class Foo { #ImPrivate = true; #meToo() { if(this.#ImPrivate) cantTouchThis(); } }

new Foo().#meToo() === BOOM

Also with using closures for "classes" takes up a whole lot more memory because new instances off all the closures are created for each instance of the class.


There are also getter (and setter) properties that can be backed by private fields:

get name() { return this.#name; }

Half of this article seems to be based on the author’s unawareness of JS class syntax.


The optimization points are good to keep in mind, no?

See for example:

https://github.com/rollup/rollup/issues/349

I think it is good to know that there is no tree shaking for class methods, even when public.

This is a valid consideration IMO.

The missing minification of identifiers and properties of the Vue instance in general were always bugging me in Vue 2, even when not using the class keyword.


Private properties don't work with Proxy.

https://lea.verou.me/blog/2023/04/private-fields-considered-...

Quote:

> Private fields, proxies, pick one, you can’t have both.


For the same reason, they are forbidden in MobX. Hello underscore!

And thank god for TypeScript compiler's private keyword! While... I can see how this plethora of possibilities looks confusing.


Private and readonly properties are 100% possible on classes, and you don't even need the newfangled private property syntax! You can roll it your own easily, and all you need is WeakMap. Unfortunately nobody seems to understand what weak maps are for...

Here's the example code: https://gist.github.com/conartist6/95f5be3488b2ae6c0dfd9a886...


There are truly private properties available now.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...


This is how everyone wrote classes in JS[1] before the class keyword was added. Most people didn't want to futz with constructor functions and prototype chains. Eventually a class keyword was added to the language.

"Having to write:

this.progressBar.addEventListener(this.handler.bind(this)); is much worse compared to:

progressBar.addEventListener(handler);"

If handler is declared as:

class Foo {

  handler = () => {
    // handle
  }
}

Then there's no need to bind it to this when using it later.[2]

You can just write

this.progressBar.addEventListener(this.handler);

Additionally if handler doesn't reference any other class properties and isn't referenced outside the class, it can also be a function in the module.

class Foo {

  constructor() {
    this.progressBar.addEventListener(handler)
  }
}

function handler () {

  // handle
}

1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...


"Closures are poor man's objects, and vice versa" [0]

[0] - https://stackoverflow.com/questions/2497801/closures-are-poo...


Amusing bug:

    console.log(`Average height for dogs is ${AVERAGE_HEIGHT_FT} kg`);
Height in kg? ;D


Those are feet kgs. Like light years.


It says the average weight is 100 kg so these are some pretty big dogs. I’d expect each foot to be a kg.


IMO before the before and after patterns are a mess compared to a struct in Rust etc or a dataclass in Python. `this` or `self` is important to be explicit; C++ is an interesting case in that it gives you a choice; `this->` is the better approach than implicit, which is ambiguous.

I can't tell from a glance from either before or after what the fields of the class are, let alone what sort of data goes in them. I suspect my IDE would have a struggle as well, and allow mistakes, at runtime, to either raise exceptions, or perform incorrect behavior.

I think maybe a cleaner approach in this domain may be TS interfaces + standalone functions that act on them.


Typescript wil not help, you can create the same mass. What me helped is to write almost all code functional and not oop. Every method had just 1 way data can in and out the method, no side effects. Classes have a lot of flexibility that make things complex and increase bugs. If you are strict in what to accept and what to produce, and start with the test, if you create mess, you can eat refactor to create clean code.


Thanks everyone for the super insightful discussion! I have learnt a lot from the comments, and I have also updated my blog post accordingly. You can find the updated version at the same link.


Funny thing is that a lot of developers like that JavaScript received class support, and makes it easier to write oop. Many people dient understand 'this' in JavaScript, and ways are created to reduce the usage of 'this'. And now people are switching from classes, a feature many want before and complained it was not in JavaScript. I think we always have to accept the language how it is, and dont find weird ways to do things the language is not made for. (like e.g. typescript)


> The this prefix is mandatory for every instance property, which increases code bloat

Stopped reading there. Whenever someone displays this mentality of "less code is better, every character above the minimum necessary is bloat" I immediately assume they've never had to read complex code written by someone else (or even their past selves) in their life.


What is the best and most modern way to write a module where you have public and private functions and data?


I'd say you might need to be more specific with your phrasing.

If you want replicate more class-like behavior (but without actual class/inheritance information):

- Some type of exported constructor function

- The function instantiates an instance of data internally that is possibly mutable (let vs const)

- That function returns an object/interface of functions that have access to that data by virtue of it being defined in a higher but accessible function scope

- All other private functions are simply not exported

Or you might mean singleton data:

- let or const data at the file/module scope level

- export functions to operate on the data

- Don't export functions you want to be private

The first is sort of reinventing a class instance, but if you are anti-inheritance for the most part (likely a good idea), then you can use a "kind" or "type" property on the returned constructed plain object if needed (TypeScript generics make this quite ergonomic to represent as well). So, you don't really need classes, but they have their uses and have familiar and comfortable syntax to programmers from other languages. You can use either and they are both accepted as idiomatic.

I usually still allow classes in codebases that I have some control over, but add a custom ESLint rule to ban extension unless with an ignore comment. This sort of encourages you to compose more like the functional/data approach, but allows you to still use instanceof since it's an actual class. It also means you're not creating a copy of all the method functions if you don't need to between instances.

There is a caveat that there is a class syntax:

method = () => {}

That auto-binds the method to this, creating that method copy for all. In this case you're encroaching on the plain object behaviour, again with a more familiar syntax.

So, I guess it's more messy than I thought reflecting on it. But you have some choices depending on what properties you'd like to encourage in your codebase.


Context is if I am writing a node app from scratch in JavaScript, I’d like to use all the best practices for laying out my code.

With so many options, it’s hard to know what you’re doing is “right” and to setup the appropriate lints and whatnot.


classes have the # prefix for private members and TS has the private keyword.

If you don’t want to go with classes, not exporting a value or a function from a module is almost the same. The rest can be done with closures.


Is there a “canonical” example of a module that is written well and uses what most would consider best practices?

When looking through npm modules, there still seems to be a variety of approaches.


Canonical? Best practices? Not that I’m aware of. Not in Node.

Deno has some ideas though:

> Don’t import any symbol with an underscore prefix: export function _baz() {}.

> Don’t link to / import any module whose path […] Has a name or parent with an underscore prefix: _foo.ts, _util/bar.ts.

https://deno.land/std@0.204.0


This is so weird! It seems like the author mostly just hates the aesthetics of classes but actually likes structuring code in an OOP way. Good on them though, I've never written a class in my life personally, so at least they're on the right path. :)


I learned how to do this shortly before JS added class syntax. While the class syntax is more ergonomic, I appreciate how this technique allows all standard OOP features and it emerges only from first-class functions and objects.


I avoid classes, but 'this' is not too horrible if you deconstruct the objects/methods/variables you need from it. 'const {foo, baz}=this;'


Yes, I use this technique and I’m glad you found it too. Also note you can put public get and set functions in the return object if you allow for a little munging.


I wonder if React has had any impact on people's general tendency for reaching for classes vs. closures, particularly after hooks were introduced.


Thing is, hook state looks like closures, but it's not the same at all.

The machinery behind the scenes might be ugly, but this syntax is also what I love about React Hooks.

I was preferring closures to ES6 classes before using React, despite the performance implications.

Since switching to TypeScript I also like ES classes though.

Especially for things like MobX... but even without it.

Regardless of React, I feel like classes are the context where getters and setters feel most natural and ergonomic.

And classes are great for grouping state and behavior in an easy-to-understand way.


I think it made using classes more popular before hooks, and drove--along with functional components--the more general popularity of functional style of js afterwards. I feel like the latter are the idioms you will find most common in modern code, docs, and training today; though class components are still fully supported.


Classes are just synthatic sugar over closures anyway. The benefit of OOP classes is that they can be extended. But closure cannot.


I never used vanilla Js, but it seems your issue with class is almost entirely fixed by using typescript.


> `this` is awkward

`this` is excellent for maintenance (being able to easily distinguish between instance variables and other things is very valuable), and generally good for performance (more subjective and nuanced involving runtime and memory and consideration of how often you instantiate and how often you use and I won’t provide any citation).

> No private properties

This is where the article falls down; and everything after it is suspect. Because all browsers have supported private class fields since mid-2021. For almost all purposes, this has fairly recently become good enough to depend on. Private class methods is right on the verge, since Safari added it in 15.0 (September 2021) rather than 14.1 (April 2021) like private class fields; I’d say that most purposes can safely depend on this now.

Reference: https://developer.mozilla.org/docs/Web/JavaScript/Reference/...

> No readonly properties

  class MyClass {
    static get prop() {
      return 123;
    }
  }
  MyClass.prop = 456;
In strict mode (e.g. ECMAScript Modules), that last line will throw a TypeError; otherwise, it will silently do nothing. (That’s how non-writable properties work in JavaScript.)

The more-efficient-but-less-friendly-to-some-tooling alternative is placing this after the class:

  Object.defineProperty(MyClass, "prop", { value: 123, writable: false });
> Poor bundler optimization

There is some legitimacy to this claim, but it’s entirely because all relevant tooling is surprisingly primitive. And most of the places where it seems legitimate, your replacement will suffer from the same issue.

But a lot of it is actually not legitimate: if you only used private fields, they would be optimised, detected as unused, &c. in all these ways.

The example is also a bit dubious in points like the AVERAGE_HEIGHT_FT field on Dog: writing it that way rather than as private or (typically my preference) as a const adjacent to Dog implies it’s supposed to be public (and the naming of these static fields supports this).

And .height and .weight being part of the public interface? I expect that to be a feature, not a bug. .weight is not unused, unless you check the whole code base and find it so. (And the surprisingly-primitive tooling can’t do that whether you use a class or an object.)

> Closures to the rescue! […] Immediately, we see several advantages:

Four are listed. Numbers 1, 2 and 4a are broadly or completely false because you just needed to use private fields. 4b has also been addressed (readonly is just spelled using a getter function).

The only interesting claim is the third:

> 3. We have lesser code bloat thanks to removing this. and Dog. prefixes.

You probably have a smaller bundle size, but it wasn’t bloat: by removing it, you’ve vastly increased memory usage requirements and normally slowed things down, because now instances share almost nothing (only static fields), instead of almost everything (all except for instance data).

—⁂—

All up, I say: try using private properties for a while, and measure various aspects of performance: actual bundle size savings, plain and gzipped; memory usage; and performance if you have any places slow enough to actually measure anything. Then come back and we can consider the topic anew.


Thanks for that (and no thanks for this)! You won't find too many this's (or deeply nested properties of any kind) in my own decade+ long adventure with JavaScript.

Will bookmark for future reference.


Glad you liked it :)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: