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
- Compatibility table at http://kangax.github.io/compat-table/es6/
- 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)
- Babel ES6 -> ES5 transpilation: https://babeljs.io/repl/
/*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):
Copy of Introduction to ECMAScript 6
By Jure Polutnik
Copy of Introduction to ECMAScript 6
- 1,014