ES2015 Classes & Symbols

Alex Bularca

Web Tools Team Lead @ Everymatrix

ES2015 Classes

  • Just syntactic sugar over existing OO patterns
  • Formalise existing best practices as a language feature
  • Remove a bunch of boilerplate

They allow you to turn this:

function inherits(child, base) {
    var klass = function() {};
    klass.prototype = base.prototype;

    child.prototype = new klass();
}

function Shape(id, x, y) {
    this.id = id;
    this.x = x;
    thix.y = y;
}
Shape.prototype.move = function (x, y) {
    this.x = x;
    this.y = y;
}

function Circle(id, x, y, radius) {
    Shape.call(this, id, x, y);
    this.radius = radius;
}
inherits(Circle, Shape);

Into this:

class Shape {
    constructor(id, x, y) {
        this.id = id;
        this.x = x;
        this.y = y;
    }

    move(x, y) {
        this.x = x;
        this.y = y;
    }
}

class Circle extends Shape {
    constructor(id, x, y, radius) {
        super(id, x, y);
        this.radius = radius;
    }
}

Support better syntax for getters and setters

function Rectangle(id, x, y, width, height) {
    Shape.call(this, id, x, y);
    this.width = width;
    this.height = height;
}
inherits(Rectangle, Shape);

Object.defineProperty(Rectangle.prototype, 'surface', {
    get: function () {
        return this.x * this.y;
    }
});
class Rectangle extends Shape {
    constructor(id, x, y, width, height) {
        super(id, x, y);
        this.width = width;
        this.height = height;
    }

    get surface() {
        return this.x * this.y;
    }
}

Even more

import React from 'react';
import mixin from 'react-mixin';
import {Status} from 'react-router';

@mixin.decorate(Status)
class MyComponent extends React.Component {
    constructor() {
        // this.getParams() comes from the Status mixin defined by react-router
        let params = this.getParams();
    }
}

Symbols - reflection with implementation

  • Globally unique
  • Immutable
  • Never conflict with string object keys

Getting our feet wet

let foo = Symbol(); // => Symbol()
let bar = Symbol('bar'); // => Symbol(bar)
Symbol('foo') == Symbol('foo'); // => false
new Symbol(); // TypeError: Symbol is not a constructor  

// symbols can be used as object keys
var myObj = {
    foo: 'foo',
    [foo]: 'bar',
    [bar]: 'baz'
};

Object.keys(myObj); // ['foo']
Object.getOwnPropertyNames(myObj); // ['foo']
Object.getOwnPropertySymbols(myObj); // [Symbol(), Symbol(bar)]

Real world usage

log.levels = {  
    DEBUG: Symbol('debug'),
    INFO: Symbol('info'),
    WARN: Symbol('warn'),
};
log(log.levels.DEBUG, 'debug message');  
log(log.levels.INFO, 'info message');

Avoiding name clashes

var size = Symbol('size');  
class Collection {  
    constructor() {
        this[size] = 0;
    }

    add(item) {
        this[this[size]] = item;
        this[size]++;
    }

    static sizeOf(instance) {
        return instance[size];
    }

}

var x = new Collection();  
assert(Collection.sizeOf(x) === 0);  
x.add('foo');  
assert(Collection.sizeOf(x) === 1);  
assert.deepEqual(Object.keys(x), ['0']);  
assert.deepEqual(Object.getOwnPropertyNames(x), ['0']);  
assert.deepEqual(Object.getOwnPropertySymbols(x), [size]); 

Providing a protocol

class Iterable {
  constructor() {
    this._data = [];
  }
  
  add(item) {
    this._data.push(item);
  }
  
  *[Symbol.iterator]() {
    let i = 0;
    
    while (this._data[i] !== undefined) {
      yield this._data[i++];
    }
  }
}

var collection = new Iterable();
collection.add('foo');
collection.add('bar');

for (let item of collection) {
  console.log(item);
}
Array.prototype.map = function (callback) {  
    var returnValue = new Array(this.length);
    this.forEach(function (item, index, array) {
        returnValue[index] = callback(item, index, array);
    });
    return returnValue;
}

class StringCollection extends Array {
    add(item) {
        if (typeof item == 'string') {
            this.push(item);
        } else {
            throw new TypeError(
                'Only string values can be inserted in a StringCollection');
        }
    }
}

let col = new StringCollection();
col.add('foo');
col.add('bar');
let result = col.map(function (item) {
    return item;
}
console.log(result instanceof StringCollection); // false

Symbol - species

Array.prototype.map = function (callback) {  
    var Species = this.constructor[Symbol.species];
    var returnValue = new Species(this.length);
    this.forEach(function (item, index, array) {
        returnValue[index] = callback(item, index, array);
    });
    return returnValue;
}

class StringCollection extends Array {
    add(item) {
        if (typeof item == 'string') {
            this.push(item);
        } else {
            throw new TypeError(
                'Only string values can be inserted in a StringCollection');
        }
    }
}

let col = new StringCollection();
col.add('foo');
col.add('bar');
let result = col.map(function (item) {
    return item;
}
console.log(result instanceof StringCollection); // true

Symbol - species

Many more built in symbols

  • Symbol.instanceof
  • Symbol.iterator
  • Symbol.isConcatSpreadable
  • Symbol.unscopables
  • Symbol.match
  • Symbol.replace
  • Symbol.search
  • Symbol.split
  • Symbol.species
  • Symbol.toPrimitive
  • Symbol.toStringTag

References

Thank you!

Questions
Anyone?
 

ES2015 Classes and Symbols

By Alex Bularcă

ES2015 Classes and Symbols

  • 684