DEV Community

Cover image for Do we really need classes in JavaScript after all?
Dominik Lubański
Dominik Lubański

Posted on • Edited on

Do we really need classes in JavaScript after all?

Among a lot of other great features, ES2015 introduced the class syntax. For some, it was a missing piece in the object-oriented programming; for others something that we should have never added in the first place. Nevertheless, classes have become beloved by the library authors and users, so today, you can find them in almost every JavaScript library or framework.

Did classes deliver, what they promised? Three years later, I can say that besides simpler syntax (instead of using function constructors and prototypes), they failed in various fields. Let's explore together some of the foremost pitfalls.

class MyComponent extends CoolComponent {
  constructor(one, two) {
    // Use super() always before calling `this`
    // and don't forget to pass arguments 🤭
    super(one, two);
    this.foo = 'bar';
  }

  update(...args) {
    this.value = '...';
    // Does CoolComponent include update method or not? 🧐
    super.update(...args);
  }
}
Enter fullscreen mode Exit fullscreen mode

Class syntax might be confusing. Libraries usually force users to use extends keyword for consuming its API. As it might look straightforward, extending requires using super() calls wherever needed. To be sure, that our methods don't overwrite internal ones defined by the parent, we have to be careful how we name them (soon it will be possible to use a fancy # keyword to create private fields).

Super calls also can be tricky - for example, you can't use this in the constructor before calling super(). Oh, and don't forget to pass constructor arguments. You have to do it manually if you define constructor method.

Of course, we can get used to it. So we did. However, it doesn't mean that this is right.

class MyComponent extends CoolComponent {
  constructor() {
    ...
    // Change onClick method name and forget update it here 😆
    this.onClick = this.onClick.bind(this); 
  }

  onClick() {
    this.foo = 'bar';
  }

  render() {
    return <button onClick={this.onClick}>...</button>;
  }
}
Enter fullscreen mode Exit fullscreen mode

Classes are tightly bounded to this syntax. In class methods this represents an instance of the class. It was never intended to pass method definitions to another instance and lose that context. I know that library authors just wanted to squeeze out what's possible from the class syntax and at the same time be creative. Unfortunately, there is no one best solution for binding a function context. For the rescue, we will be able to use yet another new syntax - class fields, which simplifies creating methods pre-bounded to the instance.

class MyComponent extends CoolComponent {
  // this method uses current state 🤨
  foo() {
    this.currentValue += 1;
    return this.currentValue;
  }

  // this method depends on other instance method 👆
  bar(nextValue) {
    const value = this.foo();
    return value + nextValue;
  }
}

class OtherComponent extends MyComponent {
  // Ups, this.bar() is broken now 😡
  foo() {
    return this.otherValue; 
  }
}
Enter fullscreen mode Exit fullscreen mode

Classes are hard to compose. The first problem here is with stateful methods. They can use the current state and return different results, even for the same input (passed arguments). The second factor is a well-known gorilla - banana problem. If you want to re-use class definition, you have to take it all or nothing. Even if you know what kind of methods parent includes, they might change in the future, so it is pretty easy to break something.

Moreover, it is almost impossible to take out a single method from the class definition, and re-use it in another one. Methods usually depend on each other or take values from class instance properties using this syntax. Yes, there is a mixins pattern, but it does not provide a clean and straightforward way for composing classes. If you wonder, there is a mixwith project for that and even ES proposal from the same author.

Is there any way out from those obstacles? Despite all of the classes burdens, they were for sure the best way to go forward in web development. The form of how we used plain objects before did not provide significant advantages over the classes. Because of that, library authors and users without thinking twice switched to them. So, is it possible to avoid all the classes problems and create a UI library, which is still powerful and easy to use at the same time?

For the last two years, I have been working on a library for creating Web Components, which I called hybrids. As the name suggests, it is a mix of two ideas - classes and plain objects. However, the final solution did not come to me just like that.

Initially, I followed common patterns, like other libraries. I built my API on top of the classes. Although, the primary goal of the library was to separate business logic from the custom element definition and let users avoid some of the classes problems (for example extends and super()). After a year, I almost finished my work, and I was ready to release a major version. The only last thing that bothered me a lot was a lack of composition mechanism. At the same time, I began to learn more about functional programming, and I liked it very much. I was sure then that class syntax was a blocker. I tried to study a lot about how to compose classes, but all the solutions were not sufficient in my opinion.

The breakthrough can only occur if you give up the available solutions and create new ones instead. For me, it was a mind-shift in how we can define components. All those problems have become an impulse to start the process again, but this time in a completely different way. Instead of using existing ideas, I started with an empty file where I tried to create a public API example, which solves those problems. Finally, I ended with something similar to this:

import { html, define } from 'hybrids';

function increaseCount(host) {
  host.count += 1;
}

const SimpleCounter = {
  count: 0,
  render: ({ count }) => html`
    <button onclick="${increaseCount}">
      Count: ${count}
    </button>
  `,
};

define('simple-counter', SimpleCounter);
Enter fullscreen mode Exit fullscreen mode

There is neither class nor this syntax, only simple values and pure functions in the definition inside of the plain object. Moreover, objects definitions can be composed with ease, as they are maps of independent properties. Custom define() function creates a class dynamically, applies properties definitions on the prototype and finally defines a custom element using Custom Elements API.

At first, I thought that it is impossible to implement API like this in the way, that it would scale and allow building complex components with more logic than a simple counting button has. Still, day after day I tried to create better ideas and solutions to make this possible.

The hard work paid off. In May 2018 I released a major version of the library. The code, which you can see above is a fully working example from the documentation! All of this was only possible because of a number of ideas used together, like property descriptors, factories, and property translation, as well as cache mechanism with change detection.

However, what about the opening question from the title? Do my ideas are the answer? Time will tell. For now, I would be happy to discuss this topic with you 💡.

GitHub logo hybridsjs / hybrids

Extraordinary JavaScript UI framework with unique declarative and functional architecture

hybrids

build status coverage status npm version

An extraordinary JavaScript framework for creating client-side web applications, UI components libraries, or single web components with unique mixed declarative and functional architecture

Hybrids provides a complete set of features for building modern web applications:

  • Component Model based on plain objects and pure functions
  • Global State Management with external storages, offline caching, relations, and more
  • App-like Routing based on the graph structure of views
  • Layout Engine making UI layouts development much faster
  • Localization with automatic translation of the templates content
  • Hot Module Replacement support without any additional configuration

Documentation

The project documentation is available at the hybrids.js.org site.

Quick Look

Component Model

It's based on plain objects and pure functions1, still using the Web Components API under the hood:

import { html, define } from "hybrids";
function increaseCount(host) {
  host.count += 1;
}

export default define({
  tag: 
Enter fullscreen mode Exit fullscreen mode

Do you want to know more? In my upcoming posts, I will explain in detail all of the core concepts of the hybrids library. For now, I encourage you to look at the project homepage and official documentation.

You can also watch my Taste the Future with Functional Web Components talk, which I gave at the ConFrontJS conference in October 2018, where I explained how I came to those ideas.


🙏 How can you support the project? Give the GitHub repository a ⭐️, comment below ⬇️ and spread the news about hybrids to the world 📢!


👋 Welcome dev.to community! My name is Dominik, and this is my very first blog post ever written - any kind of feedback is welcome ❤️.

Cover photo by Zach Lucero on Unsplash

Top comments (28)

Collapse
 
akashkava profile image
Akash Kava

Coding consists of two major parts, one logic and one is organizing logic. Consider logic as a contents of book and organizing as a book library.Book Library is organized by categories and then it is further organized by either author name or title in dictionary form. Imagine if you had a good contents of book but if it wasn't organized properly. Imagine if everyone organize contents of book and book in library with random order.

I consider class as a way to organize logic, you still have your functional ways to write logic but class is well understood by everyone and it is easy to document and organize. And it is very easy to refactor the code because IDE knows all references of identifier and it can safely refactor it. This was basically the reason why C++, Java and C# became more popular because developers need not focus on how to write it correctly and were able to focus on business needs.

Collapse
 
smalluban profile image
Dominik Lubański

This is an excellent explained opinion with an example :)

The class syntax is a natural way to express objects form a real world. They represent something, which can have unique properties attached. The problem is that we are using classes to do everything, and we mix business logic with the state. We do that because it is so easy. Only what we have to do is add methods...

it is very easy to refactor the code because IDE knows all references of identifier and it can safely refactor it

IDEs can also handle more functional approach. If not, they will. If it would be opposite, I would be very sorry for all functional languages programmers.

Collapse
 
akashkava profile image
Akash Kava

IDEs do handle refactor in functional language, but it will not be as powerful as the one with class language.

You are right about doing everything in class, infact same applies to functional, people want to do everything in functional way, that is also wrong, using best of both worlds will be perfect approach.

Collapse
 
wiredferret profile image
Heidi Waterhouse

That’s a useful analogy, thank you!

Collapse
 
wuz profile image
Conlin Durbin

I quite dislike classes in JS - I put together some of my thoughts on it here: MarxistJS. Basically, my problem with them is that they add a lot of problematic mental models for a new JS developer.

If you are coming from another programming language, you might expect classes to work the same way you are used to - which in many ways they do. But JS doesn't have real classical inheritance since it is a prototypical language. Using a standard function+object+prototype chain "classes" is a good way to learn how JS works, but hiding all that behind the class sugar is too much abstraction to me.

I feel like the addition of classes in JS was done to appease a very vocal minority and didn't take the consideration of what JS actually is.

Collapse
 
smalluban profile image
Dominik Lubański

I can't agree more. One of the more general problems of classes is that they are not what they seem to be if you came from other languages. Prototypical inheritance is much different from the classical approach to classes.

Collapse
 
tomekbuszewski profile image
Tomasz Buszewski

Hello Dominik ;)

I like classes in general, but I feel that the JavaScript implementation isn't mature enough. And with this new proposals (ridiculous #, for example), we aren't getting any better. This was one of the reasons I switched to TypeScript for my larger things. Interfaces, private/protected/public methods, all seems very in-place.

I completely understand your lack of sympathy towards extending, inheriting etc., but this comes with the OO design. If you create a class, you have to bear in mind that it may be extended by some other entity. And what happens next is the responsibility of one doing the extension. If this person overwrite your method, well, perhaps she or he should look at the base class first?

Another fact is, I tend to steer towards functional paradigm whenever I can. But the problem with this approach is, you will need some state control, sooner or later. They, you'll create more and more abstract entities only to store something that a class instance could easily contain. While this isn't always bad, it often introduces higher complexity which, in result, may yield harder maintenance.

To what this boils down to? I would say that using paradigms where they apply.

Collapse
 
smalluban profile image
Dominik Lubański

Thank you for your comment :) I can't resist showing you an interesting example. It is about one of your sentences:

If this person overwrites your method, well, perhaps she or he should look at the base class first?

Are you sure, that people are using inheritance in the right way? Below example is from one of web component implementation (here you have full source code):

class GridElement extends
      Vaadin.ElementMixin(
        Vaadin.ThemableMixin(
          Vaadin.Grid.DataProviderMixin(
            Vaadin.Grid.ArrayDataProviderMixin(
              Vaadin.Grid.DynamicColumnsMixin(
                Vaadin.Grid.ActiveItemMixin(
                  Vaadin.Grid.ScrollMixin(
                    Vaadin.Grid.SelectionMixin(
                      Vaadin.Grid.SortMixin(
                        Vaadin.Grid.RowDetailsMixin(
                          Vaadin.Grid.KeyboardNavigationMixin(
                            Vaadin.Grid.A11yMixin(
                              Vaadin.Grid.FilterMixin(
                                Vaadin.Grid.ColumnReorderingMixin(
                                  Vaadin.Grid.ColumnResizingMixin(
                                    Vaadin.Grid.EventContextMixin(
                                      Vaadin.Grid.StylingMixin(
                                        Vaadin.Grid.ScrollerElement))))))))))))))))) {
Enter fullscreen mode Exit fullscreen mode

You still think that a user of GridElement class knows every single property and method, that all of this dependencies add?

Collapse
 
tomekbuszewski profile image
Tomasz Buszewski

Are you sure you want to use Vaadin as an example for anything? :D

This code, while very extreme, shows the difference between knowing the syntax and knowing how to program. While still valid, creating such structure introduces almost impossible way of telling what is being used, overwritten etc.

Thread Thread
 
smalluban profile image
Dominik Lubański

This is one of many examples, that look like this in the web components area. Paper elements from the Polymer team are not much better in this :)

Thread Thread
 
tomekbuszewski profile image
Tomasz Buszewski

I see, but this proves nothing. Saying that "there are many examples of bad practices" can be revoked with "there are many examples of good practices". You don't stretch a rubber band the the max just because it can be stretched :)

Collapse
 
panta82 profile image
panta82

Main benefit I get out of ES6 classes is defining the shape of data I am dealing with, so I can talk and think about it in precise terms. When I say "Customer", I know exactly what fields and properties that entails (and IDE will help me remember it too).

Member functions and inheritence are less useful for the reasons you stated in your article. Any kind of business code I prefer to keep in pure functions or service objects, that are given class based objects to manipulate. Code and data kept mostly separate.

Mixins or composition patterns like yours don't really do much for me, as they muddy the waters of what each thing is and what can be done with it.

Collapse
 
smalluban profile image
Dominik Lubański

I like your approach. Classes are definitely good at creating data structures. Business logic in JavaScript can be tricky in class syntax (how I tried to point it out in the article).

Mixins or composition patterns like yours don't really do much for me, as they muddy the waters of what each thing is and what can be done with it.

The ideas behind hybrids library might be more clear to you if you look at the project documentation (especially Core Concepts section). I didn't want to mix subjects in this article and make it too long. In next weeks you can expect posts about those concepts on dev.to :)

Collapse
 
smalluban profile image
Dominik Lubański

Of course, I remember definitions on prototypes. But then, I didn't understand very well what's was going on with them. Classes made this simpler and cleaner. Also, they introduced own problems.

The side effect of the SimpleCounter is outside of the definition in purpose. It is private, so users can't trigger it accidentally. It might be strange at first, but it has own sense.

This concept is also great for testing. As you can see, we can export side effects, and create unit tests for them without component initialization. They are just simple functions.

I am not sure what it would look like completely, but this is a great start.

This is exactly what I want, a discussion about better web development. And I will be my success if I encourage someone to create next great ideas.

Collapse
 
ben profile image
Ben Halpern

Would it be fair to say that this conversation is fairly steeped in front-end web development?

How might you spin this conversation when other present or hypothetical use cases are accounted for more centrally?

The issues with scope and confusion would still be present, but might classes be more worth it in different contexts?

Collapse
 
joelnet profile image
JavaScript Joel

Would it be fair to say that this conversation is fairly steeped in front-end web development?

I think the examples used are very front-end. But I think that is because that is the larger audience. But I believe these concepts apply equally to node development as well.

Developers that come from other OO languages, reach for classes first due to familiarity with them. But JavaScript classes do not behave like those other languages, so people get easily tripped up.

JavaScript classes have their quirks due to the complexity of having to retain backward compatibility. JavaScript class Inheritance is mocked, but underneath (and hidden), it's still using prototypal inheritance.

And due to these expectations, it's my opinion that's where a lot of frustration comes in.

I am not a fan of JavaScript classes. To use them properly, you should understand how they are making prototypal inheritance. So you need to understand both anyway.

JavaScript's prototypal inheritance is exposed when you run into code like this:

class Bork {
  constructor() {
    this.message = 'Bork!'
  }
  bork() {
    return this.message
  }
}

const bork = new Bork()
const borkBork = bork.bork

bork.bork() //=> "Bork!"
borkBork() //=> Error("Cannot read property 'message' of undefined")

It is commonly found in React with something like this:

render() {
  // potential bug
  return <button onClick={this.handler} />
}

We have so many workarounds because we have an imperfect solution, the JavaScript class.

IMO, JavaScript classes were a mistake and they lead to much unnecessary confusion. They don't provide anything additional or extra and there are better ways of writing code.

This code is a good example of that as you will not run into any of the pitfalls of this.

Cheers!

Collapse
 
smalluban profile image
Dominik Lubański

Yes, my concepts were created in the context of web development. I was looking for solutions, which can make creating web components simpler and less confusing. Initially, I wasn't thinking about more general problems. I can't say certainly, that those ideas will be the best option in other contexts. However, I am sure, that is worth to try.

Described classes pitfalls are general. You have to deal with them regardless of what you create, next controller in your node.js application or component in your favorite framework. That is why I put an open question about JavaScript, not only in the web development context.

How might you spin this conversation when other present or hypothetical use cases are accounted for more centrally?

The first idea, which I would recommend to test is the property descriptor concept. It allows switching class definition to a plain object. What can we get in return? Possibility to test parts of the definition, share individual properties between definitions, using pure functions and many other.

Collapse
 
kayis profile image
K

While I like classes less and less, I think their addition to JS was a good thing.

Before them, I worked with projects that had >3 different class implementations in them, all with their own ups and downs.

Now everyone tends to use the ES2015 version and things are much clearer.

When everyone uses one class implementation, we can also learn more from each other. If someone gets rid of classes in one library, others could maybe use some of the approaches to get rid of them in their implementations too.

Collapse
 
smalluban profile image
Dominik Lubański

Using raw function constructors and prototypes was for sure complex and verbose. That advantage of the class syntax is not questionable. Unification is also a good thing. But, what do you think about next additions to this syntax, like class fields, private fields - in my opinion, it is a try to make classes more complex and less straightforward. If I create a private method, can I send it outside of the class definition? Another example - using class fields for defining methods can make it not efficient.

If someone gets rid of classes in one library, others could maybe use some of the approaches to get rid of them in their implementations too.

Can you explain what you mean here? Not using classes don't have to mean to create not understandable API, which has to be changed.

Collapse
 
kayis profile image
K

I meant that if one library creator uses classes, and finds another elegant implementation that could be used instead, this approach could be adopted by other library creators more easily if they both used the same ES2015 class implementation.

But yeah, I think the ECMAScript people will just add to the class so Java/C# people will be satisfied...

Collapse
 
abhinav1217 profile image
Abhinav Kulshreshtha • Edited

As a php developer, who use js for front-end only, Classes didn't benefit me much. I already used OO-Js by extending proto, and always had a namespace for my script.

On flipside, recently, I have used typescript to generate said front-end script, And classes makes organization better. Compiled ecma-5 code does contain extra junk than what I would have normally written, but it's a tradeoff I can accept.

Now, as a node developer who work on back-end, Classes have made my life easier, I can work with normal OOPs mindset, Organize modules in a way that make sense. Classes makes js feel like a proper programming language. Again, Typescript makes working on it better. If you remember time before typescript, code organization was messy.

So in summery, classes are surely important addition to language. What is hard is that our mindset still sees js as a scripting language, and with that, classes feels like an unnecessary complication. So a noob mindhack I used was to tell myself that js is a scripting language and TS is a compiled OOP language. That cleaned up my itchy coding.

Collapse
 
chenge profile image
chenge

Erlang's father Armstrong has a post say "Object is wrong".

Golang has OOP but no inheritance.

Elixir uses a functional way.

So in JS we can use class, but we should just use class as interface and no data inheritance like golang.

Collapse
 
patarapolw profile image
Pacharapol Withayasakpunt • Edited

My thought is there can even be object syntactic sugar as well. (Just like in Kotlin.) It makes easier to work with typings in TypeScript.

I don't particularly hate this, BTW.