Hi Edward, I have the same experience with your approach to use type and functions more than class for simple procedure, transforms, api-layer, and do it in a semi Data Oriented Design way.
The root of the problem is actually the by-the-book OOP usage which doesn't scale for huge projects.
I still use class though to scope different logics and contain data that seemingly has its own runtime.
Interesting idea with DOD Alan, this paradigm seems interestingly common in heavy computation.
It is common in heavy computation. DOD is an old code structure design, it was found again on the age of PS3 if I'm not mistaken, because the machine limitation cannot match game developers ambition at that time.
I wonder though, would you be able to achieve similar code structure with interfaces?
With TypeScript's type (interface is just a syntactic sugar of TypeScript's type) and function, you can achieve a similar code structure, although for a different reason. In programs with heavy computation DoD helps with creating cache-friendly code, in the javascript environment, performance doesn't seem to increase much, but in terms of readability and extensibility, it really improves them.
Full-time web dev; JS lover since 2002; CSS fanatic. #CSSIsAwesome
I try to stay up with new web platform features. Web feature you don't understand? Tell me! I'll write an article!
He/him
Class in JS is just merely syntactic sugar. It introduces extra layer of complexity with virtually no gain.
I have to disagree here. While it's true that the class syntax in JS is syntactic sugar on top of functions, that sugar is very sweet. You say it's an extra layer of complexity, but that depends on where you look: it's true that it's an extra layer of syntax on top of the underlying functions and prototype chains, but in my experience class declarations are often way less complex than constructor function and prototype chain statements for the developer reading them.
These two block of code do exactly the same thing. Which is more complex?
functionAnimal(name,energy){this.name=namethis.energy=energy}Animal.prototype.eat=function(amount){console.log(`${this.name} is eating.`)this.energy+=amount}Animal.prototype.sleep=function(duration){console.log(`${this.name} is sleeping.`)this.energy+=duration}Animal.prototype.play=function(duration){console.log(`${this.name} is playing.`)this.energy-=duration}functionDog(name,breed,energy){Animal.call(this,name,energy)this.breed=breed}Dog.prototype=newAnimalDog.prototype.speak=function(){console.log(`${this.name} says, "Woof!"`)}
vs...
classAnimal{constructor(name,energy){this.name=namethis.energy=energy}eat(amount){console.log(`${this.name} is eating.`)this.energy+=amount}sleep(duration){console.log(`${this.name} is sleeping.`)this.energy+=duration}play(duration){console.log(`${this.name} is playing.`)this.energy-=duration}}classDogextendsAnimal{constructor(name,breed,energy){super(name,energy)this.breed=breed}speak(){console.log(`${this.name} says, "Woof!"`)}}
I would strongly argue that the second is less complex for the developer using the code.
Unless what you meant was that classes as a paradigm are syntactic sugar that add unnecessary complexity, regardless of the language in question, in which case I have to disagree even more strongly, though that's a much longer conversation.
One more thing that's bugging me though:
John Peters: Saying the class is more complex is subjective.
Edward Tam: It is not subjective unless you can point out what can not be achieved without class.
That's just not true. Even if we suppose that classes and functions provide exactly the same capabilities and classes provide no new unique powers, the complexity that matters more in day to day life is the mental complexity for readers of the code. Some developers have an easier time thinking in functional terms and passing objects around between functions. Others (myself included) often find it far easier to think in object oriented terms, where functions are expressed as methods on objects rather than passing objects to functions. In practical terms, it's deeply subjective which paradigm is more complex.
Thanks Ken for the elegant explanation of why some of us prefer the Class construct. Indeed it is more simple; in my mind. It promotes encapsulation in a more clear manner; to me, as well as a default strict mode.
Lots of old school JavaScript interviewers will still ask you about "hoisting" which is non-existent by default with the Class. Why do anything but use the Class? If it's not an issue any longer, why do interviewers want to test your knowledge of "hoisting" even today?
Full-time web dev; JS lover since 2002; CSS fanatic. #CSSIsAwesome
I try to stay up with new web platform features. Web feature you don't understand? Tell me! I'll write an article!
He/him
Thanks for the complement, now let me disagree with you 😁
Strict mode isn't something I really worry about; I write strict JS by habit at this point, and I especially don't worry about it since I've started using modules, which are also strict mode by default.
I definitely wouldn't say that hoisting is irrelevant or not an issue any more. One way I still use it all the time is when I write utility scripts. Consider this script:
// weekly-metrics.jsconstfs=require('fs')constes=require('elasticsearch')const{sslInfo,outputFile}=processCmdLineArgs()constclient=initClient(sslInfo)constrawMetrics=getRawMetrics(client)constmetrics=processMetrics(rawMetrics)fs.writeFileSync(outputFile,JSON.stringify(metrics,null,2))////////////////functionprocessCmdLineArgs(){// ... code to process command line arguments}functioninitClient(sslInfo){// ... code to initialize an ElasticSearch client}functiongetRawMetrics(client){// ... code to query ElasticSearch for some metrics data}functionprocessMetrics(client){// ... code to process and format raw results into a custom format}
This is the sort of layout I like to use for my scripts because I can open up the file and immediately see an outline of the main steps of the script, then ctrl+click my way into whichever part I need to read or mess with at the moment. This works because of hoisting. All the functions for individual steps are hoisted to the top of the file so they're immediately available.
Tragically, I also still work on an old Angular.js 1.x project, and we do something similar to structure our Factory and Service files:
In this case, the returned object at the top of the service acts as a sort of Table of Contents for the service. Anyone who opens that service knows immediately what methods it provides, and again, they can ctrl+click directly to the one they're concerned with.
So I wouldn't say that hoisting doesn't matter any more. And as you can probably tell by my examples here, I also don't think classes are the best thing for all cases. But I do love me some classes when dealing with a large amount and variety of structured data with data-type-specific functionality that can be encapsulated as class methods!
"This is the sort of layout I like to use for my scripts because I can open up the file and immediately see an outline of the main steps of the script, then ctrl+click my way into whichever part I need to read or mess with at the moment. This works because of hoisting. All the functions for individual steps are hoisted to the top of the file so they're immediately available."
"I can immediately see the outline"...
Do you mean at the program layer? Is this really different than a class construct where as you pointed out the syntax doesn't require the function keyword?
"ctrl+click my way into whichever part I need to read or mess with at the moment"
From the command line of an IDE? Or in browser console/debug mode?
"Anyone who opens that service knows immediately what methods it provides, and again, they can ctrl+click directly to the one they're concerned with."
I hated AngularJs for the inability to get Service's to expose their properties and functions using Intellisense/Autocomplete. I think you are showing something that worked for AngularJs?
"So I wouldn't say that hoisting doesn't matter any more."
How would you define Hoisting and when to use it over other constructs?
Full-time web dev; JS lover since 2002; CSS fanatic. #CSSIsAwesome
I try to stay up with new web platform features. Web feature you don't understand? Tell me! I'll write an article!
He/him
Do you mean at the program layer? Is this really different than a class construct where as you pointed out the syntax doesn't require the function keyword?
I do mean at the program layer, yes. As far as whether it's really different than a class, I mean... yeah, it is. How would you restructure that program to use a class? Unless you want to go the Java route and use a Main class:
classMain{constructor(){const{sslInfo,outputFile}=this.processCmdLineArgs()constclient=this.initClient(sslInfo)constrawMetrics=this.getRawMetrics(client)constmetrics=this.processMetrics(rawMetrics)fs.writeFileSync(outputFile,JSON.stringify(metrics,null,2))}processCmdLineArgs(){// ...}initClient(sslInfo){// ...}getRawMetrics(client){// ...}processMetrics(rawMetrics){// ...}}// run MainnewMain()
If that's what you mean by using a class construct, then we just have very different preferences, because that's a nightmare to me.
But even if that's your personal preference, the point is that I still see plenty of code written this way by other developers, and it doesn't make sense unless you understand that function declarations are hoisted. My comment was mostly in response to when you said:
Lots of old school JavaScript interviewers will still ask you about "hoisting" which is non-existent by default with the Class. Why do anything but use the Class? If it's not an issue any longer, why do interviewers want to test your knowledge of "hoisting" even today?
My point is that hoisting is still a fundamental aspect of JavaScript and it's pretty important to understand, or at least be aware of, even if you have to look it up from time to time. Not understanding hoisting can lead to some subtle, hard-to-find bugs; I've been there a few times.
"ctrl+click my way into whichever part I need to read or mess with at the moment"
From the command line of an IDE? Or in browser console/debug mode?
Either, I suppose, though I was mostly thinking of an IDE context. My point is that anyone reading my code (including myself) can open the file and see the overview of the file, then quickly jump to the relevant function using standard IDE tools. And IIRC, ctrl+click works in browser dev tools' "Sources"/"Debug" tabs as well.
I hated AngularJs for the inability to get Service's to expose their properties and functions using Intellisense/Autocomplete. I think you are showing something that worked for AngularJs?
Totally agree, my biggest frustration with AngularJS is the total lack of intellisense. And no, unfortunately, this doesn't help with intellisense; in fact, it's more important in cases where intellisense doesn't work. The point is that when you see some code saying, dataService.getVendor(id), you can jump over to data-service.js, easily see the available methods, and ctrl+click stright to getVendor() without needing ctrl+f or other mechanisms. It's even more useful in a service that uses its own methods internally, since ctrl+f is less useful in that case.
How would you define Hoisting and when to use it over other constructs?
Hoisting is a step in the interpretation of a JavScript scope wherein all declarations are basically plucked out of their place in the script and moved to the top of the scope. It's not really something you use or don't use, it's just the way JavaScript works.
It's important to note, however, that declarations are hoisted, but initializations are not. So for example, in this code:
You'll get undefined as your output. However, function declarations are special because they don't have separate declaration and initialization steps, it's all-in-one, so they get hoisted completely to the top:
(functiondo(){console.log(x)functionx(){console.log('hey!')}})()// Output:// => function x() { console.log('hey!') }
And const and let are a bit weirder. They do get hoisted, but there's this weird thing called the Temporal Dead Zone where they're defined but not initialized and you'll still get errors if you try to reference them... so a lot of people say they "aren't hoisted", which is technically not true, but it might as well be.
Hopefully that all made sense... it's a weird bit of rather esoteric JavaScript knowledge that you can basically look up when you need it.
Hi Ken, I will start with my appreciation for you taking the time to type in some examples for the discussion. That really helps understanding your rationale and where our disagreement come from.
Let's address the code comparison first. I think class makes it less complex only if you have to stick with inheritance. Let's take a look at the following code:
constAnimal=(name,energy)=>{let_energy=energylet_name=namereturn{eat(amount){console.log(`${_name} is eating.`)_energy+=amount},sleep(duration){console.log(`${_name} is eating.`)_energy+=amount},play(duration){console.log(`${_name} is playing.`)_energy-=duration}}}constDog=(name,breed,energy)=>{let_breed=breedreturn{...Animal(name,energy),speak(){console.log(`${_name} says, "Woof!"`)}}}
I went with a factory approach (or what Alan previously mentioned, a semi Data Oriented Design), and the code pretty much look the same as your example with class, except there is no class. However, the benefit here is that you can adopt infinite number of "traits" to your object through composition, which makes it a lot more scalable. On the flip side, you may run into situations when you need multiple inheritance in the case of classes.
Unless what you meant was that classes as a paradigm are syntactic sugar that add unnecessary complexity, regardless of the language in question, in which case I have to disagree even more strongly, though that's a much longer conversation.
This is definitely not I meant. My argument in the discussion here is specifically about classes in Typescript (even Javascript is fine by me. They don't have much gain in my opinion, but there is not much loss either so whichever way is fine).
Again, as Alan has also mentioned in a previous reply, the problem here is the by-the-look OOP. Javascript classes are not actually classes and operate quite different from other class-base languages. I think the problem was not so bad until type enforcement kicks in. You will eventually find yourself running into walls trying to apply common OOP paradigms/pattern. You writing up this post is a good example.
That's just not true. Even if we suppose that classes and functions provide exactly the same capabilities and classes provide no new unique powers, the complexity that matters more in day to day life is the mental complexity for readers of the code. Some developers have an easier time thinking in functional terms and passing objects around between functions. Others (myself included) often find it far easier to think in object oriented terms, where functions are expressed as methods on objects rather than passing objects to functions. In practical terms, it's deeply subjective which paradigm is more complex.
Again, the context here is Typescript. I am not going to claim that class in general is more complex (hell no). But you can see from the example I gave, there is not much different in outcome from a readability perspective, which is what I mean by virtually no gain. The complexity comes from Javascript's expression of classes being misleading and the headaches of trying to apply OOP on top of it.
Full-time web dev; JS lover since 2002; CSS fanatic. #CSSIsAwesome
I try to stay up with new web platform features. Web feature you don't understand? Tell me! I'll write an article!
He/him
That definitely clears up your point quite a lot, and there's a lot less distance between our positions than it seemed at first, so I really appreciate the clarification and your example.
I definitely agree in general with composition-over-inheritance approaches like the one you've demonstrated, especially if you have a wide variety of features that need to be combined depending on circumstance. But to be honest, I haven't really run into many circumstances IRL where it's been an issue.
To return to my specific example, I think there's a lot of value to be found in subclassing a common Model class to represent each table in my database, especially given the use fo static getters to represent metadata like which columns are the primary keys and how different tables are related. Additionally, while your example demonstrates how to simulate private instance vars using closures with the _name, _energy, and _breed vars, if you were to modify your example above to add these vars to the returned object and to add static properties to the factory functions, it just feels more and more to me like you're writing a polyfill for the class keyword. And at that point, I honestly feel that class declarations are far more readable than these sorts of factory functions, especially when you have several long-time Java devs on your team who are now coming up to speed on JavaScript (as I do) and you want to give them a graceful transition.
And that actually brings me to something else that I'd love to hear your feedback on. You said:
The complexity comes from Javascript's expression of classes being misleading and the headaches of trying to apply OOP on top of it.
This is something I've heard expressed many, many times by other devs, that JavaScript's classes are fundamentally different from classes in other languages in important ways that make them misleading to devs who transition from these other languages. But in my experience, having written classes in Java, Python, Ruby, and JavaScript (and, in case there's any doubt, as someone who has a very deep understanding of JavaScript's prototype system and how extends works under the hood), I just really haven't found that to be true, aside from the lack of certain functionality like private and protected members, which seem to be coming down the pipe anyhow.
So as someone who clearly has both a strong opinion on the subject and a better understanding of by-the-book OOP and classic design patterns than I do (admittedly, my theory suffers there), what do you see as the most important differences between classes in JavaScript and other languages, and most importantly, what do you see as misleading about them? I'm genuinely anxious to know, because I've recently entered a role where, as I mentioned, I'm training up a few long-time Java devs in the ways of the web, and I'm anxious to avoid any misconceptions.
I'll throw some opinion that hopefully will help you with this.
Additionally, while your example demonstrates how to simulate private instance vars using closures with the _name, _energy, and _breed vars, if you were to modify your example above to add these vars to the returned object and to add static properties to the factory functions, it just feels more and more to me like you're writing a polyfill for the class keyword.
Java community and its derivative (Spring Boot, PHP Laravel, Ruby on Rails) to put the "smartness" of their library in the runtime bootstrap process and Objection.js is one of the libraries that walks into their path. In time this will conflict with TypeScript's approach to get types right, which is more of functional approach to type correctness (rust, Haskell, elm).
Let me take a detour a bit:
Edward's example of an object factory has a perfect type and perfect JavaScript encapsulation which is very convenient for people who cares about encapsulation and OOP in general.
constAnimal=(name,energy)=>{let_energy=energylet_name=namereturn{eat(amount){// object of type "function" is created everytime Animal is calledconsole.log(`${_name} is eating.`)_energy+=amount},sleep(duration){console.log(`${_name} is eating.`)_energy+=amount},play(duration){console.log(`${_name} is playing.`)_energy-=duration}}}/*
Back then in my early javascript day I used this exact
pattern to make a mini game (don't laugh), and man it was
damn slow. If you really have to add a method, use
prototype.[fnName] or use class.
*/
If you look closely to the problems you are having, they are pretty much all class oriented.
As Edward might notice, OOP's paradigm to mix data and operations in a single instance a.k.a class is where it gets problematic. And it is encouraged by languages like Java and javascript because the structure/prototype is described in the runtime, thus it can be queried a.k.a Reflection. Reflection is NOT wrong. It's a great feature, but the side effect is that it encourages the wrong approach to a problem, the storytelling.
Structures should be written like how we describe a character in a story, explicit and at once:
struct A{
a: SomeType
b: SomeOtherType
}
And operations should be written like how we describe a scene in a story, step by step, chronologically.
Let that new paradigm sink in. Then, let's rewrite the Animal/Dog code with some bonus code (because I really want to show you guys how this paradigm is development-scalable).
// Using type instead of class because type is zero-cost in the runtimetypeActivity<ActivityTypesextendsstring[],Dataextendsobject>={activityType:ActivityTypes,activityData:Data};typeEntity={activity:Activity};// animal.ts// a type based inheritance-like typeAnimal=Entity&{energy:number,name:string};// dog.tsexportconstDogTypeSiberianHusky:uniquesymbol=Symbol()exportconstDogTypeGoldenRetriever:uniquesymbol=Symbol()typeDogActivity=Activity<"sleeping"|"idle",{duration:number}>typeDog=Animal&{activity:DogActivity,kind:"dog",// literal stringbreed:DogTypeSiberianHusky|DogTypeGoldenRetriever}exportconstDogActivityFns={// create* functions are factory-ishcreateIdle:():DogActivity=>({activity:"idle",duration:-1}),createSleep:(duration):DogActivity=>({activity:"sleeping",duration}),isExpired:(activity:DogActivity)=>activity.duration<=0,canSleep:(activity:DogActivity)=>activity.type==="idle"}exportconstDogFns={// called on render on every entity instantiated in worldonUpdate:(dog:Dog)=>{// assumptions are Dog as an Entity is a mutable object// meaning in this scenario dog object are not recreated { ...dog }// for the sake of performanceconst{activity}=dogswitch(activity.type){case"sleeping":{if(activity.type==="sleeping"){activity.duration-=1if(DogActivityFns.isExpired(activity)){dog.activity=DogActivityFns.createIdle(activity)}}return}}},triggerSleep:(dog:Dog,duration:number)=>{if(!DogActivityHelper.canSleep(dog.activity))returndog.activity=DogActivityHelper.createSleep(duration)}}// And then somewhere on the code// There's an event listener that can be fired outside of the game's main loopactivityEventTrigger.subscribe("triggerSleep",(dog,duration)=>DogFns.triggerSleep(dog,duration))
The code above shows a really easy way to get a pretty complex idea done in a simple manner, only by separating operations and structure as opposed to the classical OOP where method and property is in one place so that it's confusing whether we should write attack() or receiveDamage().
Let's go back to the case of ObjectionJS, we've detoured pretty far.
TypeScript clearly doesn't do much good to the usage of ObjectionJS as it's very complex in terms of type correctness (e.g. idColumn could be string or string[]).
I'd treat ObjectionJS modules as another "unknown territory" in the application, like localStorage.get, fetch, fs.readFileSync, redisClient.get, etc. Let it be functions that return unknown. You could practically give unknown or if not possible any (forgive me, lord) to the idColumn and other static method return type.
Now create a funnel function where it returns that unknown object as a result of ObjectionJS query with a precise type or return an error if it is not the expected result.
asyncfunctionfetchUserFunnel(){constraw=awaitfetchUserWithObjectionJS();if(!User.is(raw)){// decode process, will explain laterreturn{value:null,error:newDecodeError()};}return{value:raw,error:null};}
To scale this pattern let's make a function factory a.k.a higher-order function for it. I'll be using io-ts for the parser to demonstrate the new paradigm. Also, check out io-ts if you haven't, it's a cool lib.
exportclassDecodeErrorextendsError{}// the hoffunctionmakeFunnel<Textendsobject>(validateFn:(t:unknown)=>tisT){return(raw:unknown):{value:T,error:null}|{value:null,error:DecodeError}=>{if(!validateFn(raw)){return{value:null,error:newDecodeError()};}return{value:raw,error:null}}}// the usageexportconstUserCodec=t.type({userId:t.string,userName:t.union([t.string,t.null]),})exportconstUserFromDB=t.intersection([User,t.type({children:t.array(UserCodec)}),]);exporttypeUserFromDB=t.TypeOf<typeofUserFromDB>;exportconstfunnelUser=makeFunnel((t:unknown)=>UserFromDB.is(t))exportconstfetchUserById=(id:string)=>User.query().where("id",id).withGraphFetched('children').then(result=>funnelUser(result[0]))
And using the fetch function would be
// And how to use it would beconstresult=awaitfetchUserById(query.id)if(result.error)returnres.send(500);// corrupted data in the database, data is not contractualreturnres.send(result.value)
If you're really sure that your database will always return the correct schema, do this, but I don't recommend this.
The code is long and it's already the optimal amount of code to achieve type correctness in your application.
I guess enforcing strict TypeScript in the ObjectionJS is not the best angle to approach this problem. ObjectionJS is of a different paradigm, and let it be that way because we can't change how it behaves nor we can put a strict TypeScript rule to it.
Let ObjectionJS be unknown. As a substitute, enforce type correctness in the business logic, detached from the resource facing layer.
Ken;
You had asked "How would you restructure that program to use a class?" with respect to the goodness of "hoisting".
If multiple components need to use something in common, I usually abstract the component to an Angular Service today. It gives me that ability to re-use anything anywhere. The only drawback is that I have to import the service in order to reuse the code within it.
Full-time web dev; JS lover since 2002; CSS fanatic. #CSSIsAwesome
I try to stay up with new web platform features. Web feature you don't understand? Tell me! I'll write an article!
He/him
@Alan, thanks for the in-depth response! I'll have to take some time to read through it and consider it. Interestingly, I notice that Objection.js actually provides typings, though they seem rather convoluted and maybe more intended for internal library use; what do you think? github.com/Vincit/objection.js/blo...
@john
Peters, sure, I think that's a great approach, and personally I don't consider an extra import a drawback at all; if anything, I prefer smaller individual modules, so I'm happy to ad an extra import in exchange for a smaller overall file 😁
@Ken I would take the shortest, easiest, and safest path that scales both in performance and development. TypeScript strict typing is relieving me from constantly fearing "undefined is not a function" issue, but if objection typing turns out to be a burden more than a help to my team, I would consider excluding objection from the strict typing in favor of development efficiency. It's just my opinion though, you know the lib better and might prove me wrong on this.
To return to my specific example, I think there's a lot of value to be found in subclassing a common Model class to represent each table in my database, especially given the use fo static getters to represent metadata like which columns are the primary keys and how different tables are related.
If you find value in using subclassing in your case, then go for it. I am not going to pretend to be the know-it-all :)
having written classes in Java, Python, Ruby, and JavaScript ...
So we pretty much have the same background :)
what do you see as the most important differences between classes in JavaScript and other languages, and most importantly, what do you see as misleading about them?
I think my biggest complaint about JS classes is the encapsulation. Let's see the following code as example:
As you can see person.talk is not encapsulated as a part of the class Person. Instead, the context is related to where it is called instead. This problem is especially painful when you work with frontend code (e.g. React). You will find the context being mutable, and will be forced to bind them everywhere. So as Alan also mentioned in the other reply, I tend to go with functions and closures in order to workaround this hustle.
Full-time web dev; JS lover since 2002; CSS fanatic. #CSSIsAwesome
I try to stay up with new web platform features. Web feature you don't understand? Tell me! I'll write an article!
He/him
Ah I see. Yeah that's definitely a frustration sometimes, and I've dealt with the React issues you mention. That said though, I wouldn't tie that to JavaScript's class system at all; that's just how objects in general work in JavaScript, and it has its pros and cons. While it's true that it can cause some issues, it also facilitates the composition-over-inheritance style you mentioned above, as it lets you copy methods from one object directly to another without any fancy footwork. Pros and cons, I guess
Interesting for sure! I have never seen this before.
Class Encapsulation seems to imply the name property only for the class object. So the behavior you've shown would be expected because mockElement.onClick is not a Person object. Right?
Indeed changing the assignment to const mockElement = this.person gives proper context when calling mockElement.Talk();
It looks to me like the class object's context is not mutable. That's a good thing.
You can actually manipulate the reference of this. This is what makes it misleading in an OOP perspective because you would have thought the context is part of the encapsulation in the definition. The problem is not strictly coming from javascript classes, but more from the discrepancy of the expected behaviour of how class should be vs how javascript class actually is.
Full-time web dev; JS lover since 2002; CSS fanatic. #CSSIsAwesome
I try to stay up with new web platform features. Web feature you don't understand? Tell me! I'll write an article!
He/him
But I really don't think it has anything to do with classes. I think a better statement would be, "The problem is coming from the discrepancy of the expected behavior of how objects should be vs how javascript objects actually are". IMHO, the confusion is identical using a factory:
I can't see this example being any less confusing than the class example just because we don't use the new keyword. The confusion all boils down to this step:
Regardless of how we built the person object, what's confusing is that the talk method loses its this context when attached to something else.
Now of course, one way to solve this problem is to use purely private vars and closures like you did in your Animal example, but personally, I have one really big problem with that approach: it makes the properties themselves inaccessible. You can no longer do doggo.name = 'Fido' to rename your dog. And hey, If all you need is private vars, go for it, but I don't think this approach covers all cases, or even most.
You can, of course, use a getter and a setter for each public property to make them accessible while keeping the closure and its advantages, but at that point the complexity of the code really ramps up while the readability falls, and personally, I just don't know if it's worth the trade-off:
constAnimal=(name,energy)=>{let_energy=energylet_name=namereturn{getenergy(){return_energy},setenergy(energy){_energy=energy},getname(){return_name},setname(name){_name=name},eat(amount){console.log(`${_name} is eating.`)_energy+=amount},sleep(duration){console.log(`${_name} is eating.`)_energy+=amount},play(duration){console.log(`${_name} is playing.`)_energy-=duration}}}constDog=(name,breed,energy)=>{let_breed=breedreturn{...Animal(name,energy),getbreed(){return_breed},setbreed(breed){_breed=breed},speak(){console.log(`${_name} says, "Woof!"`)}}}
That up there feels like a lot of boilerplate to produce an object with three properties, just so I can occasionally write myBtn.click = myDoggo.speak instead of myBtn.click = () => myDoggo.speak().
This is definitely a personal preference, but I don't think the relatively minor tradeoff of context-free methods is worth it. I personally don't use them nearly often enough to justify that kind of a change across the board. If you do, hey, maybe it's for you, but I personally am so used to JavaScript objects and how functions and this work that it's barely even a frustration, and tbh I just really love the elegance of the class syntax. Unpopular opinion, but IMO it will be even better once the class field and private class field syntaxes become standard.
I think a better statement would be, "The problem is coming from the discrepancy of the expected behavior of how objects should be vs how javascript objects actually are".
I think that is a fair statement. Regardless, that was fun discussion and I think I learnt something from it :)
Full-time web dev; JS lover since 2002; CSS fanatic. #CSSIsAwesome
I try to stay up with new web platform features. Web feature you don't understand? Tell me! I'll write an article!
He/him
Hi Edward, I have the same experience with your approach to use
type
andfunctions
more than class for simple procedure, transforms, api-layer, and do it in a semi Data Oriented Design way.The root of the problem is actually the by-the-book OOP usage which doesn't scale for huge projects.
I still use class though to scope different logics and contain data that seemingly has its own runtime.
Interesting idea with DOD Alan, this paradigm seems interestingly common in heavy computation.
I wonder though, would you be able to achieve similar code structure with interfaces?
It is common in heavy computation. DOD is an old code structure design, it was found again on the age of PS3 if I'm not mistaken, because the machine limitation cannot match game developers ambition at that time.
With TypeScript's type (interface is just a syntactic sugar of TypeScript's type) and function, you can achieve a similar code structure, although for a different reason. In programs with heavy computation DoD helps with creating cache-friendly code, in the javascript environment, performance doesn't seem to increase much, but in terms of readability and extensibility, it really improves them.
I have to disagree here. While it's true that the
class
syntax in JS is syntactic sugar on top of functions, that sugar is very sweet. You say it's an extra layer of complexity, but that depends on where you look: it's true that it's an extra layer of syntax on top of the underlying functions and prototype chains, but in my experienceclass
declarations are often way less complex than constructor function and prototype chain statements for the developer reading them.These two block of code do exactly the same thing. Which is more complex?
vs...
I would strongly argue that the second is less complex for the developer using the code.
Unless what you meant was that classes as a paradigm are syntactic sugar that add unnecessary complexity, regardless of the language in question, in which case I have to disagree even more strongly, though that's a much longer conversation.
One more thing that's bugging me though:
That's just not true. Even if we suppose that classes and functions provide exactly the same capabilities and classes provide no new unique powers, the complexity that matters more in day to day life is the mental complexity for readers of the code. Some developers have an easier time thinking in functional terms and passing objects around between functions. Others (myself included) often find it far easier to think in object oriented terms, where functions are expressed as methods on objects rather than passing objects to functions. In practical terms, it's deeply subjective which paradigm is more complex.
Thanks Ken for the elegant explanation of why some of us prefer the Class construct. Indeed it is more simple; in my mind. It promotes encapsulation in a more clear manner; to me, as well as a default strict mode.
Lots of old school JavaScript interviewers will still ask you about "hoisting" which is non-existent by default with the Class. Why do anything but use the Class? If it's not an issue any longer, why do interviewers want to test your knowledge of "hoisting" even today?
Thanks for the complement, now let me disagree with you 😁
Strict mode isn't something I really worry about; I write strict JS by habit at this point, and I especially don't worry about it since I've started using modules, which are also strict mode by default.
I definitely wouldn't say that hoisting is irrelevant or not an issue any more. One way I still use it all the time is when I write utility scripts. Consider this script:
This is the sort of layout I like to use for my scripts because I can open up the file and immediately see an outline of the main steps of the script, then ctrl+click my way into whichever part I need to read or mess with at the moment. This works because of hoisting. All the functions for individual steps are hoisted to the top of the file so they're immediately available.
Tragically, I also still work on an old Angular.js 1.x project, and we do something similar to structure our Factory and Service files:
In this case, the returned object at the top of the service acts as a sort of Table of Contents for the service. Anyone who opens that service knows immediately what methods it provides, and again, they can ctrl+click directly to the one they're concerned with.
So I wouldn't say that hoisting doesn't matter any more. And as you can probably tell by my examples here, I also don't think classes are the best thing for all cases. But I do love me some classes when dealing with a large amount and variety of structured data with data-type-specific functionality that can be encapsulated as class methods!
Ken, thanks for the banter;
"This is the sort of layout I like to use for my scripts because I can open up the file and immediately see an outline of the main steps of the script, then ctrl+click my way into whichever part I need to read or mess with at the moment. This works because of hoisting. All the functions for individual steps are hoisted to the top of the file so they're immediately available."
"I can immediately see the outline"...
Do you mean at the program layer? Is this really different than a class construct where as you pointed out the syntax doesn't require the function keyword?
"ctrl+click my way into whichever part I need to read or mess with at the moment"
From the command line of an IDE? Or in browser console/debug mode?
"Anyone who opens that service knows immediately what methods it provides, and again, they can ctrl+click directly to the one they're concerned with."
I hated AngularJs for the inability to get Service's to expose their properties and functions using Intellisense/Autocomplete. I think you are showing something that worked for AngularJs?
"So I wouldn't say that hoisting doesn't matter any more."
How would you define Hoisting and when to use it over other constructs?
I do mean at the program layer, yes. As far as whether it's really different than a class, I mean... yeah, it is. How would you restructure that program to use a class? Unless you want to go the Java route and use a Main class:
If that's what you mean by using a class construct, then we just have very different preferences, because that's a nightmare to me.
But even if that's your personal preference, the point is that I still see plenty of code written this way by other developers, and it doesn't make sense unless you understand that function declarations are hoisted. My comment was mostly in response to when you said:
My point is that hoisting is still a fundamental aspect of JavaScript and it's pretty important to understand, or at least be aware of, even if you have to look it up from time to time. Not understanding hoisting can lead to some subtle, hard-to-find bugs; I've been there a few times.
Either, I suppose, though I was mostly thinking of an IDE context. My point is that anyone reading my code (including myself) can open the file and see the overview of the file, then quickly jump to the relevant function using standard IDE tools. And IIRC, ctrl+click works in browser dev tools' "Sources"/"Debug" tabs as well.
Totally agree, my biggest frustration with AngularJS is the total lack of intellisense. And no, unfortunately, this doesn't help with intellisense; in fact, it's more important in cases where intellisense doesn't work. The point is that when you see some code saying,
dataService.getVendor(id)
, you can jump over todata-service.js
, easily see the available methods, and ctrl+click stright togetVendor()
without needing ctrl+f or other mechanisms. It's even more useful in a service that uses its own methods internally, since ctrl+f is less useful in that case.Hoisting is a step in the interpretation of a JavScript scope wherein all declarations are basically plucked out of their place in the script and moved to the top of the scope. It's not really something you use or don't use, it's just the way JavaScript works.
It's important to note, however, that declarations are hoisted, but initializations are not. So for example, in this code:
You'll get
undefined
as your output. However, function declarations are special because they don't have separate declaration and initialization steps, it's all-in-one, so they get hoisted completely to the top:And
const
andlet
are a bit weirder. They do get hoisted, but there's this weird thing called the Temporal Dead Zone where they're defined but not initialized and you'll still get errors if you try to reference them... so a lot of people say they "aren't hoisted", which is technically not true, but it might as well be.Hopefully that all made sense... it's a weird bit of rather esoteric JavaScript knowledge that you can basically look up when you need it.
Ken thank you for spending so much time on this. Definitely something for me to study.
Hi Ken, I will start with my appreciation for you taking the time to type in some examples for the discussion. That really helps understanding your rationale and where our disagreement come from.
Let's address the code comparison first. I think class makes it less complex only if you have to stick with inheritance. Let's take a look at the following code:
I went with a factory approach (or what Alan previously mentioned, a semi Data Oriented Design), and the code pretty much look the same as your example with class, except there is no class. However, the benefit here is that you can adopt infinite number of "traits" to your object through composition, which makes it a lot more scalable. On the flip side, you may run into situations when you need multiple inheritance in the case of classes.
This is definitely not I meant. My argument in the discussion here is specifically about classes in Typescript (even Javascript is fine by me. They don't have much gain in my opinion, but there is not much loss either so whichever way is fine).
Again, as Alan has also mentioned in a previous reply, the problem here is the by-the-look OOP. Javascript classes are not actually classes and operate quite different from other class-base languages. I think the problem was not so bad until type enforcement kicks in. You will eventually find yourself running into walls trying to apply common OOP paradigms/pattern. You writing up this post is a good example.
Again, the context here is Typescript. I am not going to claim that class in general is more complex (hell no). But you can see from the example I gave, there is not much different in outcome from a readability perspective, which is what I mean by virtually no gain. The complexity comes from Javascript's expression of classes being misleading and the headaches of trying to apply OOP on top of it.
That definitely clears up your point quite a lot, and there's a lot less distance between our positions than it seemed at first, so I really appreciate the clarification and your example.
I definitely agree in general with composition-over-inheritance approaches like the one you've demonstrated, especially if you have a wide variety of features that need to be combined depending on circumstance. But to be honest, I haven't really run into many circumstances IRL where it's been an issue.
To return to my specific example, I think there's a lot of value to be found in subclassing a common Model class to represent each table in my database, especially given the use fo static getters to represent metadata like which columns are the primary keys and how different tables are related. Additionally, while your example demonstrates how to simulate private instance vars using closures with the
_name
,_energy
, and_breed
vars, if you were to modify your example above to add these vars to the returned object and to add static properties to the factory functions, it just feels more and more to me like you're writing a polyfill for theclass
keyword. And at that point, I honestly feel thatclass
declarations are far more readable than these sorts of factory functions, especially when you have several long-time Java devs on your team who are now coming up to speed on JavaScript (as I do) and you want to give them a graceful transition.And that actually brings me to something else that I'd love to hear your feedback on. You said:
This is something I've heard expressed many, many times by other devs, that JavaScript's classes are fundamentally different from classes in other languages in important ways that make them misleading to devs who transition from these other languages. But in my experience, having written classes in Java, Python, Ruby, and JavaScript (and, in case there's any doubt, as someone who has a very deep understanding of JavaScript's prototype system and how
extends
works under the hood), I just really haven't found that to be true, aside from the lack of certain functionality like private and protected members, which seem to be coming down the pipe anyhow.So as someone who clearly has both a strong opinion on the subject and a better understanding of by-the-book OOP and classic design patterns than I do (admittedly, my theory suffers there), what do you see as the most important differences between
class
es in JavaScript and other languages, and most importantly, what do you see as misleading about them? I'm genuinely anxious to know, because I've recently entered a role where, as I mentioned, I'm training up a few long-time Java devs in the ways of the web, and I'm anxious to avoid any misconceptions.This convo is fun :D
I'll throw some opinion that hopefully will help you with this.
Java community and its derivative (Spring Boot, PHP Laravel, Ruby on Rails) to put the "smartness" of their library in the runtime bootstrap process and Objection.js is one of the libraries that walks into their path. In time this will conflict with TypeScript's approach to get types right, which is more of functional approach to type correctness (rust, Haskell, elm).
Let me take a detour a bit:
Edward's example of an object factory has a perfect type and perfect JavaScript encapsulation which is very convenient for people who cares about encapsulation and OOP in general.
As Edward might notice, OOP's paradigm to mix data and operations in a single instance a.k.a
class
is where it gets problematic. And it is encouraged by languages like Java and javascript because the structure/prototype is described in the runtime, thus it can be queried a.k.aReflection
. Reflection is NOT wrong. It's a great feature, but the side effect is that it encourages the wrong approach to a problem, the storytelling.Structures should be written like how we describe a character in a story, explicit and at once:
And operations should be written like how we describe a scene in a story, step by step, chronologically.
Let that new paradigm sink in. Then, let's rewrite the Animal/Dog code with some bonus code (because I really want to show you guys how this paradigm is development-scalable).
The code above shows a really easy way to get a pretty complex idea done in a simple manner, only by separating
operations
andstructure
as opposed to the classical OOP wheremethod
andproperty
is in one place so that it's confusing whether we should writeattack()
orreceiveDamage()
.Let's go back to the case of ObjectionJS, we've detoured pretty far.
TypeScript clearly doesn't do much good to the usage of ObjectionJS as it's very complex in terms of type correctness (e.g. idColumn could be string or string[]).
I'd treat ObjectionJS modules as another "unknown territory" in the application, like
localStorage.get
,fetch
,fs.readFileSync
,redisClient.get
, etc. Let it be functions that returnunknown
. You could practically giveunknown
or if not possibleany
(forgive me, lord) to theidColumn
and other static method return type.Now create a funnel function where it returns that unknown object as a result of ObjectionJS query with a precise type or return an error if it is not the expected result.
To scale this pattern let's make a function factory a.k.a higher-order function for it. I'll be using io-ts for the parser to demonstrate the new paradigm. Also, check out io-ts if you haven't, it's a cool lib.
And using the fetch function would be
If you're really sure that your database will always return the correct schema, do this, but I don't recommend this.
The code is long and it's already the optimal amount of code to achieve type correctness in your application.
I guess enforcing strict TypeScript in the ObjectionJS is not the best angle to approach this problem. ObjectionJS is of a different paradigm, and let it be that way because we can't change how it behaves nor we can put a strict TypeScript rule to it.
Let ObjectionJS be unknown. As a substitute, enforce type correctness in the business logic, detached from the resource facing layer.
Ken;
You had asked "How would you restructure that program to use a class?" with respect to the goodness of "hoisting".
If multiple components need to use something in common, I usually abstract the component to an Angular Service today. It gives me that ability to re-use anything anywhere. The only drawback is that I have to import the service in order to reuse the code within it.
@Alan, thanks for the in-depth response! I'll have to take some time to read through it and consider it. Interestingly, I notice that Objection.js actually provides typings, though they seem rather convoluted and maybe more intended for internal library use; what do you think? github.com/Vincit/objection.js/blo...
@john Peters, sure, I think that's a great approach, and personally I don't consider an extra import a drawback at all; if anything, I prefer smaller individual modules, so I'm happy to ad an extra import in exchange for a smaller overall file 😁
@Ken I would take the shortest, easiest, and safest path that scales both in performance and development. TypeScript strict typing is relieving me from constantly fearing "undefined is not a function" issue, but if objection typing turns out to be a burden more than a help to my team, I would consider excluding objection from the strict typing in favor of development efficiency. It's just my opinion though, you know the lib better and might prove me wrong on this.
If you find value in using subclassing in your case, then go for it. I am not going to pretend to be the know-it-all :)
So we pretty much have the same background :)
I think my biggest complaint about JS classes is the encapsulation. Let's see the following code as example:
As you can see
person.talk
is not encapsulated as a part of the classPerson
. Instead, the context is related to where it is called instead. This problem is especially painful when you work with frontend code (e.g. React). You will find the context being mutable, and will be forced tobind
them everywhere. So as Alan also mentioned in the other reply, I tend to go with functions and closures in order to workaround this hustle.Ah I see. Yeah that's definitely a frustration sometimes, and I've dealt with the React issues you mention. That said though, I wouldn't tie that to JavaScript's class system at all; that's just how objects in general work in JavaScript, and it has its pros and cons. While it's true that it can cause some issues, it also facilitates the composition-over-inheritance style you mentioned above, as it lets you copy methods from one object directly to another without any fancy footwork. Pros and cons, I guess
Interesting for sure! I have never seen this before.
Class Encapsulation seems to imply the name property only for the class object. So the behavior you've shown would be expected because mockElement.onClick is not a Person object. Right?
Indeed changing the assignment to const mockElement = this.person gives proper context when calling mockElement.Talk();
It looks to me like the class object's context is not mutable. That's a good thing.
Actually, class object's context is mutable. Let's take a closer look at the example again:
You can actually manipulate the reference of
this
. This is what makes it misleading in an OOP perspective because you would have thought the context is part of the encapsulation in the definition. The problem is not strictly coming from javascript classes, but more from the discrepancy of the expected behaviour of how class should be vs how javascript class actually is.But I really don't think it has anything to do with classes. I think a better statement would be, "The problem is coming from the discrepancy of the expected behavior of how objects should be vs how javascript objects actually are". IMHO, the confusion is identical using a factory:
I can't see this example being any less confusing than the class example just because we don't use the
new
keyword. The confusion all boils down to this step:Regardless of how we built the
person
object, what's confusing is that thetalk
method loses itsthis
context when attached to something else.Now of course, one way to solve this problem is to use purely private vars and closures like you did in your Animal example, but personally, I have one really big problem with that approach: it makes the properties themselves inaccessible. You can no longer do
doggo.name = 'Fido'
to rename your dog. And hey, If all you need is private vars, go for it, but I don't think this approach covers all cases, or even most.You can, of course, use a getter and a setter for each public property to make them accessible while keeping the closure and its advantages, but at that point the complexity of the code really ramps up while the readability falls, and personally, I just don't know if it's worth the trade-off:
That up there feels like a lot of boilerplate to produce an object with three properties, just so I can occasionally write
myBtn.click = myDoggo.speak
instead ofmyBtn.click = () => myDoggo.speak()
.This is definitely a personal preference, but I don't think the relatively minor tradeoff of context-free methods is worth it. I personally don't use them nearly often enough to justify that kind of a change across the board. If you do, hey, maybe it's for you, but I personally am so used to JavaScript objects and how functions and
this
work that it's barely even a frustration, and tbh I just really love the elegance of theclass
syntax. Unpopular opinion, but IMO it will be even better once the class field and private class field syntaxes become standard.I think that is a fair statement. Regardless, that was fun discussion and I think I learnt something from it :)
Definitely 😁 Thanks to everyone in this thread for the back and forth, it was a good discussion and we made it out without any flames