7 ms·
How to rewrite classes using closures in JavaScript
- bduffany 3y agoI wonder if React has had any impact on people's general tendency for reaching for classes vs. closures, particularly after hooks were introduced.
- moritzwarhier 3y agoThing 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.
- nurple 3y agoI 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.
- gaurang_tandon 3y agoThanks 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.
- linuxontheweb 3y agoThanks 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.
- gaurang_tandon 3y agoGlad you liked it :)
- pard68 3y agoI avoid classes, but 'this' is not too horrible if you deconstruct the objects/methods/variables you need from it. 'const {foo, baz}=this;'
- rabbits_2002 3y agoplease dont do this in any codebase other people are working on
- masklinn 3y agoIt’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.
- pzmarzly 3y agoThe good old let-over-lambda http://www.csci.viu.ca/~wesselsd/courses/csci330/slides/lol.pdf 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.
- bandrami 3y agoLove 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).
- pleoxy 3y agoExcept 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.
- w3news 3y agoFunny 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)
- hyperhello 3y agoYes, 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.
- rickreynoldssf 3y agoThere 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.
- stupidcar 3y agoThere 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.
- moritzwarhier 3y agoThe optimization points are good to keep in mind, no? See for example: https://github.com/rollup/rollup/issues/349 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.
- _nalply 3y agoPrivate properties don't work with Proxy. https://lea.verou.me/blog/2023/04/private-fields-considered-harmful/ https://lea.verou.me/blog/2023/04/private-fields-considered-... Quote: > Private fields, proxies, pick one, you can’t have both.
- moritzwarhier 3y agoFor 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.
- jrsj 3y agoSeems 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.
- d3w4s9 3y agoI'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.
- azangru 3y ago"Closures are poor man's objects, and vice versa" [0] [0] - https://stackoverflow.com/questions/2497801/closures-are-poor-mans-objects-and-vice-versa-what-does-this-mean https://stackoverflow.com/questions/2497801/closures-are-poo...
- lemper 3y ago> 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.
- mumblemumble 3y agoThe 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 https://wiki.c2.com/?ClosuresAndObjectsAreEquivalent
- mjburgess 3y agoYeah, 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
- MatthiasPortzel 3y agoI 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.
- metadat 3y agoAmusing bug: console.log(`Average height for dogs is ${AVERAGE_HEIGHT_FT} kg`); Height in kg? ;D
- ec109685 3y agoWhat is the best and most modern way to write a module where you have public and private functions and data?
- gherkinnn 3y agoclasses 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.
- ec109685 3y agoIs 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.
- gherkinnn 3y agoCanonical? 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 https://deno.land/std@0.204.0
- lucasyvas 3y agoI'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.
- gherkinnn 3y agoOh 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.
- Kwantuum 3y agoThere are several points in the article which are just plain wrong: - no private properties (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields 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?
- nurple 3y agoAgreed. 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.
- masklinn 3y ago> 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.
- hn_throwaway_99 3y agoLord, 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.
- lucideer 3y ago> 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.
- chrismorgan 3y ago
- moltar 3y agoClasses are just synthatic sugar over closures anyway. The benefit of OOP classes is that they can be extended. But closure cannot.
- conartist6 3y agoPrivate 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/95f5be3488b2ae6c0dfd9a8867fe2330 https://gist.github.com/conartist6/95f5be3488b2ae6c0dfd9a886...
- moltar 3y agoThere are truly private properties available now. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
- the__alchemist 3y agoIMO 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.
- w3news 3y agoTypescript 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.
- triceratops 3y agoThis 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/Reference/Operators/new https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... 2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#arrow_functions https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
- thot_experiment 3y agoThis 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. :)
- chrismorgan 3y ago> `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/Classes/Private_class_fields 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.
- bakugo 3y ago> 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.
- orwin 3y agoI never used vanilla Js, but it seems your issue with class is almost entirely fixed by using typescript.