Exciting ES2015 Features

Internet Explorer 8 was released 6 years ago. It's the last browser with a nontrivial market share that is stuck on ES3.

Surprisingly, the biggest complaint that I hear about JavaScript on IE8 is having to deal with trailing commas in object literals. This is a solved problem. ES5 brings a whole lot more to the table than giving the syntax some slack. Getters/setters/properties were introduced in ES5, but I'm hard pressed to find any libraries which use them to their advantage. Object.freeze(), Object.seal(), and Object.preventExtensions() are extremely useful defensive mechanisms which help programs enforce their invariants. Using these new(ish) ES5 features could have huge impacts on how we read and write JavaScript. So where are they?

Wait, I thought this was about ES2015 (ES6).

I'm afraid that the features that are getting the most attention in ES2015 are similar to that trailing comma. This new version gives us much more than light syntactic sugar (as opposed to heavy syntactic sugar, like Haskell's do notation). ES2015 gives us tools that can (and hopefully will) change how JavaScript is written and read. Let's dive in.

Map & Set

Here's a bold claim: the majority of JS written today uses pointers. Yes, pointers: like in C. I know this sounds a bit crazy, but bear with me. This is because an Object cannot have keys which are themselves Objects.

Our libraries and frameworks have been so engrained with this limitation that it might not even seem useful! But if we look at the APIs which we call today, we call functions which return "unique" primitives to refer to a handle and we write functions which take these primitives and return the objects they refer to.

In other words, we allocate things that live elsewhere and are given a primitive string or number which we later dereference in order to get at its underlying meaning. These are the fundamental pointer operations. And just like in C, bad things happen when these "pointers" are passed to functions which expect pointers of a different type, when they are pre-calculated, or when they refer to objects which have since been deleted.

This behavior is so pervasive that we don't bat an eye at documentation assuming identifiers always start with the value 1 or demonstrate that they can be referenced by literals.

Why is this? Ultimately, the only tool we have to efficiently create an association between an object and external metadata is the built-in Object, which can only take strings as its keys.

ES2015's Map allows us to create an association between an arbitrary Object and arbitrary data. It lets us do this without resorting to dirty tricks which either won't work if we use Object.freeze() or require searching through arrays of key-value pairs to find an identity match.

So let's look at an example. Imagine a different implementation of setTimeout and clearTimeout, which uses Map internally to verify that only setTimeout handles are passed to clearTimeout:

var timers = (function () {
  var sentinel = {};
  var handleMap = new Map();
  return {
    setTimeout: function (func, ms) {
      var handle = window.setTimeout(func, ms);
      var safeHandle = Object.create(sentinel);
      // safeHandle's prototype is sentinel
      handleMap.set(safeHandle, handle);
      return handle;
    },
    clearTimeout: function (safeHandle) {
      if (Object.getPrototypeOf(safeHandle) !== sentinel) {
        // safeHandle is not from setTimeout
        throw new TypeError('clearTimeout handle not created by setTimeout');
      }
      var handle = handleMap.get(safeHandle);
      if (handle === undefined) {
        // safeHandle no longer valid
        throw new Error('clearTimeout called with unknown handle');
      }
      handleMap.delete(safeHandle);
      return window.clearTimeout(handle);
    }
  };
})();

This example clearTimeout can only be called with values returned from setTimeout exactly once. By adding an extra layer of protection, we can ensure that our API is used as designed and future results cannot be "predicted". If the same approach is taken for requestAnimationFrame and clearAnimationFrame, an attempt to call clearAnimationFrame(setTimeout(func, ms)) would result in a TypeError exception. This makes our contracts and invariants explicit. If we bake the limits of our software's design into our APIs, there are fewer opportunities to surface bugs. On the other hand, if we execute clearAnimationFrame(setTimeout(func, ms)), we'd be sad to discover that the callback func is called after ms milliseconds and there are no exceptions raised or errors logged.

With Map and Set, we can write APIs which prevent the construction of references and associate external data with an object without modifying it. We can move away from the primitive obsession forced upon us by ES5 and before.

WeakMap

This brings us to our next related feature: the ability to store associations in a memory-efficient way.

Let's say we want to associate data with an arbitrary object. We could use a Map where the keys are the object we want to add associated data to, and the values would be the associated data. If we use this, we can add associated data without modifying the original object, but we'll need to be aware of when the object is no longer referenced in order to .remove() it from the map so that our program doesn't leak memory.

It's a bad practice to mutate objects that we don't own, But in ES5, when we want to associate data with an object that we don't own and don't want to mutate (and doesn't have a disposal notification mechanism), we're out of luck: we'll either have to mutate the object or will be forced to leak memory.

What can we do here? One of JavaScript's design trade-offs is that it doesn't tell us when objects are freed. It would be amazing if we could be notified that an object is completely unused so we could remove it from our Map. But we just can't know when data is dereferenced unless we build a reference counter, track all of our objects' references, and build a subscription notification mechanism when our objects are fully dereferenced. This is a pain, it'd be building our own garbage collector.

Thankfully, there's another way! WeakMap is a new interface which lets us associate without having to leak memory, mutate objects, or use our own home-grown garbage collector.

This can be a bit abstract, so let's go through a simple example:

A Twitter clone has Tweet and User models. We need to create an association of Tweet to an array of Users which represents the users who have favorited the Tweet. How can we do this without adding a new field to the Tweet model?

In ES5, we'd probably end up creating a new TweetFavorites interface which held a mapping between Tweet and a set of Users. It'll probably look like this:

function TweetFavorites() {
  this.tweetUsers = {};
}
TweetFavorites.prototype.getFavoriteUsers = function (tweet) {
  if (!this.tweetUsers[tweet.id]) {
    this.tweetUsers[tweet.id] = [];
  }
  return this.tweetUsers[tweet.id];
}

If we use ES2015's Map, we can remove the string .id:

function TweetFavorites() {
  this.tweetUsers = new Map();
}
TweetFavorites.prototype.getFavoriteUsers = function (tweet) {
  if (!this.tweetUsers.has(tweet)) {
    this.tweetUsers.set(tweet, []);
  }
  return this.tweetUsers.get(tweet);
}

This code has the same problem we just described. Each item we add to the array returned from .getFavoriteUsers() is now (and forever) strongly referenced by this TweetFavorites interface. If the rest of the application decides to remove all references to a specific Tweet, our TweetFavorites implementation would still hold on to the Tweet as a key in its internal Map and all of the items in its associated array. Without any references remaining, we leak memory for the duration of the application.

To solve this, we would need to manually delete the Tweet's favorites array within the TweetFavorites implementation when a Tweet is "disposed" and ready to be removed from the rest of the application. This might look something like this:

function TweetFavorites() {
  this.tweetUsers = new Map();
}
TweetFavorites.prototype.getFavoriteUsers = function (tweet) {
  if (!this.tweetUsers.has(tweet)) {
    this.tweetUsers.set(tweet) = [];
    tweet.onDispose(function () {
      this.tweetUsers.delete(tweet);
    }.bind(this));
  }
  return this.tweetUsers.get(tweet);
}

But if the Tweet interface did not have an .onDispose() subscription, we'd be out of luck.

But we can use a WeakMap! A WeakMap is just like a Map except it weakly holds on to its keys. When the garbage collector comes around looking for unreferenced objects, it treats the keys in our WeakMap as if they are not there. If one of the WeakMap's Tweet keys has all of its references removed, it's as if .delete() was called, and is allowed to be garbage collected.

Here's what our TweetFavorites implementation would look like with WeakMap:

function TweetFavorites() {
  this.tweetUsers = new WeakMap();
}
TweetFavorites.prototype.getFavoriteUsers = function (tweet) {
  if (!this.tweetUsers.has((tweet)) {
    this.tweetUsers.set(tweet, []);
  }
  return this.tweetUsers.get(tweet);
}

It's the same as the naive Map, but it is no longer susceptible to leaking memory!

One other notable use case for WeakMap is to have true private values for our objects in a memory-efficient way. Here's a demonstration of what this would look like:

var MyClass = (function () {
  var PrivateData = new WeakSet();
  function MyClass(val) {
    var privates = {};
    PrivateData.set(this, privates);
    privates.value = val;
    privates.secret = "you can't read this!";
  }
  MyClass.prototype.getValue = function () {
    var privates = PrivateData.get(this);
    return privates.value;
  }
  return MyClass;
)();

No matter how hard we try (aside from using a debugger, overriding the WeakMap.prototype methods, or calling toString on the function), an outside observer will not be able to access our private data! And unlike Crockford's suggestion of how to create true privates with closures, the function implementations all live on the prototype, so this approach is much more memory-efficient.

WeakSet

Similar to WeakMap is WeakSet, which is great for "tagging" objects that may eventually go away (like keeping track of Tweets that we have viewed in our session so we can display them differently). It works like this:

function Tweet(name) {/*...*/}
var viewedTweets = new WeakSet();
var tweet = new Tweet('@alice', 'Hello, world!');
viewedTweets.has(tweet); // false, we haven't added her yet
viewedTweets.add(tweet);
viewedTweets.has(tweet); // true, we just added her
viewedTweets.delete(tweet);
viewedTweets.has(tweet); // false, we just removed her
viewedTweets.add(tweet);
tweet = null; // @alice's Tweet is now unreachable
// @alice's tweet is no longer held within viewedTweets
// ...but we can't programmatically verify this.

We can add an item to the set and check if items are in the set. If an item is held weakly in the WeakSet, but is dereferenced everywhere else and unreachable (which means we can't check if it is present in the set), the garbage collector will also remove it from the set for us. A WeakSet is identical to a WeakMap where the values are all true (or null or false or any other single value).

WeakMap and WeakSet make it easier for us to build small and low-coupled components without having to worry about object lifecycle clean up.

Feature Request: WeakRef

Unfortunately, there's one missing piece here. Keeping in line with the design trade-off of JavaScript preventing us from observing object lifecycle events, ES2015 is missing a WeakRef: a weakly held reference to an object that we can obtain if it hasn't been garbage collected.

With weak references, we could label our objects properties with explicit ownership semantics (like how we tag members as strong or weak in Objective-C @property declarations) and rely on the garbage collector (or something like ARC) to keep our memory footprint efficient.

It's important to note that this isn't a radical request. Weak references are present in practically every language which has a garbage collector or some form of automated memory management: C#, C++, Haskell, Java, Lua, Objective-C, OCaml, PHP, Perl, Python, R, Ruby.

Generators

I've previously written about generators and why they are a game changer here. Go ahead and take a read, it's worth it.

Proxy

Despite having incredibly good introspection facilities, we are still limited in what we could simulate with user-defined objects. Proxy lets us create objects with powers that only browser vendors had access to.

One of the surprisingly complex things that jQuery does is make each jQuery object act like an array-like value. It makes $('.foo')[0] act like $('.foo').get(0). The internal jQuery.merge function is responsible for maintaining this array-like behavior.

But with Proxy, we can create array-like objects without having to define every single index. We can react to the internal [[get]] method by creating a Proxy on an object which declares the get action.

function InfiniteDoubleArray(length) {
  return new Proxy({
    length: Infinity
  }, {
    get: function (target, name) {
      if (name in target) {
        return target[name];
      }
      var maybeIndex = parseInt(name, 10);
      if (!isNaN(maybeIndex)) {
        return 2 * maybeIndex;
      }
      return undefined;
    }
  });
}

var doubleArray = new InfiniteDoubleArray();
console.log(doubleArray[3]); // --> 6
console.log(doubleArray[256]); // --> 512
console.log(doubleArray[1234321]); // --> 2468642

Though this example is contrived, Proxy allows us to dynamically react to properties or methods which may or may not truly exist on the underlying object. This is just like the what we have in other languages: Python's __getattr__() method, PHP's __get() magic method, Ruby's method_missing function, Objective-C's message forwarding mechanism, etc, etc.

We're not limited to just retrieving values or calling functions, but we can dynamically react to any of the fundamental operations that can be performed on an object. We can now change the following fundamental operations:

  • getPrototypeOf: respond when accessing an object's prototype (explicitly and implicitly)
  • setPrototypeOf: respond when attempting to set an object's prototype
  • isExtensible: respond when new properties are checked if they can be added to an object
  • preventExtensions: respond when Object.preventExtensions(target) is called on the target object
  • getOwnPropertyDescriptor: respond to generic property access
  • defineProperty: respond when Object.defineProperty(target, prop) is called on the target object
  • has: change the behavior of the in operator
  • get: change the behavior of property reads
  • set: change the behavior of property writes
  • delete: change the behavior of the delete keyword
  • enumerate: provide an iterator for an object
  • ownKeys: respond when Object.keys(target) is called on the target object
  • apply: provide behavior when an object is called as a function
  • construct: provide behavior when an object is called with the new keyword

In short, Proxy allows us to write APIs as if we had the same powers as browser implementors. If we wanted to, we could use this to perfectly simulate the document.cookie API, or the behavior of complex, native HTML elements, like how the value of Node.prototype.innerText performs whitespace normalization in certain cases as well as depends on the calculated layout of the element.

Reflect

As a counterpoint to Proxy, Reflect gives us the ability to trigger these same fundamental operations as function calls. This is mostly helpful for writing introspective code in a functional style. Here's how it works:

// construct
Reflect.construct(MyClass, [arg1, arg2]);
// functionally equivalent to:
new MyClass(arg1, arg2);

// deleteProperty
Reflect.deleteProperty(obj, 'key');
// functionally equivalent to:
delete obj.key;

// get
Reflect.get(obj, 'key');
// functionally equivalent to:
obj.key;

// set
Reflect.set(obj, 'key', value);
// functionally equivalent to:
obj.key = value;

// has
Reflect.has(obj, 'key');
// functionally equivalent to:
'key' in obj;

Take a look at the full set of Reflect operations in MDN's documentation.

const and Real Immutability

const declarations give us immutable references, but the objects it refers to is still able to change contents. Object.freeze() and a handy deepFreeze wrapper gives us immutable objects. Together, we can have real immutability.

// To make obj fully immutable, freeze each object in obj.
// To do so, we use this function.
function deepFreeze(obj) {

  // Retrieve the property names defined on obj
  var propNames = Object.getOwnPropertyNames(obj);

  // Freeze properties before freezing self
  propNames.forEach(function(name) {
    var prop = obj[name];

    // Freeze prop if it is an object
    if (typeof prop == 'object' && prop !== null && !Object.isFrozen(prop))
      deepFreeze(prop);
  });

  // Freeze self
  return Object.freeze(obj);
}

const myObject = deepFreeze(anything);
// myObject will now always refer to a truly immutable object
// we can always assume it and its contents will never change

This deepFreeze example by Mozilla Contributors is licensed under CC-BY-SA 2.5.

Better Unicode Support

Ever wondered why '💩'.length === 2? It's because ECMAScript decided to use UTF-16 code units as its internal character encoding. In other words, JavaScript has a Unicode problem.

Mathias says it best in his article about unicode-aware regex.

The bad news is that there still isn't a property on String which returns the number of Unicode code points.

The good news is that we will have the ability to read and create Unicode code points using String.prototype.codePointAt() and String.fromCodePoint()! Unfortunately, there's a really unfortunate wart in the design of String.prototype.codePointAt(), which means we need to be careful:

// Get a simple character from a string:
'x💩x'.codePointAt(0) === 0x78;
// 0x78 is the Unicode code point for the letter 'x'

// Get a complex character from a string:
'x💩x'.codePointAt(1) === 0x1F4A9;
// 0x1F4A9 is the Unicode code point for '💩'

// WARNING: the following does not make much sense
'x💩x'.codePointAt(2) === 0xDCA9;
// 0xDCA9 is the *second surrogate pair* of the
// UTF-16 code unit pair for '💩'

// Get a simple character from a string:
'x💩x'.codePointAt(3) === 0x78
// 0x78 is the Unicode code point for the letter 'x'

Though things have gotten slightly better, we still must be very careful when dealing with individual "characters" in strings, especially when performing validation on user input.

Conclusion

ES2015 has the opportunity to change how we write JavaScript. Unfortunately, it is impossible for most of these benefits to be achieved with the use of ES2015 to ES5 compilers like Babel or Closure Compiler.

I can't wait until the day we live in a world where we can ship raw ES2015 source code to be executed natively on the browsers we support.


Lastly, a big thanks to Daniel Espeset and Dan Na for reviewing a draft of this and providing valuable feedback.