My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Thursday, April 16, 2015

Bringing Symbols to ES5

Update Apparently I got enumerability wrong in version 0.1 but now things are fixed all over in 0.2.2
Symbols are by default defined as enumerable, configurable, and writable, but these will NOT pollute for/in loops, neither will show up in Object.keys
Since native implementations already behave like this, and as behavior is very important to not break Object.assign logic, where only symbols explicitly defined as non enumerable will be ignored, I've decided to patch all the things so that native and polyfilled behaves the same.
TL;DR After few tests, considerations, and some brainstorming, I've decided to push get-own-property-symbols to npm and make the code available for browsers too.
I know there was already a module, but in order to fix weird problems I know and probably only I can test, and in order to add Object.getOwnPropertySymbols too, a method that MUST exist the moment we have Symbol primitive in, I thought that having an alternative instead of replacing current ES6-Symbol module would have probably been a better option.

Before Symbols

Since ES5, JavaScript objects have the ability to somehow hide properties through non-enumerability.
var o = {};
Object.defineProperty(o, 'hidden', {value: 123});

for (var key in o) {
  // nothing will ever happen ...
}

Object.keys(o); // []
// empty Array
Another example of non enumerable property is the length of any Array.
In order to find these "hidden" properties, we need to use Object.getOwnPropertyNames.
var o = {};
Object.defineProperty(o, 'hidden', {value: 123});

Object.keys(o); // []
Object.getOwnPropertyNames(o); // ['hidden']
The convenience, when it comes to define properties in the ES5 way, is that we can always directly access a property, and simply writing it as it would be for o.hidden or o['any other property'].

Introducing Symbol([description]) Basics

Directly from the MDN page
A symbol is a unique and immutable data type and may be used as an identifier for object properties.
What else is unique and immutable in JavaScript? Any primitive string as example is, and symbols are indeed very similar to regular strings used as objects properties accessors.
var o = {};
var k = 'key';
var s = Symbol();

o[k] = 123;
o[s] = 456;

typeof k; // string
typeof s; // symbol <== !!!
If we'd like to compare normal properties with symbol properties, here a quick summary:
  • o[string] = value creates a configurable, writable, and enumerable property, and same is for o[symbol] = value, except symbol will not show up in for/in and Object.keys, but it will still be considered enumerable through o.propertyIsEnumerable(symbol) check
  • Object.keys(o) will return all enumerable properties, excluding then symbols, together with other non enumerable properties
  • Object.getOwnPropertyNames(o) will return all enumerable and non enumerable properties, still excluding symbols
  • both strings and symbols can be used to define properties and to retrieve properties descriptors
  • String() === String() but Symbol() !== Symbol(), and even using a descriptor, a symbol is always different from another one, unless Symbol.for('symbol name') is used, which is always the same symbol, providing the same label/name.

So ... What Are Symbols About ?

Here a quick list of benefits regarding the usage of symbols instead of regular strings:
  • symbols are great when it comes to create conflict free properties accessors, create your own Symbol in your closure and use it to set or read specific properties related to that closure
  • great also for globally shared, conflicts free, libraries and utilities behaviors, if some library exposes its Symbol.for('underscore'), as example, every method of such library, and every plugin defined elsewhere, could eventually read the associated data
  • symbols have zero interferences with most common libraries, since developers can easily ignore them and these won't be on their way via common ES3 or ES5 patterns
  • accordingly, symbols are usable to easily define even more hidden properties than what enumerable: false has done until now
Here a simple example on how we could link any kind of data to an object, and without needing a WeakMap:
function link(object, key, data) {
  var s = Symbol.for('@@link:' + key);
  return arguments.length === 2 ?
    object[s] : (object[s] = data);
}

// generic object
var view = {};

// generic node link
link(object, 'node', document.body);

// retrieve the node any time
var body = link(object, 'node');
Another useful example could be a simplified EventEmitter constructor:
var EventEmitter = (function (s) {'use strict';

  function EventEmitter() {
    this[s] = Object.create(null);
  }

  EventEmitter.prototype = {
    on: function (type, handler) {
      return (
        this[s][type] || (this[s][type]=[])
      ).push(handler) && this;
    },
    off: function (type, handler) {
      return this[s][type].splice(
        this[s][type].indexOf(handler), 1
      ) && this;
    },
    emit: function (type, err, ok) {
      return this[s][type].forEach(function (h) {
        h.call(this, err, ok);
      }) || this;
    }
  };

  return EventEmitter;

}(Symbol('event-emitter')));


// basic test
var log = console.log.bind(console);
var e = (new EventEmitter)
  .on('log', log)
  .emit('log', Math.random())
  .off('log', log);
Above example is probably the tiniest emitter implementation I could imagine ... and all thanks to Symbol, how sweet is that?
You can also implement a WeakMap polyfill in few lines of code.

Polyfill Caveats

Well, the first caveat is that Object.create(null) dictionaries, as well as those created via {__proto__:null}, will not work as expected. Symbols would be set as generic keys in there so, in case you need symbols, be aware of this limit and bear in mind all other shimms and polyfills have same limit.

The other biggest inconsistency with native Symbol is that typeof will return string and not symbol.
If we need to perform such check, here a little utility that can help:
var isSymbol = (function (cavy) {
  return function isSymbol(s) {
    switch (typeof s) {
      case 'symbol': return true;
      case 'string':
        cavy[s] = 1;
        var isit = Object.getOwnPropertyNames(cavy).indexOf(s) < 0;
        delete cavy[s];
        return isit;
      default: return false;
    }
  };
}({}));

isSymbol('');       // false
isSymbol(Symbol()); // true
These two are the most inconsistent points I have but the good news is: yayyyyyy, we've got Symbols in basically every bloody browser and JS engine!

No comments: