Skip to content

Instantly share code, notes, and snippets.

@ewinslow
Last active January 24, 2018 04:03
Show Gist options
  • Save ewinslow/b1667290f1134fdf0ee0 to your computer and use it in GitHub Desktop.
Save ewinslow/b1667290f1134fdf0ee0 to your computer and use it in GitHub Desktop.
How I wish ES6 classes were implemented

Introduction

ES6 classes are happening, and there's no way the syntax is going to change radically just for me this late in the game, but I still figure what the heck. Maybe I can write a language someday that has this feature. Or maybe I'm wrong and the feature can change with enough motivation...

Background

The main premise of the currently accepted syntax is to be simple sugar over the prototype style of class definitions:

function Foo() {
  this.bar_ = 'baz';
}

Foo.prototype.doSomething = function() {
  return this.bar_ + 'bop';
};

This is a very common way to write classes in JS and is more memory-efficient than its cousin, the 'function-closure' approach:

function Foo() {
  var bar_ = 'baz';

  this.doSomething = () => bar_ + 'bop';
}

The main benefit of the function-closure approach is truly private instance variables. Notice that it's also much more terse, even without any special syntax for classes (especially since we can use ES6 arrow functions inside the closure and they work as expected).

The prototype-based syntax attempts to mimic privacy by introducing Symbol, but even this is not enough because as far as I can tell, the latest spec still makes symbols enumerable, so private state can be accessed and modified by reflection, which kind of defeats the point of making something private.

I think there is a strong possibility that we could get the best of both worlds here from a class syntax that resembles the function-closures approach:

  • Truly private variables
  • An optimizable syntax that could have similar memory-consumption traits of the prototype syntax -- haven't looked into this, but those folks are clever so I'm betting they could come up with something...
  • Much terser: fewer keywords, less ceremony, less boilerplate

Example:

// There is no separate definition for the constructor function.
// Instead the constructor arguments are listed after the class identifier
class Dog(legs, voiceBox) {
  // Assignments are allowed and this is sugar for declaring *private* variables
  speed = 'stopped';

  // Instance methods are defined identically to the existing proposal
  // ...but they have access to all private variables
  // They are public by default, contrary to property assignments shown above
  bark() {
    return voiceBox.makeSound('bark')
  }

  // "public" properties are exposed via getters
  // They can be made writable by declaring a corresponding setter
  get isPanting() {
    return speed == 'fast';
  }

  // Methods have access to all other methods without needing to use this.*
  run() {
    move('fast')
  }

  // Arguments to methods are defined as normal.
  move(newSpeed) {
    speed = newSpeed;

    legs.move(newSpeed);
  }

}

Detailed explanation

Most class syntaxes I've ever seen are implemented by specifying one specially-named method to be the constructor, e.g. __construct or constructor or {NameOfClass}. This is bad because:

  • It is syntactically awkward. Constructors are not methods. They shouldn't be defined like one.
  • They require you to manually store references to private variables. Constructors for well-designed classes just take their arguments and assign them to private variables. This mind-numbing boilerplate gets maddening quickly.
  • It's one more thing to name and type out.

For example, ES6 classes will look something like this:

class Animal {
  // Type each parameter no less than 3x... :(
  constructor(legs) {
    this.legs_ = legs;
  }
  
  move(speed) {
    return this.legs_.move(speed);
  }

  walk() {
    return this.move('slow');
  }
}

Dart has tried to alleviate this constructor situation by allowing this.* to appear in the constructor which automatically does assignments. It would end up looking something like this in JS:

class Animal {
  constructor(this.legs_)
  
  // ...
}

This is better but you can see that we still have to store references to the injected services on the instance.

Underscore is meant to denote private, but this is a lie because in JS assigning this.* always makes the variable a publicly accessible member of the class. You need a tool to check for inappropriate accesses for you, which isn't great because it isn't enforced in uncompiled code.

In my experience this situation commonly leads to bad code like:

  • Tests that access "private" state (because it's actually public).
  • Collaborating classes that reach into private state of a collaborator class.
  • A private member variable of a subclass can accidentally override that of the super class, causing bugs.
  • etc...

Instead, if we used the existing function scoping of ES5, we get true privacy and the class definition would gets more compelling IMHO:

class Animal(legs) {
  move(speed) {
    return legs.move(speed);
  }

  walk() {
    return this.move('slow');
  }
}

We could even shorten these single-line-return methods to look like arrow functions. Normally this wouldn't be a big deal, but size matters since JS is commonly shipped across the network. Shorter is better.

class Animal(legs) {
  move(speed) => legs.move(speed)

  walk() => this.move('slow')
}

And wouldn't it be nice if we could leave off that extra this.? move method should be available locally.

class Animal(legs) {
  move(speed) => legs.move(speed)

  walk() => move('slow')
}

This syntax could de-sugar to:

function Animal(legs) {

  let move = this.move = (speed) => legs.move(speed);

  let walk = this.walk = () => move('slow');
}

Or perhaps:

function Animal(legs) {
  this.move = function move(speed) {
    return legs.move(speed);
  };

  this.walk = function walk() {
    return move('slow');
  };
}

Or maybe even...

function Animal(legs) {
  let move = (speed) => legs.move(speed);
  let walk = () => move('slow');

  return {
    get isRunning() { return legs.speed == 'fast'; },
    move,
    walk,
  };
}

Which in itself isn't too bad, except that you have to repeat the names of public reference in order to avoid using "this" in any method bodies...

Private variables could be designated like so:

class Animal(legs) {
  moves = [];

  // ...
}

Which would de-sugar to:

function Animal(legs) {
  let moves = [];

  // ...
}

So variable assignments are always locally-scoped and private. It's not possible to reassign global variables in a class constructor, or to expose public variables... This is a little bit of me exposing my personal coding philosophy, but I find public member variables to be an anti-pattern. We have getters/setters in JS so they're not really necessary anyways. Getters/setters can give you "read-only" access (by simply not defining a setter):

class Animal(legs) {
  moves_ = [];

  get moves() => moves_

  // ...
}

The alternative to this would be using more keywords. public and const probably.

class Animal(legs) {
  public const moves = [];

  // ...
}

This is OK, but it's limiting because you can't do any transformations on the private state before it is returned (e.g. cloning or returning an iterator instead of the array itself). It's not extensible. So once you want to do anything fancy, you'd have to move to getters/setters anyways. It's even worse for setters because it's even more likely you might want custom code to run in a setter. So it seems like public const foo wouldn't actually be shorthand and is more limiting than getters/setters.

What about private methods? Don't need a special syntax or keyword, just assign a function to a private member variable:

class(bar) {
  foo = () => bar.baz()
}

I'm torn whether to even define class inheritance. But I suppose its used often enough that people want it...

class Animal(legs) extends LivingThing {}

extends is another keyword and you know how I feel about keywords by now... they bloat the source. JS doesn't need bloat. Instead, we could use :, like the proposed type declaration syntax for AtScript, which seems like it makes sense since inheritance is a kind of type information as well.

class Animal(legs): LivingThing {}

Here is an example if we were to equivalent for function return type annotations and parameter type hints:

class Dog(legs: Legs, voiceBox: VoiceBox): Animal {
  super(legs) // super call must be first

  isPanting:boolean = false

  bark(): boolean => voiceBox.makeSound('bark')

  move(speed): boolean {
    isPanting = speed == 'fast';

    // can only call current super method
    return super(speed);
  }

  run(): boolean => move('fast')
}

I think my proposal is better than the status quo because:

  • It's about as terse as you can possibly get. No extraneous keywords for (this, function, or even return in simple cases).
  • It still works in a way that JS devs should be familiar (function scoping)
  • Private variables are actually private
  • It's not possible to re-bind the this context of instance methods so code like promise.then(animal.walk) works as intended.

Best of all, this type of syntax could be compatible with the current ES6 class syntax. The parens after the class identifier indicate you're using this alternate format so they can live side-by-side harmoniously. You can use one or the other syntax depending on whether you need the prototype format or the function-scoped format.

This may be confusing to newbies because now there's two subtly different ways to define a class. I could also see JS engines just optimizing the terse format so it doesn't have as much memory consumption in the first place. Then the difference is less important and you just use whatever syntax you prefer.

@rowlandekemezie
Copy link

How do you make private and public functions with Js classes? How do you expose them also?

@thedanheller
Copy link

silly question: why do you add the _ on variables?

@thedanheller
Copy link

thedanheller commented Jul 7, 2016

regarding your proposal, it looks much better, for me.

@ponmudivn
Copy link

i wish ES6 is implemented this way :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment