Introduction to ECMAScript 6

David Kane

Contents

  • What is ECMAScript?
  • Transpiling

New features in ES6

  • Classes
  • Inheritance
  • Modules
  • Symbols
  • Iterators
  • Generators
  • Block scoping
  • Arrow functions
  • Collections
  • Template literals
  • Object literals
  • Assignment destructuring
  • Promises
  • Spread/rest parameters
  • Proxies
  • Reflection

What is ECMAScript?

  • A standard for scripting languages
  • JavaScript most popular dialect
  • Also: ActionScript, Lua

May '95

Dec '95

Sep '95

1997

Aug '96

1999

1998

2009

2003

2015

2011

2016

Mocha (Brendan Eich, Netscape)

LiveScript

JavaScript

Edition 1

JScript (Microsoft)

Edition 2

Edition 3

Edition 4

Edition 5

Edition 5.1

Edition 6

Edition 7

ECMA-262 specification

What is ECMAScript?

  • Latest finalised version ECMAScript6 (ES6, ES2015, Harmony)
  • Change to yearly naming convention (ES7 = ES2016)

Compatibility

  • Native support in browsers still being implemented
  • Support in browsers through transpiling

ES6 Transpilation

  • Transpiler compiles code latest version -> older version
  • ES5 widely supported by browsers
  • Two main ES6 -> ES5 transpilers -
  • ES6 should be compiled to ES5 for compatibility
  • Traceur
  • Babel

ES6 Transpilation

  • Babel generally preferred:
    • More readable transpiled code
    • JSX support (compatibility with React.js)
  • Alternatively use higher-level language implementing ES6 features
    • CoffeeScript
    • Dart
    • Typescript (more later)
/*jshint esnext: true */

import Todo from './todo';
import {values} from './generators';

class TodoList {
  constructor() {
    this.todos = {}; 
    this.length = 0;
  }

  add(text, done = false) {
    let todo = new Todo(text, done);
    this.todos[String(todo.timestamp)] = todo;
    this.length++;
  }

  archiveCompleted() {
    for (let todo of values(this.todos)) {
      if (todo.done) this.delete(todo);
    }
  }

  delete(todo) {
    delete this.todos[String(todo.timestamp)];
    this.length--;
  }

  getUncompletedCount() {
    let count = 0;
    for (let todo of values(this.todos)) {
      if (!todo.done) count++;
    }
    return count;
  }
}

export default TodoList;
/*jshint esnext: true */

'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

var _todo = require('./todo');

var _todo2 = _interopRequireDefault(_todo);

var _generators = require('./generators');

var TodoList = (function () {
  function TodoList() {
    _classCallCheck(this, TodoList);

    this.todos = {};
    this.length = 0;
  }

  _createClass(TodoList, [{
    key: 'add',
    value: function add(text) {
      var done = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1];

      var todo = new _todo2['default'](text, done);
      this.todos[String(todo.timestamp)] = todo;
      this.length++;
    }
  }, {
    key: 'archiveCompleted',
    value: function archiveCompleted() {
      var _iteratorNormalCompletion = true;
      var _didIteratorError = false;
      var _iteratorError = undefined;

      try {
        for (var _iterator = (0, _generators.values)(this.todos)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
          var todo = _step.value;

          if (todo.done) this['delete'](todo);
        }
      } catch (err) {
        _didIteratorError = true;
        _iteratorError = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion && _iterator['return']) {
            _iterator['return']();
          }
        } finally {
          if (_didIteratorError) {
            throw _iteratorError;
          }
        }
      }
    }
  }, {
    key: 'delete',
    value: function _delete(todo) {
      delete this.todos[String(todo.timestamp)];
      this.length--;
    }
  }, {
    key: 'getUncompletedCount',
    value: function getUncompletedCount() {
      var count = 0;
      var _iteratorNormalCompletion2 = true;
      var _didIteratorError2 = false;
      var _iteratorError2 = undefined;

      try {
        for (var _iterator2 = (0, _generators.values)(this.todos)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
          var todo = _step2.value;

          if (!todo.done) count++;
        }
      } catch (err) {
        _didIteratorError2 = true;
        _iteratorError2 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion2 && _iterator2['return']) {
            _iterator2['return']();
          }
        } finally {
          if (_didIteratorError2) {
            throw _iteratorError2;
          }
        }
      }

      return count;
    }
  }]);

  return TodoList;
})();

exports['default'] = TodoList;
module.exports = exports['default'];

ES5

ES6

  • Traceur ES6 -> ES5 transpilation: command line
/*jshint esnext: true */

import Todo from './todo';
import {values} from './generators';

class TodoList {
  constructor() {
    this.todos = {}; 
    this.length = 0;
  }

  add(text, done = false) {
    let todo = new Todo(text, done);
    this.todos[String(todo.timestamp)] = todo;
    this.length++;
  }

  archiveCompleted() {
    for (let todo of values(this.todos)) {
      if (todo.done) this.delete(todo);
    }
  }

  delete(todo) {
    delete this.todos[String(todo.timestamp)];
    this.length--;
  }

  getUncompletedCount() {
    let count = 0;
    for (let todo of values(this.todos)) {
      if (!todo.done) count++;
    }
    return count;
  }
}

export default TodoList;
$traceurRuntime.registerModule("todoservice.js", [], function() {
  "use strict";
  var __moduleName = "todoservice.js";
  var Todo = $traceurRuntime.getModule($traceurRuntime.normalizeModuleName("./todo", "todoservice.js")).default;
  var values = $traceurRuntime.getModule($traceurRuntime.normalizeModuleName("./generators", "todoservice.js")).values;
  var TodoList = function() {
    function TodoList() {
      this.todos = {};
      this.length = 0;
    }
    return ($traceurRuntime.createClass)(TodoList, {
      add: function(text) {
        var done = arguments[1] !== (void 0) ? arguments[1] : false;
        var todo = new Todo(text, done);
        this.todos[String(todo.timestamp)] = todo;
        this.length++;
      },
      archiveCompleted: function() {
        var $__7 = true;
        var $__8 = false;
        var $__9 = undefined;
        try {
          for (var $__5 = void 0,
              $__4 = (values(this.todos))[Symbol.iterator](); !($__7 = ($__5 = $__4.next()).done); $__7 = true) {
            var todo = $__5.value;
            {
              if (todo.done)
                this.delete(todo);
            }
          }
        } catch ($__10) {
          $__8 = true;
          $__9 = $__10;
        } finally {
          try {
            if (!$__7 && $__4.return != null) {
              $__4.return();
            }
          } finally {
            if ($__8) {
              throw $__9;
            }
          }
        }
      },
      delete: function(todo) {
        delete this.todos[String(todo.timestamp)];
        this.length--;
      },
      getUncompletedCount: function() {
        var count = 0;
        var $__7 = true;
        var $__8 = false;
        var $__9 = undefined;
        try {
          for (var $__5 = void 0,
              $__4 = (values(this.todos))[Symbol.iterator](); !($__7 = ($__5 = $__4.next()).done); $__7 = true) {
            var todo = $__5.value;
            {
              if (!todo.done)
                count++;
            }
          }
        } catch ($__10) {
          $__8 = true;
          $__9 = $__10;
        } finally {
          try {
            if (!$__7 && $__4.return != null) {
              $__4.return();
            }
          } finally {
            if ($__8) {
              throw $__9;
            }
          }
        }
        return count;
      }
    }, {});
  }();
  var $__default = TodoList;
  return {get default() {
      return $__default;
    }};
});
$traceurRuntime.getModule("todoservice.js" + '');

ES5

ES6

New features in ES6

Classes

  • ES5 objects inherit from Object.protoype 
  • Can instantiate and define methods using the protoype:
function Client (firstname, lastname) {
  this.id = undefined
  this.firstname = firstname
  this.lastname = lastname
}
Client.prototype.getFullName = function () {
    return this.firstname + this.lastname;
}

Client.prototype.setId = function () {
    this.id = generateGuid();
}
var Client = new Client()
client.getFullName()
client.setId()

ES5

ES5

ES5

Classes

  • ES6 classes are 'syntactic sugar' on top of prototypal inheritance
  • Makes classes easier to read/express
class Client {

    constructor(firstname, lastname) {
        this.id = undefined
        this.firstname = firstname
        this.lastname = lastname
    }

    getFullName() {
        return this.firstname + this.lastname
    }

    generateId() {
        this.id = generateGuid()
    }

}
function Client (firstname, lastname) {
  this.id = undefined
  this.firstname = firstname
  this.lastname = lastname
}

Client.prototype.getFullName = function () {
    return this.firstname + this.lastname
}

Client.prototype.setId = function () {
    this.id = generateGuid()
}

ES6

ES5

Classes

class Client {

    constructor(firstname, lastname) {
        this.id = undefined
        this.firstname = firstname
        this.lastname = lastname
    }

    getFullName() {
        return this.firstname + this.lastname
    }

    generateId() {
        this.id = generateGuid()
    }

}
  • Method signature notation - shorter, cleaner
  • No commas between properties, methods

ES6

Classes - static methods

class Client {

    constructor(firstname, lastname) {
        this.id = undefined
        this.firstname = firstname
        this.lastname = lastname
    }

    getFullName() {
        return this.firstname + this.lastname
    }

    generateId() {
        this.id = generateGuid()
    }

    static generateGuid() {
        function s4() {
            return Math.floor((1 + Math.random()) * 0x10000)
            .toString(16)
            .substring(1)
        }
        
        return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
        s4() + '-' + s4() + s4() + s4()
    }
    
}
  • Can't be called on an instantiated class
  • No access to 'this' properties/methods
  • Most useful when:
  • Associated with class, BUT
  • Don't need unique variables of instanced class

ES6

New features in ES6

Inheritance

  • In ES5, the Object.create(parentObject) method creates object-to-object inheritance
function Client (firstname, lastname) {
    this.id = undefined
    this.firstname = firstname
    this.lastname = lastname
}
var TCSClient = Object.create(Client)

TCSClient.evalSystem = "TCS"

TCSClient.firstname = "Joe Bloggs"
  • Can add extra properties to child object
  • If property does not exist, checks further up prototype 'chain'

ES5

ES5

Inheritance

  • ES6 introduces the 'extend' keyword
class TCSClient extends Client {

    this.evalSystem = "TCS"

}
var TCSClient = Object.create(Client);

TCSClient.evalSystem = "TCS"
  • Can override functions by re-implementing them in child class

ES5

ES6

class TCSClient extends Client {

    getFullName() {
        return this.firstname + " " +
            this.lastname
    }

}
var TCSClient = Object.create(Client)

TCSClient.prototype.getFullName() {
    return this.firstname + " " +
        this.lastname
}

ES5

ES6

  • Can access methods of the parent object using super()
class DRClient extends Client {

    this.vcNumberPrefix = "W"

    generateVCNumber() {
        super.generateVCNumber()
    }

}
var TCSClient = Object.create(Client)

TCSClient.vcNumberPrefix = "A"

TCSClient.prototype.generateVCNumber = function() {
    Client.prototype.generateVCNumber.call(this)
}

ES5

ES6

Inheritance

function Client () {
    this.vcNumber = undefined
    this.vcNumberPrefix = ""
}

Client.prototype.generateVCNumber = function () {
    var number = Math.floor(Math.random() * 10000000)
    this.vcNumber = this.vcNumberPrefix + pad(number, 7)
}

ES5

  • Parent class (ES5)
  • .call(this) calls function of same name on parent object
  • use super keyword to use base function, current object implied

New features in ES6

Modules

  • Files that export an API
  • Implemented in ES6 as standard
  • Emulated in libraries such as Common JS, AMD before ES6

Modules

  • Exported bindings are available to other modules that import them
// ClientService.js

class ClientService {
    
    clients = [];

    addClient(client) {
        this.clients.add(client);
    }

}

export default ClientService;
import ClientService from './ClientService';

class ClientComponent {

    submitClient(client) {

        ClientService.addClient(client);

    }

}

ES6

ES6

Modules

  • Bindings can be exported as:
  • Default bindings
class ClientService {
    
    clients = [];

    addClient(client) {
        this.clients.add(client);
    }

}

export default ClientService;

ES6

  • Only one default export per module

Modules

  • Default bindings
// ClientService.js

class ClientService {
    
    clients = [];

    addClient(client) {
        this.clients.add(client);
    }

}

export default ClientService;

ES6

  • Can give a different name when importing
import ClientService from './ClientService';

ES6

import MyService from './ClientService';

class ClientComponent {

    submitClient(client) {

        MyService.addClient(client);

    }

}

Modules

  • Difference in CommonJS / ES6 syntax
module.exports = ClientService;

CommonJS

export default ClientService;

ES6

var ClientService = require('./ClientService');

CommonJS

import ClientService from ('./ClientService');

ES6

  • N.B. CommonJS and ES6 modules are interoperable!
  • Difference in CommonJS / ES6 syntax
function foo() {
    module.exports = 'bar';    // OK
}

CommonJS

Modules

  • Exports in ES6 must be top-level
function foo() {
    export default 'bar';    // Syntax error
}

ES6

  • Can't define and expose APIs conditionally
  • Bindings can be exported as:
  • Named exports
// Constants.js

var $BANKRUPTCY_MIN_DEBT = 1001;
var $SEQUESTRATION_MIN_DEBT = 3000;

export $BANKRUPTCY_MIN_DEBT;
export $SEQUESTRATION_MIN_DEBT;

ES6

  • Many exports per module

Modules

  • Can use with default export
// Constants.js

var $BANKRUPTCY_MIN_DEBT = 1001;
var $SEQUESTRATION_MIN_DEBT = 3000;

export { $BANKRUPTCY_MIN_DEBT,
    $SEQUESTRATION_MIN_DEBT}

ES6

  • Can be expressed as a list
  • Importing named exports:

Modules

ES6

ES6

import {$BANKRUPTCY_MIN_DEBT,
    $SEQUESTRATION_MIN_DEBT} from './Constants';

function isEligibleForBankruptcySequestration(
    isScottish, debtTotal) {
   
    var minDebt;

    if(isScottish) { minDebt = $SEQUESTRATION_MIN_DEBT }
        else { minDebt = $BANKRUPTCY_MIN_DEBT };

    return debtTotal >= minDebt;

}
// Constants.js

var $BANKRUPTCY_MIN_DEBT = 1001;
var $SEQUESTRATION_MIN_DEBT = 3000;

export $BANKRUPTCY_MIN_DEBT;
export $SEQUESTRATION_MIN_DEBT;
  • Can rename when importing:

Modules

ES6

ES6

import {$BANKRUPTCY_MIN_DEBT as minBank,
    $SEQUESTRATION_MIN_DEBT as minSeq
    } from './Constants';

function isEligibleForBankruptcySequestration(
    isScottish, debtTotal) {
   
    var minDebt;

    if(isScottish) { minDebt = minSeq }
        else { minDebt = minBank };

    return debtTotal >= minDebt;

}
// Constants.js

var $BANKRUPTCY_MIN_DEBT = 1001;
var $SEQUESTRATION_MIN_DEBT = 3000;

export $BANKRUPTCY_MIN_DEBT;
export $SEQUESTRATION_MIN_DEBT;
  • Importing the entire namespace:

Modules

ES6

ES6

import * as incomes from './IncomesController'

function submitIncomes() {

    incomes.incomeSupport = this.valIncomeSupport;
    incomes.jobseekers = this.valJobseekers;
    incomes.incapacity = this.valIncapacity;
    
    this.valTotalBenefitIncome = incomes.TotalBenefitIncome;

}
// IncomesController.js

var incomeSupport;
var jobSeekers;
var incapacity;

var totalBenefitIncome = function() {
    this.valIncomeSupport +
        this.valJobSeekers +
        this.valIncapacity;
}
  • Any default export would be placed in alias.default
  • A namespace import must have an alias

Modules

  • Modules export bindings - not values or references
  • Imported items subject to change:
// ChangeNumber.js

var changingNumber = 1;

var changeNumber = function() {
    this.changingNumber += 1;
}

export changingNumber;
export changeNumber;
import {changingNumber, changeNumber}
    from './ChangeNumber';

console.log(changingNumber);    // changingNumber = 1

changeNumber;

console.log(changingNumber);    // changingNumber = 2

ES6

ES6

Modules

  • No native support in current browsers
  • Transpilers concatenate modules into one file
  • 'Strict mode' on by default in ES6 modules:
  • Variables can't be left undeclared
  • Function parameters need unique names
  • Can't use 'with'
  • Can't use octal numbers
  • Errors thrown on assignment to read only properties
  • Reserved words can't be bound (e.g. protected, static, interface)

New features in ES6

Symbols

  • New primitive type
  • Similar to strings:
  • Immutable
  • Can't add properties on them (as with any primitive)
  • Can be used as a property key
var mySymbol = Symbol("description");

console.log(typeof mySymbol);    // 'symbol'

ES6

Symbols

  • Unlike strings:
  • Unique (even where description is same)
Symbol('foo') === Symbol('foo');    // false

ES6

  • Does not use 'new' operator:
var symbol = new Symbol();    // TypeError

ES6

Symbols

  • Uses:
  • Avoiding name clashes in property keys - e.g.
  • May want to set a flag on a moving element:
if (element.isMoving) {
  smoothAnimations(element);
}
element.isMoving = true;
  • Potential for clashes with existing / future libraries
var isMoving = Symbol("isMoving");

...

if (element[isMoving]) {
  smoothAnimations(element);
}
element[isMoving] = true;

ES6

  • Creates a unique property in scope - no collisions

Symbols

  • Uses:
  • Defining protocols
  • Can flag an object as having a set of common methods by attaching certain symbols, e.g.
  • Iterable objects use the 'well-known' Symbol.iterator
var myArray = ['a', 'b', 'c'];

var myIterable = myArray[Symbol.iterator]();

ES6

> iterable.next()
{ value: 'a', done: false };
> iterable.next()
{ value: 'b', done: false };
> iterable.next()
{ value: 'c', done: false };
> iterable.next()
{ value: undefined, done: true };
  • myArray has Symbol.iterator property:
  • each item in collection has 'value' and 'done' properties
  • can iterate through list with next()

Symbols

  • Can place symbols on the 'global registry'
  • Accessible across code realms
Symbol.for('foo') === Symbol.for('foo');   // true
  • Symbol.for(key)
  • Returns symbol with key from global registry
  • Creates a symbol with key if doesn't exist
  • Description set to same as key name
  • Symbol.keyFor(symbol)
  • Returns symbol with key from global registry
var mySymbol = Symbol.for('foo');

console.log(Symbol.keyFor(mySymbol));    // 'foo'

New features in ES6

Iterators

  • ES6 introduces iterator and iterable protocols
  • 'Iterability' involves data sources and data consumers

Iterators

  • Data consumers:
  • for - of loops
  • Array.from
  • spread operator (more later)
  • Data sources: some built-ins are iterable by default in ES6
  • Arrays
  • Strings
  • Maps
  • Sets
  • Arguments (of a function)
  • DOM data structures (e.g. querySelectorAll method)

Iterators

  • The Symbol.iterator is used to assign an 'iterator' to any object
  • Symbol.iterator equates to a method that must return an object that complies with the iterator protocol.
  • To comply with the iterator protocol:
  • Object must have a next() method
  • The next() method takes no arguments
  • The next() method returns an object with at least:
  • A 'done' property: true (sequence done), or false (may be more values)
  • A 'value' property: current item in sequence
var foo = { [Symbol.iterator]: iterable }    or    foo[Symbol.iterator] = iterable

Iterators

  • Example:
[Symbol.iterator]: () => ({
    
    items: ['f', 'o', 'o'],

    next: function next() {

        return {
            done: this.items.length === 0,
            value: this.items.shift()
        }
    }
})

ES6

next() method (no args)

returns Object with done, value

Adheres to iterator protocol

Iterators

  • All Objects have '@@iterator' method in ES6 spec
  • If an Object is given a valid [Symbol.iterator] method, this is assigned to @@iterator method
  • To recap:
  • Assign method to [Symbol.iterator]  to mark object as iterable
  • [Symbol.iterator] method must return object adhering to iterator protocol
  • This method becomes object's @@iterator method and this is called when object needs to be iterated

Iterators

  • Arrays, Maps, Objects all have their own default @@iterator methods
  • Method describes how to pull values from object using a data consumer, .e.g
  • for .. of loops (new to ES6)
  • Array.from
  • spread operator

New features in ES6

Generators

  • Special kind of iterator
  • 'Generator function' declared, using asterisk notation:
function* myGenerator() {}

// or

function *myGenerator() {}

ES6

Generators

  • Generator functions contain 'yield' expressions
  • On 'yield' expression, function:
function* myGenerator() {

    yield 'f';
    console.log('o');
    yield 'o';
    console.log('b');
    yield 'a';
    console.log('r');

}
  • Suspends execution
  • Emits a value
var fooBar = myGenerator();

for(var item of fooBar) {

    console.log(item);        // 'f' 'o' 'o' 'b' 'a' 'r'

}

ES6

ES6

Generators

  • Calling next() on Generator object starts function or resume from last yield expression
function *myGenerator(x) {
    
    var y = x + 3;
    var z = 1 + (yield(y));
    return z;
}

ES6

var gen = myGenerator(2);

console.log( gen.next() );

console.log( gen.next(6) );

ES6

1 - myGenerator function instantiated with param x = 2

{ value: 5, done: false }

1

2

2 - next() called on generator function. No 'yield' expression encountered yet, so no param

3

3 - execution paused on yield expression; var y passed out as 5

4 - console logs

4

7

5 - execution resumes with yield expression as passed in param (6)

6

6 - since yield expression is 6, z becomes 7 (1 + 6)

7 - end of generator function; console logs

{ value: 7, done: true }
  • Params passed to next() replace yield expression

5

  • Allows 2-way message passing into / out of functions
  • Allows for cooperative concurrency of programming

Generators

New features in ES6

'let' and 'const'

  • Alternatives to declaring a variable with 'var'
  • 'let' block-scopes variables rather than var's function- scoping

'let' and 'const'

  • Hoisting
  • variables and function declarations get pulled to the top of their scope
var foo = true;

function bar() {
	if (!foo) {
		var foo = "Foo is false!"
	};
	console.log(foo)
}

bar()

ES5

> Foo is false!

Console:

var foo;

function bar() {
    var foo;
    if (!foo) {
        foo = "Foo is false!"
    };
    console.log(foo);
}

foo = true;

bar();

ES5

'let' and 'const'

  • Allows references to variables declared later without exceptions, but
  • Variables should be declared at top of function to avoid errors
  • Hoisting
  • Results can be unintuitive
var x = 1;
if (x) {
    var x = 2;
};
console.log(x);
> 2

Console:

ES5

'let' and 'const'

  • { } blocks do not create a new scope
  • What if we want a temporary scope for the inner x?
var x = 1;
if (x) {
    (function () {
        var x = 2;
    })
};
console.log(x);
> 1

Console:

ES5

'let' and 'const'

  • Could create a new function scope
var x = 1;
if (x) {
    let x = 2;
}
console.log(x);
> 1

Console:

ES6

'let' and 'const'

  • Using 'let' allows for block-scoping ( {} )
  • Maintains encapsulation principles
  • Allows same variable names where appropriate

'let' and 'const'

  • const:
  • must be decalred using an initializer
  • also block-scoped
  • can only be assigned to once 
const stuff = { things: ['chair', 'desk', 'computer'] };

e.g.

  • will fail silently if assigned again
const stuff = { things: ['chair', 'desk', 'computer'] };
stuff = {};                                                // fails
console.log(stuff.things);                                 // ...

e.g.

  • does not make the assigned value itself immutable
const stuff = { things: ['chair', 'desk', 'computer'] };
stuff.things.push('pot plant');
console.log(stuff.things);
// chair,desk,computer,pot plant

e.g.

New features in ES6

Arrow functions

  • Also known as lambda expressions
  • Makes code more 'terse'

Arrow functions

  • Rules:
  • A 'fat arrow' takes the place of the 'function' keyword
var arrow = function() {}
var arrow = () => {}

becomes

function() {}
() => {}

becomes

  • No parentheses required for 1 argument
  • 'return' keyword implied for single expressions in function body
function(a) { return a * a }
a => a * a

becomes

  • Parentheses for 0, or >1 arguments
function(a, b) { return a + b }
(a, b) => a + b

becomes

Arrow functions

  • In ES5, functions create their own scope
  • Calling 'this' within the function refers to itself; not the scope outside the function
  • To bring outside scope into the function, store 'this' in a variable, or use .bind()
function Counter() {
    var that = this;
    that.seconds = 0;
    setInterval(function() {
        that.seconds++
    }, 1000);
}

ES5

function Counter() {
    this.seconds = 0;
    setInterval(function() {
        this.seconds++
    }.bind(this), 1000);
}

ES5

Arrow functions

  • Best used for functions with low amount of arguments/statements
  • (num, index) => is marginally shorter than function (num, index)
  • Can't name arrow functions
  • Arrow functions are bound to their lexical scope
  • 'this' in an arrow function is the same as the outer scope
function Counter() {
    this.seconds = 0;
    setInterval(function() {
        this.seconds++
    }.bind(this), 1000);
}

ES5

function Counter() {
    this.seconds = 0;
    setInterval(() => this.seconds++, 1000);
}

ES6

New features in ES6

Collection types

  • Map
  • A key-value data structure new in ES6
  • Exists in many other languages
  • Previously only one collection type in JavaScript (Array)
  • Arrays only use numeric indices
  • Objects used when non-numeric indices necessary

Collection types

  • Map
  • ES5 maps often created using regular Objects
var hashMap = {};

function add(key, value) {
    hashMap[key] = value;
}

function get(key) {
    return hashMap[key];
}

add('request', { description: 'Simplified HTTP request client' });
add('moment', { description: 'Parse, validate, manipulate, and display dates' });
add('lodash', { description: 'The modern build of lodash modular utilities' });

ES6

Collection types

  • Map
  • Only allows strings as keys
  • Might want to use non-string values as keys:
  • Problems with this approach:
var registry = {};

function add(element, meta) {
    registry[element] = meta;
}

function get(element) {
    return registry[element];
}

add(document.getElementById("wages-panel"), { display: true });
add(document.getElementById("child-income-panel"), { display: false });
add(document.getElementById("benefits-panel"), { display: true });
add(document.getElementById("otherincome-panel"), { display: true });

ES6

Collection types

  • Map
add(document.getElementById("wages-panel"), { display: true });

ES6

  • Here the <div> element will be cast to string [Object HTMLDivElement]
  • Using elements as keys will mean constantly overwriting same key
  • Maps allow for any type to be used as key or value
var map = new Map();

map.set(new Date(), function today() {});
map.set(() => 'key', { foo: 'bar' });
map.set(Symbol('items'), [1, 2]);

ES6

Collection types

  • Map
  • Maps have many useful iteration features
  • To iterate over a javascript object used as a map:
for (let key in object) {

    if (object.hasOwnProperty(key)) {
        console.log(key, object[key]);
    }
}

using objects

  • Have to avoid properties from the object prototype being included
for (let key in object) {

    if (object.hasOwnProperty(key) &&
        typeof object[key] !== "function") {
            console.log(key, object[key]);
        }
}

using objects

  • Have to avoid methods from the prototype object being included

Collection types

  • Map
  • With Maps, no unwanted properties /methods 'leaking' from prototype to deal with
  • Maps can iterate on keys, values or entries
for (let key of map.keys()) {

    console.log(key);

}
for (let value of map.values()) {

    console.log(value);

}
for (let entry of map) {

    console.log(entry[0], entry[1]);

}
  • 'entries()' default iterator for Maps

using maps

using maps

using maps

  • each key-value pair stored as two item array

Collection types

  • Map
  • Useful Map methods:
  • set()
  • delete()
  • size()
  • clear()
  • has()
  • get()
map.get("key");

get value of 'key'

map.set("key", "value");

insert new item (will overwrite a duplicate key)

map.has("key");

checks if key exists (true/false)

map.delete("key");

deletes item from map

map.size();

gets number of items in map

map.clear();

removes all items from map (without losing reference to it)

  • Map
  • Any iterable object can be assigned to a Map

Collection types

var items = [
    [new Date(), function today() {})],
    [() => 'key', { foo: 'bar' })],
    [(Symbol('items'), [1, 2])
]

var map = new Map();

map = items;

ES6

  • Map entries are always iterated in insertion order

Collection types

  • WeakMap
  • A subset of Map, with some limitations:
  • Not iterable - no .entries(), .keys(), .values() or clear()
  • Can use .has(), .get(), .set(), .delete()
  • Every key must be an object - value types not admitted
  • A WeakMap holds references to its keys weakly
  • i.e. if no other references to a key, the entry will be garbage collected
  • WeakMap
  • Why use a WeakMap?

Collection types

  • Very specific use cases
  • Mapping values to objects that might disappear in future
  • WeakMap
  • Use case - Private class variables

Collection types

  • No concept of private class variables in ES6
const privateData = new Map();

class myClass {
    constructor (name, age) {
        privateData(this, { name: name, age: age });
    }

    getname () {
        return privateData.get(this).name;
    }

    getAge() {
        return privateData.get(this).age;
    }

}

export default MyClass;

ES6

  • Still a reference to Map when myClass instance destroyed
  • Memory leak issue
  • WeakMap

Collection types

const privateData = new WeakMap();

class myClass {
    constructor (name, age) {
        privateData(this, { name: name, age: age });
    }

    getname () {
        return privateData.get(this).name;
    }

    getAge() {
        return privateData.get(this).age;
    }

}

export default MyClass;

ES6

  • When myClass destroyed no references to key (this) exists
  • privateData will be garbage collected
  • memory freed
  • WeakMap
  • Storing data on a DOM element

Collection types

let answersMap = new WeakMap();

let childBenefit = document.getElementById('child-benefit');

myElement.addEventListener('change', function() {
    var answerFrequency = getFrequency();
    answersMap.set(childBenefit, {frequency: answerFrequency}
}, false);

ES6

  • Storing the frequency of an answer on the DOM element
let childNumber = document.getElementById('child-number')

if(childNumber.number === 0) {
    childBenefit.parentNode.removeChild(childBenefit)
}

ES6

  • If childBenefit key removed, entry is garbage collected
console.log(answerMap.get(childBenefit)        //  undefined

ES6

  • Set
  • Very similar to map, but only have values

Collection types

  • Values used as keys, so must be unique
  • set.set becomes set.add
  • no set.get() (get(value) => value)
  • WeakSet
  • use set when all values must be unique
  • As Map -> WeakMap
  • Not iterable
  • No other references to value - garbage collection

New features in ES6

Template literals

  • A string wrapped in backticks ( ` )
  • Can be multiline
var text = `
    This is a template literal
    'It can span multiple lines'
    "And can contain quotation marks"

    `

ES6

  • Strings can contain ' and " characters without escaping
var text = (
    'This is the old method\n' +
    'for strings spanning\n' +
    'several lines'
)

var text = [
    'Or',
    'perhaps',
    'this'
].join('\n')
    

ES5

Template literals

  • Can interpolate expressions with ${expression}
var text = `the time and date is ${today.toLocaleString()}`

// 'the time and date is 1/1/2016, 09:20:00 AM'

ES6

  • Particularly useful for chunks for HTML with interpolated variables - e.g. Angular2 templates

Template literals

@View({

    template:`
        <div *ng-if="userService.isAnyUsers()">
    	    <div class="userlist-search">
	        <p><label>Search user:</label>  <input type="text" [(ng-model)]='searchTerm'></p>
    	    </div>
        </div>
    `

})

Angular2

New features in ES6

Object literals

  • ES6 enhancements:
  • Comma separated-list of name-value pairs in curly braces
var myObject = { foo:'bar', baz: 'qux' }

ES5

  • If property value matches property name, can omit value
var foo = { bar: 'bar' }

console.log(foo.bar)    // 'bar'

ES5

var foo = { bar }

console.log(foo.bar)    // 'bar'

ES6

Object literals

  • ES6 enhancements:
  • Can compute property names using []
var foo = 'bar'
var baz = { [foo]: 'qux' }
console.log(baz)

// { bar: 'qux' }

ES6

var foo = { [bar + baz]: 'qux' }
console.log(foo)

// { barbaz: 'qux' }

ES6

  • Possible use case:
function getAnswerModel (dataItem, amt, frequency) {
    return {
        ['ans_' + dataItem] : {
            amount: amt;
            frequency: freq
        }
    }
}

ES6

Object literals

  • ES6 enhancements:
  • declaring methods on an object
var foo = {

    bar(baz) { }

}

ES6

var foo = {

    bar: function(baz) { }

}

ES5

  • 'function' keyword inferred from context

Object literals

  • ES6 enhancements:
  • getters and setters declared on functions
var = clientDetails {

    name: 'Joe Bloggs',

    get name(value) {
        return this.name
    },

    set name(value) {
        if (!value) {
            throw new Error('Name invalid')
        }
        this.name = value
    }

}
    

ES6

New features in ES6

Assignment destructuring

  • An object literal lets us create multiple properties at the same time
let myObject = { foo:'bar', baz: 'qux' }
  • An object pattern lets us extract multiple properties at the same time

ES6

let { foo, baz } = myObject

console.log(foo)                        // 'bar'

console.log(baz)                        // 'qux'

ES6

  • Properties mapped to aliases

Assignment destructuring

  • Properties not found are saved as undefined
let myObject = { foo:'bar', baz: 'qux' }

let { norf: a } = myObject

console.log(a)                        // undefined

ES6

  • Properties can be mapped to aliases
let { foo : f, baz : b } = myObject

console.log(f)                        // 'bar'

console.log(b)                        // 'qux'

ES6

Assignment destructuring

  • Patterns can be nested arbitrarily deeply
let myObject = { foo: [{ bar: 'baz', qux: 'norf' }] }

let { foo: [{ bar: a }] } = myObject

console.log(a)                        // 'baz'

ES6

  • Can define default values if 'pulled' property is undefined
let myObject = { foo:'bar', baz: undefined }

let { baz = 'default' } = myObject

console.log(baz)                        // 'default'

ES6

Assignment destructuring

  • Can skip over items not cared about:
let [, x, y] = [1, 2, 3]

console.log(x)                        // 2
console.log(y)                        // 3

ES6

  • Similarly, for arrays:
let [x, y] = [1, 2, 3]

console.log(x)                        // 1
console.log(y)                        // 2

ES6

Assignment destructuring

  • Can swap variables without need for a temporary variable
function swapIf () {
    var left = 10;
    var right = 20;
    var aux;
    
    if(right > left) {
        aux = right;
        right = left;
        left = aux;
    }
}

ES5

function swapIf () {

    var left = 10
    var right = 20    
    
    if(right > left) {
        [left, right] = [right, left]
    }
}

ES6

Assignment destructuring

  • Can destructure a function's parameter list
function displayNameAge ( {name, age} ) {
    console.log(`${name} is ${age} years old`)
}

displayNameAge( { age: 27, name: 'John'} )            // 'John is 27 years old'

ES6

Assignment destructuring

  • Possible use cases
function getCoordinates() {
    return {
        x: 5,
        y: 22
    }
}

var {x, y} = getCoords()

console.log(x)                   // 5
console.log(y)                   // 22

ES6

  • More terse interaction with returned objects
function getCoordinates() {
    return {
        x: 5,
        y: 22
    }
}

var object = getCoordinates();
var x = object.x;
var y = object.y;

console.log(x)                   // 5
console.log(y)                   // 22

ES5

Assignment destructuring

  • Possible use cases
  • Define default options for a method
  • Similar to named parameters in C#
function random() ( {min=1, max=100} ) {

    return Math.floor(Math.random() * (max - min) + min

}

console.log(random( {} ))                                // 67
console.log(random( { max: 20 } )                        // 11

ES6

Assignment destructuring

  • Possible use cases
  • Using regular expressions to break down longer strings and name them
function getUrlParts(url) {
  var regEx = /^(https?):\/\/(debtremedy.stepchange\.org)(\/page\/([a-z0-9-]+))$/
  return regEx.exec(url)
}

var parts = getUrlParts('http://debtremedy.stepchange.org/page/benefitIncome')
var [,protocol,host,pathname,slug] = parts
console.log(protocol)                                                            // 'http'
console.log(host)                                                                // 'debtremedy.stepchange.org'
console.log(pathname)                                                            // '/page/YourIncome'
console.log(slug)                                                                // 'YourIncome'

ES6

New features in ES6

Spread/rest parameters

  • Rest parameters
  • Creates an array out of a function's arguments
  • ES5 - used 'arguments' variable to work with large numbers of arguments
function concat() {
    
    return Array.prototype.slide.call(arguments).join(' ')

}

var result = concat('Lorem', 'ipsum', 'dolor', 'sit', 'amet')

console.log(result)                                            // 'Lorem ipsum dolor sit amet'

Spread/rest parameters

ES5

  • Using rest parameter syntax
function concat(...words) {
    
    return words.join(' ')

}

var result = concat('Lorem', 'ipsum', 'dolor', 'sit', 'amet')

console.log(result)                                            // 'Lorem ipsum dolor sit amet'

ES6

  • If other parameters, rest param must be rightmost
function concat(prefix, suffix, ...words) {
    
    return prefix + words.join(' ') + suffix

}

var result = concat('START', 'END', 'Lorem', 'ipsum', 'dolor', 'sit', 'amet')

console.log(result)                                                  // 'START Lorem ipsum dolor sit amet END'

ES6

  • If 'arguments' was used, need to cut first two params by shifting

ES5

function concat() {
    
    var words = Array.prototype.slide.call(arguments);
    var prefix = words.shift();
    var suffix = words.shift();
    return prefix + words.join(' ') + suffix;

}

var result = concat('START', 'END', 'Lorem', 'ipsum', 'dolor', 'sit', 'amet')

console.log(result)                                                  // 'START Lorem ipsum dolor sit amet END'

Spread/rest parameters

Spread/rest parameters

  • Spread operator
  • Passes an array as arguments to a function
var missing = [4, 5, 6]

var sequence = [1, 2, 3, ...missing, 7, ,8 , 9]

ES6

  • Prefix array with '...'
  • To recap:
  • spread/rest operators both use '...' syntax
  • Rest operator extracts data (creates array from parameters)
  • Spread operator constructs data (inserts parameters from array)

New features in ES6

  • What if events happen before listeners are established?

Promises

  • Javascript is single threaded
  • To avoid program suspension, e.g. when loading an image, we might use an event listener
var img = document.querySelector('.img');

img.addEventListener('load', function() {
    // Image loaded
});

imag.addEventListener('error', function() {
    // Error
}

ES5

Promises

  • Could check image is complete before listener established 
var img = document.querySelector('.img');

if(img.complete) {
    loaded();
}
else (
    img.addEventListener('load', function() {
    loaded();
});

img.addEventListener('error', function() {
    // Error
}

function loaded() {
    // Loaded
}

ES5

  • Still not catching errors on images before listener is attached (no property for this)
  • Complexity increases if we want to know when set of images loaded

Promises

  • Events fine for things that happen multiple times on same object 
img.callThisIfLoadedOrWhenLoaded(function() {
    // loaded
}).orIfFailedCallThis(function() {
    // failed
});
  • e.g. 'click', 'change', 'keyup'
  • Not important if these occur before listener attached
  • For async success/failure, we want something like:
whenAllTheseHaveLoaded([img1, img2].callThis(function() {
    // loaded
}).orIfSomeFailedCallThis(function() {
    // failed
});
  • Or for multiple objects

Promises

  • Promises do exactly this:
img.ready().then(function() {
    // loaded
}), function() {
    // failed
});
Promise.all([img1.ready(), img2.ready()]).then(function() {
    // loaded
}), function() {
    // failed
});
  • If an 'img' element had a 'ready()' method returning a promise:
  • or for multiple 'img' elements:
var promise = new Promise(function(resolve, reject) {
    
    // do something

    if( /* thing was done */ ) {
        resolve("Success");
    }
    else {
        reject(Error("Failed"));
    }

});

ES6

  • Takes one argument:
  • Do something with the callback (perhaps async)
  • Call resolve() if worked
  • Call reject() if not

callback with 'resolve' and 'reject' params

Promises

  • Creating a promise:
  • Using a promise:
promise.then(function(result) {
    // Action on success
}, function(err) {
    // Action on error
});

ES6

  • then() takes two arguments:

callback for success case

callback for failure case

function getData($timeout, $q) {

    return function() {

        var defer = $q.defer()

        // async function
        setTimeout(function() {
            if (Math.round(Math.random())) {
                defer.resolve('data received');
            }
            else {
                defer.reject('error');
            }
        }, 1000);
        
        return defer.promise
    }
}

AngularJS

Promises

  • Have existed non-natively for a while
  • May have used deferred objects in AngularJS
  • Deferred object exposes a promise and resolve/reject methods

Promises

  • Can chain 'then's to run async actions in sequence
getJSON('faq.json').then(function(faq) {
    return getJSON(faq.sectionUrl[0]);
}).then(function(section1) {
    console.log('Section 1: ' + section1);
}

ES6

  • first call to 'then()' - response is a promise
  • the next 'then()' is only called when that promise is resolved
  • 'then()' returns a value 
  • the value is passed as parameter to the next 'then()' call

ES6 resources

ES6 Overview in 350 Bullet Points, ponyfoo.com:

2ality, various tutorials and free ES6 ebook:

HTML5 rocks (various tutorials, good explanation of Promises):

JavaScript playground (various tutorials):

Most of the code examples in this presentation are adapted from ones from the following sources:

Introduction to TypeScript

David Kane

What is TypeScript?

  • Superset of ES6
  • Includes all ES6 features
  • 'Syntactic sugar' for JavaScript
  • makes programs more readable
  • makes coding more intuitive
  • Worked on by Anders Hejlsberg, lead C# architect
  • May look more similar to C# / Java than JavaScript
  • Free, open source, Microsoft-developed
  • Translates ES6 features (classes, modules, etc.) into ECMAScript 5
  • Browser compatibility
  • Existing JavaScript programs are valid TypeScript programs
  • Designed to meet the needs of teams developing large JavaScript programs
  • How?

What is TypeScript?

TypeScript: features

  • Variable type inferred from value
var n = 1
console.log(typeof(n))            // number

var s = "Hello World"
console.log(typeof(s))            // string

function f() {
    return "hello"
}

console.log(typeof(f.call()))    // string

TypeScript

Type inference/annotation

  • Might not know data type provided by user or 3rd party library
var notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;

var list:any[] = [1, true, "free"];

TypeScript

TypeScript: features

  • In these cases can use 'any' type
  • Can also use 'any' type for mixed arrays
  • Can opt-out of type checking and let values be checked at compile time

Type inference/annotation

  • Can express return type for functions
function add(num1, num2): number {
    return num1 + num2;
}

function concat(str1, str2): string {
    return str1 + str2;   
}

TypeScript

  • 'void' type is the absence of any type at all
function warnUser(): void {
    alert("This function doesn't return a type");
}

TypeScript

  • commonly used as return type of functions with no return value

Type inference/annotation

  • Can express type requirement for parameters
function add(num1: number, num2: number) {
    return num1 + num2;
}

function concat(str1: string, str2: string) {
    return str1 + str2;   
}

TypeScript

  • Type checking immediately removes an entire class of bugs from codebase
  • Useful for teams working on different parts of a program

Type inference/annotation

TypeScript: features

  • Parameters always optional in JavaScript functions
  • TypeScript prohibits omitting typed arguments unless specified as optional ('?')
function getRange(max, min, exclusive?) {
    // ...
}

TypeScript

  • Effectively gives it a default parameter value of 'undefined'
function getRange(max, min, exclusive = undefined) {
    // ...
}

TypeScript

Optional parameters

TypeScript: features

  • Added datatype
enum Color {Red, Green, Blue}

var colorName: string = Color[2]

alert(colorName)                     // 'Green'

TypeScript

  • Gives 'friendly' names to sets of numeric values
  • Members numbered starting with 0 - though can be manually set
enum Color {Red = 3, Green = 1, Blue = 2}

var colorName: string = Color[1]

alert(colorName)                     // 'Green'

TypeScript

Enums

TypeScript: features

  • Define the required structure of an object
interface ILabelledValue {
    label: string;
}

function printLabel(labelledObj: ILabelledValue) {
    console.log(labelledObj.label)
}

var myObj = { size: 10, label: "Size 10 object" };
printLabel(myObj);

TypeScript

  • 'labelledObj' must satisfy requirement defined in interface (has string 'label' property)
  • can add other properties as long as required ones exist

Interfaces

  • Define 'contracts' between code within, and outside project
  • Objects used to communicate data are recognisable to sender and receiver

Interfaces

  • Why use interfaces?
  • Can use optional properties ('?')
interface ILabelledValue {
    label: string;
    size?: number;
}

TypeScript

  • Don't have to create these properties when implementing interface

Interfaces

  • Will catch mistyped property names
function printLabel(labelledObj: ILabelledValue) {
    console.log(labelledObj.label)
}

var myObj = { sise: 10, label: "Size 10 object" };
printLabel(myObj);                                    // error - myObj does not satisfy LabelledValue

TypeScript

  • can extend other interfaces

Interfaces

interface IRectangle {
    x: number;
    y: number;
}
interface ICuboid extends IRectangle {
    z: number;
}

TypeScript

TypeScript

  • can extend multiple interfaces
interface IColor {
    r: number;
    g: number;
    b: number;
}
interface IColoredCuboid extends ICuboid, IColor {
    alpha: number;
}

TypeScript

TypeScript

interface ISearchFunc {
    (source: string, subString: string): boolean;
}

TypeScript

  • can also describe functions
  • like a function declaration with only param list and return type:
  • param names don't need to match - just the types
var mySearch: ISearchFunc;

mySearch = function(src: string, sub: string) {
    var result = src.search(sub);
    if (result == -1) {
        return false;
    }
    else {
        return true;
    }
}

TypeScript

Interfaces

  • known as a method 'signature'
interface IStringArray {
    [index: number]: string;
}

var myArray: IStringArray;
myArray = ["John", "Doe"];

TypeScript

  • can describe arrays

Interfaces

  • arrays have 'index' types - types allowed to index object
  • must be string or number
  • string-indexed arrays: dictionaries
interface IClock {
    currentTime: Date;
    setTime(d: Date);
}

class MyClock implements IClock {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m:number) { }
}

TypeScript

  • can describe classes

Interfaces

property

method

  • an interface can't have a constructor signature - why?
  • constructor only defined on class
  • static vs instance variables/methods

Interfaces

  • static: one per class

shared by every Object of that Class

  • instance: one per Object

each Object of that Class has its own copy

  • when a class implements an interface, only instance side is checked
  • constructor is a static method
  • since it creates the instance, must be invoked before the instance is created

TypeScript: features

Private/public modifiers

  • In C#/ Java, to make class members visible outside class we need explicit 'public' label
  • All class members public by default in TypeScript
  • Members can be marked as private
  • Interfaces do not check against private members

Private/public modifiers

  • When comparing two objects with private members, only equivalent if private members come from same declaration, e.g.
class Mammal {
    private name: string;
    constructor(_name: string) { this.name = _name; }
}

TypeScript

class Reptile {
    private name: string;
    constructor(_name: string) { this.name = _name; }
}

TypeScript

  • The classes appear identical in structure, but
var cat = new Animal('Cat');
var lizard = new Animal('Lizard');
cat = lizard;                        // error: Animal and Reptile are not compatible
  • Not equivalent as private members taken from different declarations

Private/public modifiers

class Mammal {
    private name: string;
    constructor(_name: string) { this.name = _name; }
}

TypeScript

class Dog extends Animal {
    constructor() { super("Dog"); }
}

TypeScript

var cat = new Animal('Cat');
var dog = new Dog();
cat = dog;                        // OK
  • Private members declared in same object - equivalent

TypeScript: features

Generics

  • Exist in C# / Java
  • Allow for creation of components that can work with a variety of types
function echo(arg: any): any {
    return arg;
}

TypeScript

  • This function returns the argument passed to it
  • It is 'generic' in that it will accept any type of argument passed to it
  • Will lose the type of object passed in (cast to any)

Generics

  • Can use a 'type variable' to capture type of argument passed in
function echo<T>(arg: T): T {
    return arg;
}

TypeScript

  • Expressed with <T> at end of function or property
  • This method truly 'generic' as it can use any type and not lose information about the type.

Generics

  • Compiler enforces correct use of generically typed parameters, e.g.
function echo<T>(arg: T): T {
    console.log(arg.length());                // error - arg might not have .length member
}

TypeScript

  • If we knew we only wanted the function to accept types with a .length() property:
interface IHasLength {
    length: number;
}

function echo<T extends IHasLength>(arg: T): T {
    console.log(arg.length());                        // now we know arg has a length property
}

TypeScript

  • Force T to comply with interface that has .length property - adding a 'constraint'

Generics

  • function will now not work for all types, though:
interface IHasLength {
    length: number;
}

function echo<T extends IHasLength>(arg: T): T {
    console.log(arg.length());                        // now we know arg has a length property
}

echo(3);                                              // Error - number doesn't have .length property
echo({length: 5, value: 3});                          // OK

TypeScript

Generics

  • Generic classes
class GenericClass<T>{
    zeroValue: T;
    add: (x:T, y:T) => T;
}
  • Generic type parameter list in <> following class name
  • Putting type parameter on class itself gives all members of class access to it
  • Class has potential to work with variety of types
var myGenericNumberClass = new GenericClass<number>();
myGenericNumberClass.zeroValue = 0;
myGenericNumberClass.add = function(x, y) { return x + y };
var myGenericStringClass = new GenericClass<string>();
myGenericStringClass.zeroValue = "";
myGenericStringClass.add = function(x, y) { return x + y };

Generics

  • Static members of generic classes cannot use type parameter
  • Only generic over 'instance side'

TypeScript: features

Mixins

  • Classes can be built up from simpler partial classes
class Join() {
    function concat (...words) {
        return words.join(' ')
    }
}

TypeScript

class Split {
    function split (word) {
        return word.split(' ');
    }
}

TypeScript

class StringOperations implements Join, Split {
    
    prefix(word) {
        return "START" + word;
    }
    
    // Join
    join: (string[]) => string;
    // Split
    split: (string) => string;
}

TypeScript

Mixins

TypeScript

class StringOperations implements Join, Split {
    
    prefix(word) {
        return "START" + word;
    }
}
  • Instead of using 'extends', uses 'implements'
  • Treats classes as interfaces, so only member types used rather than implementations
  • This would usually mean providing implementation in-class  - but this is what we want to avoid using Mixins

TypeScript

class StringOperations implements Join, Split {
    
    prefix(word) {
        return "START" + word;
    }
    
    // Join
    join: (string[]) => string;
    // Split
    split: (string) => string;
}
  • To satisfy the need for implementations, we create stand-in properties for partial method members

Mixins

  • Then use a helper function to do mixing for us
applyMixins(StringOperations, [Join, Split]);

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            derivedCtor.prototype[name] = baseCtor.prototype[name];
        })
    }); 
}

TypeScript

TypeScript: features

Declaration merging

  • Typescript merges any number of declarations with same name into a single defintion
interface IShape {
    positionX : number;
    positionY: number;
}

TypeScript

interface IShape {
    height: number;
    width: number;
}

TypeScript

var shape: IShape = { positionX: 10, positionY: 5, height: 20, width: 15 }

TypeScript

Declaration merging

  • Interface merging
  • Merges the members of both declarations into a single interface with same name

Declaration merging

  • Interface merging
  • Members of the interfaces must be unique or the compiler will error, except:
  • Function members of the same name are treated as overload methods
interface Document {
    createElement(tagName: any): Element;
}
interface Document {
    createElement(tagName: string): HTMLElement;
}
interface Document {
    createElement(tagName: "div"): HTMLDivElement; 
    createElement(tagName: "span"): HTMLSpanElement;
    createElement(tagName: "canvas"): HTMLCanvasElement;
}

TypeScript

interface Document {
    createElement(tagName: "div"): HTMLDivElement; 
    createElement(tagName: "span"): HTMLSpanElement;
    createElement(tagName: "canvas"): HTMLCanvasElement;
    createElement(tagName: string): HTMLElement;
    createElement(tagName: any): Element;
}

TypeScript

Declaration merging

  • Interface merging
  • overloads declared later take precedence

Declaration merging

  • Module merging
  • Similarly, modules of same name will merge members into one module
module Shape {

    export interface IDimensions { width: number, height: number }
    
    export class Square { }
    export class Rectangle { }
    export class Triangle { }

}
module Shape {
    
    var pi = 3.1415926
        
    export function calculateArea( radius: number ) {
        return radius * radius * this.pi
    }

    export interface IDimensions { radius: number }
    
    export class Circle { }

}

TypeScript

TypeScript

module Shape {
        
    export function calculateArea( radius: number ) {
        return radius * radius * this.pi
    }

    export interface IDimensions { width: number, height: number, radius: number }
    
    export class Square { }
    export class Rectangle { }
    export class Triangle { }
    export class Circle { }

}

TypeScript

Declaration merging

  • Module merging

Declaration merging

  • Module merging
module Shape {
        
    export function calculateArea( radius: number ) {
        return radius * radius * this.pi
    }

    export interface IDimensions { width: number, height: number, radius: number }
    
    export class Square { }
    export class Rectangle { }
    export class Triangle { }
    export class Circle { }

}

TypeScript

  • Interfaces merged
  • Not desirable, e.g. radius not valid for square
  • 'pi' variable not exported so not visible in merged module
  • exported function will error

TypeScript: features

Declaration files

  • a .d.ts file required when using an external JavaScript library or new host API
  • declaration file exposes the 'shape' of that library to TypeScript
  • exposed objects, functions, classes, interfaces
  • many popular libraries will have a definition file you can use

Declaration files

var Geometry = {

    point: (function() {
        function point(x, y) {
            this.x = x;
            this.y = y;
        }
        return point;
    }){},

    line: (function() {
        function line(p1, p2) {
            this.point1 = {x: p1.x, y = p1.y};
            this.point2 = {x: p2.x, y = p2.y};
        }
        return line;
    }){},

    distance: function (line) {
        var p1 = line.point1;
        var p2 = line.point2;
        return Math.sqrt(Math.pow(p2.x - p1.x, 2) +
            Math.pow(p2.y - p1.y, 2)
        };

};

Geometry.js

declare module MGeometry {

}
  • Constructing Geometry.d.ts
  • A module declaration:
declare module MGeometry {

    export interface Main {
        
    }

}
  • An interface importable into the TypeScript file:

Declaration files

var Geometry = {

    point: (function() {
        function point(x, y) {
            this.x = x;
            this.y = y;
        }
        return point;
    }){},

    line: (function() {
        function line(p1, p2) {
            this.point1 = {x: p1.x, y = p1.y};
            this.point2 = {x: p2.x, y = p2.y};
        }
        return line;
    }){},

    distance: function (line) {
        var p1 = line.point1;
        var p2 = line.point2;
        return Math.sqrt(Math.pow(p2.x - p1.x, 2) +
            Math.pow(p2.y - p1.y, 2)
        };

};

Geometry.js

  • The properties/functions exposed by the library:
declare module MGeometry {

    export interface Main {
        point:Point;
        line:Line;
        distance(line:Line):number;    
    }

}
  • Interfaces describing shape of individual members:
    export interface Point {
        x:number;
        y:number;
        new(x:number, y:number):Point;
    }

    export interface Line {
        point1:Point;
        point2:Point;
        new(p1:Point, p2:Point):Line;
    }

Declaration files

var Geometry = {

    point: (function() {
        function point(x, y) {
            this.x = x;
            this.y = y;
        }
        return point;
    }){},

    line: (function() {
        function line(p1, p2) {
            this.point1 = {x: p1.x, y = p1.y};
            this.point2 = {x: p2.x, y = p2.y};
        }
        return line;
    }){},

    distance: function (line) {
        var p1 = line.point1;
        var p2 = line.point2;
        return Math.sqrt(Math.pow(p2.x - p1.x, 2) +
            Math.pow(p2.y - p1.y, 2)
        };

};

Geometry.js

  • Perhaps give the interface a more friendly name
declare module MGeometry {

    export interface Main {
        point:Point;
        line:Line;
        distance(line:Line):number;
    }

    export interface Point {
        x:number;
        y:number;
        new(x:number, y:number):Point;
    }

    export interface Line {
        point1:Point;
        point2:Point;
        new(p1:Point, p2:Point):Line;
    }

    declare var Geometry:MGeometry.Main;

}

TypeScript resources

TypeScript handbook (pretty much everything you need to know):

Language specification (uses a lot of the same examples):

Introduction to WebComponents

David Kane

What are WebComponents?

  • A way of writing your own HTML tags and behaviours
  • Using WebComponents a project might look like this:
<html>

<head>
    ......
</head>

<body>
    <app-header>
        <li link="/home" class="active">Home</li>
        <li link="/about">About</li>
        <li link="/contact">Contact</li>
    </app-header>

    <app-filterlist>
        <p>Item 1</p>
        <p>Item 2</p>
        <p>Item 3</p>
        <p>Item 4</p>
        <p>Item 5</p>
    </app-filterlist>

    <app-fatfooter>
        <section>
            <li link="/linkedin" class="active">LinkedIn</li>
            <li link="/google">Google</li>
            <li link="/facebook">Facebook</li>
        </section>
        <section>
            <li link="/blog" class="active">Blog</li>
            <li link="/youtube">Youtube</li>
        </section>
    </app-fatfooter>
</body>

</html>
        

What are WebComponents?

What are WebComponents?

  • Easier to visually parse
  • The custom tags represent re-usable bits of code
  • Code can be reused over multiple pages or projects
  • Cuts down on repeated sections of boilerplate code

What are WebComponents?

  • e.g. for a navbar in Bootstrap, need to copy/paste:

for each page!

<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
    <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex6-collapse">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">Brand</a>
    </div>
    <div class="collapse navbar-collapse navbar-ex6-collapse">
        <ul class="nav navbar-nav">
            <li class="active"><a href="#">Home</a></li>
            <li><a href="#">About</a></li>
            <li><a href="#">Contact</a></li>
        </ul>
    </div>
</nav>

What are WebComponents?

  • 4 main elements to WebComponents spec:
  • HTML Templates
  • Shadow DOM
  • Custom Elements
  • HTML Imports

WebComponents specification

HTML Templates

  • Take the form <template></template>
  • 'Custom tag'
  • Any code inside this tag is just parsed, not rendered

HTML Templates

  • Example: HTML5 audio controls wrapped in a <template></template> tag
<template id="myTemplate">
    <img src="image.png">
    <audio controls>
        <source src="audio.ogg" type="audio/ogg">
        <source src="audio.ogg" type="audio/mpeg">
        Your browser does not support the audio tag.
    </audio>
</template>

HTML

  • Were this in the DOM the audio controls would not be rendered

HTML Templates

  • Content inside <template> tags hidden from the DOM
  • Wouldn't show up in 'Elements' view in browser tools by default
  • Couldn't find <img> tag within the <template> using document.querySelectorAll("template.img")

WebComponents specification

Shadow DOM

  • Allows browser to include subtrees of DOM elements not in main document DOM tree
  • Shadow DOM not visible in 'light' DOM by default
  • Can peek into an element's Shadow DOM using Chrome dev tools:

Shadow DOM

Shadow DOM

  • Restricts visibility and accessibility of component's undelying code
  • Encapsulates markup, styles and scripts according to OO principles
  • Before ShadowDOM, only way to isolate parts of HTML code was iFrames

Shadow DOM

  • Terminology
  • A document tree is a node tree (DOM) whose root node is a document
  • A shadow host is an element hosting one or more hidden node trees, or shadow trees
  • A shadow root is the root node of a shadow tree
  • If more than one shadow tree under same shadow host, more recently added shadow tree is the younger shadow tree
  • Less recently added one is the older shadow tree
  • Root nodes of these are the younger shadow node and older shadow node

Shadow DOM

  • Terminology
  • The shadow boundary is the boundary between what we can see in the DOM and the invisible implementation detail

Shadow DOM

  • Example: adding a Shadow DOM to an element
<!-- HTML holder -->
<div id="myMessage">
    <h1>
        This is a message
    </h1>
</div>

<!-- Template -->
<template id="message">
    <h1>
        This is a new message
    </h1>
</template>

HTML

page output

  • Since contents of <template> are not parsed as HTML, only content of HTML holder shows

Shadow DOM

<!-- HTML holder -->
<div id="myMessage">
    <h1>
        This is a message
    </h1>
</div>

<!-- Template -->
<template id="message">
    <h1>
        This is a new message
    </h1>
</template>

HTML

  • Example: adding a Shadow DOM to an element
var host = document.querySelector('#myMessage');

var root = host.attachShadow(open);

var template = document.querySelector('#message');

root.appendChild(template.content);

JavaScript

  • Create a shadow root on the 'myMessage' DOM element
  • Append template content to that shadow root

Shadow DOM

  • Example: adding a Shadow DOM to an element

Resultant DOM:

page output

  • Original content of div no longer showing
  • Shadow root takes precedence

Shadow DOM

  • Example: adding a Shadow DOM to an element
<!-- HTML holder -->
<div id="myMessage">
    <h1 slot="heading">
        This is a message
    </h1>
    <div slot="subheading">
        This is another message
    </div>
</div>

<!-- Template -->
<template id="message">
    <div style="color:red;">
        <slot name=".heading"></content>
    </div>
    <h2>
        <slot name=".subheading"></content>
    </h2>
</template>

HTML

var host = document.querySelector('#myMessage');

var root = host.attachShadow(open);

var template = document.querySelector('#message');

root.appendChild(template.content);

JavaScript

  • Can use HTML holder's original content
  • <slot> tag used to grab contents of original div with corresponding slot property (previously <content> tag)

Shadow DOM

  • Example: adding a Shadow DOM to an element
  • <slot> tag wrapped in selectors that change presentation of content

page output

<!-- HTML holder -->
<div id="myMessage">
    <h1 slot="heading">
        This is a message
    </h1>
    <div slot="subheading">
        This is another message
    </div>
</div>

<!-- Template -->
<template id="message">
    <div style="color:red;">
        <slot name=".heading"></slot>
    </div>
    <h2>
        <slot name=".subheading"></slot>
    </h2>
</template>

HTML

Shadow DOM

  • Visualization
  • When a new shadow root created and appended to DOM:

Shadow DOM

  • Visualization
  • As rendered:

Shadow DOM

  • Events
  • Events fired from within shadow tree can be listened to in the document.
  • The source of the event would be logged as the <audio> element itself, rather than button inside it 
  • e.g. 'mute' button in an <audio> HTML5 element is clicked, event listener on an enclosing div would hear it
<div onclick="alert('fired by: ' + event.target)">
    <audio controls src="test.wav"></audio>
</div>

HTML

  • Events 'retargeted' when crossing shadow boundary to avoid exposing internals of shadow subtree

Shadow DOM

  • Events
  • Some events never cross the 'shadow boundary':
  • abort
  • scroll
  • load
  • select
  • error
  • change
  • reset
  • resize
  • selectstart

Shadow DOM

  • Shadow DOM and CSS
  • Selectors don't cross the shadow boundary
  • Styles defined in shadow tree are scoped to that tree
<div><h3>Light DOM</h3></div>

HTML

var root = document.querySelector('div').attachShadow(open);
root.innerHTML = '<style>h3{ color: red; }</style>' + 
                 '<h3>Shadow DOM</h3>';

JavaScript

page output

  • This allows styles to be fully encapsulated within a component

Shadow DOM

  • Shadow DOM and CSS
  • Can 'include' stylesheets within templates:
<template>
    <style>{{ include: "./normalize.css" }}</style>
    <style>{{ include "./layout-utils.css }}</style>
    <div>
        <!-- template elements -->
    </div>
</template>

HTML

Shadow DOM

  • Shadow DOM and CSS
  • There were a few options to allow host document CSS to pierce the shadow boundary
  • These have since been removed (against principles of style encapsulation?)

Shadow DOM

  • Shadow DOM and CSS
  • Can uss CSS variables to create placeholders in Shadow DOM

HTML

<style>
    #host {
        --button-text-color: green;
        --button-font: "Times New Roman", Georgia, Serif;
    }
</style>
<div></div>

<script>
var root = document.querySelector('div').attachShadow(open);
root.innerHTML = '<style>' + 
                 'button {' +
                    'color: var(--button-text-color, blue);' +
                    'font-family: var(--button-font);' +
                 '{' +
                 '</style>' +
                 '<content></content>';
</script>

Shadow DOM

  • Shadow DOM and CSS
  • If we are using <slot> to pull content from light DOM to Shadow DOM, retains styling from light DOM
  • We can override this from within the Shadow DOM using ::slotted pseudo-element

page output

  • Shadow DOM and CSS
<div>
  <h3>Light DOM</h3>
  <section>
    <div>I'm not underlined</div>
    <p>I'm underlined in Shadow DOM!</p>
  </section>
</div>
var div = document.querySelector('div');
var root = div.attachShadow(open);
root.innerHTML = '<style>' +
      'h3 { color: red; }' +
      'content[select="h3"]::slotted > h3 {' +
        'color: green;' +
      '}' +
      '::slotted section p {' +
        'text-decoration: underline;' +
      '}' +
    '</style>' +
    '<h3>Shadow DOM</h3>' +
    '<slot name="h3"></slot>' + 
    '<slot name="section"></slot>';

HTML

JavaScript

Shadow DOM

WebComponents specification

Custom Elements

  • At its simplest level, HTML templates + Shadow DOM
  • Allows developers to define new types of HTML element
  • Name should always contain a hyphen
  • e.g. <my-element></my-element>
  • distinguishes them from native HTML elements

  • ensures new elements (HTML6+) will not have same name as any custom elements

Custom Elements

  • Use document.registerElement() to register a new element

var message = document.registerElement('my-message', {
    // Inherits from base HTMLElement
    prototype: Object.create(HTMLElement.prototype)
});

JavaScript

  • It may inherit from existing elements, e.g.

var message = document.registerElement('custom-button', {
    prototype: Object.create(HTMLButtonElement.prototype),
    extends: 'button'
});

JavaScript

  • This will create a <custom-button></custom-button> element for use in HTML

  • Will have all features of a <button> tag, which can be added to

Custom Elements

  • Initialize element, adding child nodes:

var message = new message();
document.body.appendChild(message);

JavaScript

  • Lifecycle methods:

  • createdCallback()

when an instance of the custom element is created

  • attachedCallback()

when the instance is attached to the DOM

  • detachedCallback()

when the instance is removed from the DOM

  • attributeChangedCallback()

when an attribute of the instance is added, updated or removed

WebComponents specification

HTML Imports

  • After creating new custom tag, make code reusable by writing definition inside a HTML file
  • Using HTML imports, can include this definition on all pages using custom tag
<html>
<head>
    <link rel="import" href="/templates/my-message.html">
</head>
<body>
    <my-message></my-message>
<body>
</html>

HTML

  • Usage:

HTML Imports

<script async>
    function handleLoad(e) {
        console.log('Loaded import: ' + e.target.href');
    }
    function handleError(e) {
        console.log('Error loading import: ' + e.target.href');
    }
</script>

<link rel="import" href="/templates/my-message.html"
      onload="handleLoad(event)" onerror="handleError(event)">

HTML

  • Might include some script to handle errors while importing:

WebComponent example project

WebComponents resources

The Jackal of Javascript (pretty exhaustive):

WebComponents.org (various resources):

Made with Slides.com