Introduction to ECMAScript 6
Contents
- What is ECMAScript?
- Hands-on - transpailing, demo
ES6 ~ ES2015
- Classes, Inheritance
- Modules
- Iterators, Generators
- Scoping
- Promises, Proxies
- Arrow functions
- Collections
- ...
TypeScirpt
- Type Inference
- Type compatibility
- Optional parameters
- Enums
- Interfaces
- Classes
- Mixing, Mergins
- Decleration files
- ...
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 is a type of compiler that takes the source code of a program written in one programming language as its input and produces the equivalent source code in another programming language.
- 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
/*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
/*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
- TypeScript -> ES5 transpilation
// Todo Model
// ----------
// Our basic **Todo** model has `content`, `order`, and `done` attributes.
class Todo extends Backbone.Model {
// Default attributes for the todo.
defaults() {
return {
content: "empty todo...",
done: false
}
}
// Ensure that each todo created has `content`.
initialize() {
if (!this.get("content")) {
this.set({ "content": this.defaults().content });
}
}
// Toggle the `done` state of this todo item.
toggle() {
this.save({ done: !this.get("done") });
}
// Remove this Todo from *localStorage* and delete its view.
clear() {
this.destroy();
}
}
// Todo Model
// ----------
// Our basic **Todo** model has `content`, `order`, and `done` attributes.
var Todo = (function (_super) {
__extends(Todo, _super);
function Todo() {
_super.apply(this, arguments);
}
// Default attributes for the todo.
Todo.prototype.defaults = function () {
return {
content: "empty todo...",
done: false
};
};
// Ensure that each todo created has `content`.
Todo.prototype.initialize = function () {
if (!this.get("content")) {
this.set({ "content": this.defaults().content });
}
};
// Toggle the `done` state of this todo item.
Todo.prototype.toggle = function () {
this.save({ done: !this.get("done") });
};
// Remove this Todo from *localStorage* and delete its view.
Todo.prototype.clear = function () {
this.destroy();
};
return Todo;
}(Backbone.Model));
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
this
var foo = {};
foo.someMethod = function(){
alert(this);
}
var foo = function(){
alert(this);
}
foo();
foo.someOtherMethod = function (){
var that=this;
function bar(){
alert(that);
}
}
function Foo(){
this.confusing = 'hell yeah';
}
var myObject = new Foo();
var foo = {};
foo.someMethod = function(){ console.log(this.toString()); }
foo.someMethod() // [object Object]
foo.someMethod.apply (foo, []) // [object Object]
foo.someMethod.apply ("foo", []) // foo
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
- 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 (extend)
- 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 (super)
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
-
ES6 brings formal standardization
- Currently: AMD, CommonJS
- Before that: None (global)
- In ECMAScript 6, modules are stored in files.
- one module per file
- one file per module
Modules (current)
- CommonJS
- AMD
// someModule.js
function doSomething () {
console.log("foo");
}
exports.doSomething = doSomething;
//otherModule.js
var someModule = require('someModule');
someModule.doSomething();
// someModule.js
define(["lib"], function (lib) {
function doSomething() {
console.log("foo");
}
return {
doSomething: doSomething
}
});
//otherModule.js
require(["someModule"], function(someModule){
someModule.doSomething();
});
- Compact syntax
- Synchronous loading
- Server side (Node.js)
- Asynchronous loading
- No compilation step
- Browser (RequireJS)
Modules (ES6)
- Similar to CommonJS
- compact syntax
- preference for single exports
- support for cyclic dependencies
- Similar to AMD
- direct support for asynchronous loading
- configurable module loading
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 exports
//------ MyClass.js ------
class MyClass { ... };
export default MyClass;
//------MyFunction.js ----
function myFunction() { ... };
export default myFunction;
//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}
Named exports
// ----- main.js ---
import * as lib from './lib';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5
// ----- main.js ---
import MyClass from './MyClass'
import thatFunction from './myFunction'
var my = new MyClass();
var res = thatFunction();
Modules
- Default exports
- Only one per module
- Can rename on import
// ClientService.js
class ClientService {
clients = [];
addClient(client) {
this.clients.add(client);
}
}
export default ClientService;
ES6
import ClientService from './ClientService';
ES6
import MyService from './ClientService';
class ClientComponent {
submitClient(client) {
MyService.addClient(client);
}
}
- Difference in CommonJS / ES6 syntax
module.exports = ClientService;
CommonJS
export default ClientService;
ES6
var Service = require('./ClientService');
import Service from ('./ClientService');
- CommonJS and ES6 modules are interoperable!
Modules (ES6 vs CommonJS)
function foo() {
module.exports = 'bar'; // OK
}
CommonJS
Modules (ES6 vs CommonJS)
- Exports in ES6 must be top-level
function foo() {
export default 'bar'; // Syntax error
}
ES6
// Constants.js
var $BANKRUPTCY_MIN_DEBT = 1001;
var $SEQUESTRATION_MIN_DEBT = 3000;
export $BANKRUPTCY_MIN_DEBT;
export $SEQUESTRATION_MIN_DEBT;
ES6
Modules (named exports)
// 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
- many exports per module
- can mix with default export
- Importing named exports:
Modules (named exports)
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() {
var valIncomeSupport = incomes.incomeSupport;
var valJobseekers = incomes.jobseekers ;
var valIncapacity = incomes.incapacity ;
var valTotalBenefitIncome = incomes.totalBenefitIncome();
}
// IncomesController.js
export var incomeSupport;
export var jobSeekers;
export var incapacity;
export var totalBenefitIncome = function() {
incomeSupport +
jobSeekers +
incapacity;
}
- 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:
//------ lib.js ------
export let mutableValue = 3;
export function incMutableValue() {
mutableValue++;
}
//------ main.js ------
import * as lib from './lib';
// The imported value is live
console.log(lib.mutableValue); // 3
lib.incMutableValue();
console.log(lib.mutableValue); // 4
// The imported value can’t be changed
lib.mutableValue++; // TypeError
ES6
ES6
- Why bindings? Easier to deal with cyclic dependencies
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
- Use cases:
- 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
- Use cases:
- 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]();
> 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
-
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.
var iterable = {
next: function(){
returns {value: 1, done: true}
}
}
var foo = { [Symbol.iterator]: iterable }
foo[Symbol.iterator] = iterable
iterable.next() // returns {value: 1, done: true}
Iterators
- Example:
[Symbol.iterator]: () => ({
items: ['f', 'o', 'o'],
next: function () {
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
New features in ES6
'let' and 'const'
- Alternatives to declaring a variable with 'var'
- 'var'
- function-scoping
- hoisting
- 'let'
- block-scoping
- hositing w/ temporal dead zones
'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
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
if (x) {
let foo;
let foo; // TypeError thrown.
}
function do_something() {
console.log(foo); // ReferenceError
let foo = 2;
}
'let' and 'const'
- Redeclaration not allowed
- 'let' is also hoisted
- Temporal dead-zone
for (let i = 0; i<10; i++) {
console.log(i); // 0, 1, 2, ... 9
}
console.log(i); // i is not defined
- 'let' in loops
'let' and 'const'
- const:
- must be decalred using an initializer
- also block-scoped
const IP = 3.14
const STUFF = { things: ['chair', 'desk', 'computer'] };
const FOO; // SyntaxError: missing = in const declaration
e.g.
- will fail silently if assigned again
const stuff = { things: ['chair', 'desk'] };
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'
- Useful for anonymous functions with few arguments and statements
- '=>' replaces 'function'
function(param1, param2, ..., paramN) { statements }
// Equals to
(param1, param2, …, paramN) => { statements }
Arrow functions
- No parentheses required for 1 argument
- 'return' keyword implied for single expressions
function(a) { return a * a }
// Equals to
a => a * a
- () can be replaced with _
() => { statements }
// Equals to
_ => { statements }
(param1, param2, …, paramN) => { return expression; }
// Equals to
(param1, param2, …, paramN) => expression
Arrow functions (scope)
- 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 (scope)
- 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
- New in ES6 : Map, Set, WeakMap, WeakSet
- 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' });
Collection types
- Map
- Might want to use non-string values as keys
- e.g. DOM elements
- Problems with this approach:
- Only allows strings as keys
- Difficult to iterate through keys
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 });
Collection types
- Map
add(document.getElementById("wages-panel"), { display: true });
- <div> element is cast to string
- [Object HTMLDivElement]
- Using objects 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]);
Collection types
- Map
- Methods:
- get(), set(), has(), delete()
- entries(), keys(), values()
- size(), clear()
- forEach()
var items = [
[new Date(), function today() {}],
[() => 'key', { foo: 'bar' }],
[(Symbol('items'), [1, 2])
]
// 2D key-value Array to map
var map = new Map(items);
map.get(Symbol('items')) // returns [1,2]
- Relation with array
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
- Use case - Private class variables
Collection types
- No concept of private class variables in ES6
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;
}
}
- When myClass destroyed no references to key (this) exists
- privateData will be garbage collected
- 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);
- Storing the frequency of an answer on the DOM element
let childNumber = document.getElementById('child-number')
if(childNumber.number === 0) {
childBenefit.parentNode.removeChild(childBenefit)
}
console.log(answerMap.get(childBenefit) // undefined
- If childBenefit key removed, entry is garbage collected
- 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
- Expression interpolation with ${expression}
var text = `the time and date is ${today.toLocaleString()}`
// 'the time and date is 1/1/2016, 09:20:00 AM'
ES6
Template literals
- Tagged template literals
- modify the output of template literals using a function
- must return string
var a = 5;
var b = 10;
function tag(strings, ...values) {
console.log(strings[0]); // "Hello "
console.log(strings[1]); // " world "
console.log(values[0]); // 15
console.log(values[1]); // 50
return "Bazinga!";
}
tag`Hello ${ a + b } world ${ a * b }`;
// "Bazinga!"
- 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 bar = 'bar';
var foo = { bar };
console.log(foo.bar); // 'bar'
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:
- Can compute property names using []
var bar = 'bar'
var baz = { [bar]: 'qux' }
console.log(baz)
// { bar: 'qux' }
ES6
var foo = { ['bar' + 'baz']: 'qux' }
console.log(foo)
// { barbaz: 'qux' }
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 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
- Define default options for a method
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
// Function calls
myFunction(...iterable);
// Array literals
[...iterable, 4, 5, 6]
// Destructing
[a, b, ...iterable] = [1, 2, 3, 4, 5];
- 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
Promises
- The Promise object is used for deferred and asynchronous computations. A Promise represents an operation that hasn't completed yet, but is expected in the future.
new Promise(executor);
new Promise(function(resolve, reject) { ... });
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
- Chaining
- Promise.prototype.then(onFulfiled, onRejected)
- Promise.all(iterable)
- Promise.race(iterable)
New features in ES6
Proxies
- The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).
var p = new Proxy(target, handler);
-
target - object or function to wrap with Proxy.
-
handler - an object whose properties are functions which define the behavior of the proxy when an operation is performed on it.
Proxies
var target = {}
var handler = {}
var proxy = new Proxy(target, handler)
proxy.a = 'b'
console.log(target.a) // <- 'b'
console.log(proxy.c === undefined) // <- true
- By default, proxies don’t do much – in fact they don’t do anything
- no-op forwarding proxy
Proxies
var handler = {
get (target, key) {
console.info(`Get on property "${key}"`)
return target[key]
}
}
var target = {}
var proxy = new Proxy(target, handler)
proxy.a = 'b'
proxy.a // <- 'Get on property "a"'
proxy.b // <- 'Get on property "b"'
- handler traps
- get (target, key)
- set (target, key)
- apply (target, f, args)
- construct (target, args)
Proxies (example)
var handler = {
get (target, key) {
invariant(key, 'get')
return target[key]
},
set (target, key, value) {
invariant(key, 'set')
return true
}
}
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`)
}
}
var target = {}
var proxy = new Proxy(target, handler)\
proxy.a = 'b'
console.log(proxy.a) // <- 'b'
proxy._prop // <- Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c' // <- Error: Invalid attempt to set private "_prop" property
- Access control - deny access to private properties (prefixed with '_')
Proxies (example)
var validator = {
set (target, key, value) {
if (key === 'age') {
if (typeof value !== 'number' || Number.isNaN(value)) {
throw new TypeError('Age must be a number')
}
if (value <= 0) {
throw new TypeError('Age must be a positive number')
}
}
return true
}
}
var person = { age: 27 }
var proxy = new Proxy(person, validator)
proxy.age = 'foo' // <- TypeError: Age must be a number
proxy.age = NaN // <- TypeError: Age must be a number
proxy.age = 0 // <- TypeError: Age must be a positive number
proxy.age = 28
console.log(person.age) // <- 28
- Validation - age validator
Proxies
var target = {}
var handler = {}
var {proxy, revoke} = Proxy.revocable(target, handler)
proxy.a = 'b'
console.log(proxy.a) // <- 'b'
revoke()
console.log(proxy.a) // <- TypeError: illegal operation attempted on a revoked proxy
- Revocable proxy
- after revoke() is called, proxy is not usable anymore
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
What is TypeScript?
TypeScript: features
- Variable type inferred from value
var x:number = 1
var n = 1 // var n:number = 1
console.log(typeof(n)) // number
var s = "Hello World" // var s:string = "Hello World"
console.log(typeof(s)) // string
n = s; // error
s = n; // error
function f() {
return "hello"
}
console.log(typeof(f.call())) // string
TypeScript
Type inference/annotation
- Unknown type?
- Might not know data type provided by user or 3rd party library
- use 'any' type to opt-out of type checking
Type inference/annotation
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.buton); // <- Error
};
window.onmousedown = function(mouseEvent: any) {
console.log(mouseEvent.buton); // <- OK
};
TypeScript
- Best common type
- When a type inference is made from several expressions it consider each candidate type and chooses one that is compatible with all the other candidates
var x = [0, 1, 2]; // number[]
var y = [0, '1', 2]; // any[]
var zoo = [new Rhino(), new Elephant(), new Snake()]; // Object[]
var zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()]; // Animal[]
Type inference/annotation
TypeScript
- Can express return type for functions
function add(num1, num2): number {
return num1 + num2;
}
function concat(str1, str2): string {
return str1 + str2;
}
- 'void' type
- absence of any type at all
- used as return type of functions with no return value
function warnUser(): void {
alert("This function doesn't return a type");
}
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 large code-bases with teams working on different parts of a program
Type inference/annotation
- Better toolings
TypeScript: features
- Parameters always optional in JavaScript functions
- TypeScript prohibits omitting typed arguments unless specified as optional ('?')
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
var result1 = buildName("Bob"); //works correctly now
var result2 = buildName("Bob", "Adams", "Sr."); //error, too many parameters
var result3 = buildName("Bob", "Adams"); //ah, just right
TypeScript
Optional parameters
Default parameters
- Optional parameter is essence a default parameter with value 'undefined'
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
var result1 = buildName("Bob"); //works correctly now, also
var result2 = buildName("Bob", "Adams", "Sr."); //error, too many parameters
var result3 = buildName("Bob", "Adams"); //ah, just right
function buildName(firstName: string, lastName = undefined) {
return firstName + " " + lastName;
}
TypeScript: features
- New datatype
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
enum Polygon { Triangle=3, Square=4, Octagon=8 }
var status = Status.Ready;
status = Color.Green; //error
var color = Color.Blue; // 1
var colorName = Color[1]; // 'Blue'
var edges = Polygon.Octagon // 8
TypeScript
- Enums are compatible with numbers, and numbers are compatible with enums
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);
- 'labelledObj' must satisfy requirement defined in interface (has string 'label' property) - duck-typing
- can add other properties as long as required ones exist
Interfaces
- optional properties ('?')
interface ILabelledValue {
label: string;
size?: number;
}
Interfaces
function printLabel(labelledObj: ILabelledValue) {
console.log(labelledObj.label)
}
var myObj = { count: 10, label: "Size 10 object" };
printLabel(myObj); // ok
var myObj = { size: 10, lbl: "Size 10 object" };
printLabel(myObj); // error - myObj does not satisfy LabelledValue
- extending other interfaces
Interfaces
interface IRectangle {
x: number;
y: number;
}
interface ICuboid extends IRectangle {
z: number;
}
- can extend multiple interfaces
interface IColor {
r: number;
g: number;
b: number;
}
interface IColoredCuboid extends ICuboid, IColor{
alpha: number;
}
interface ISearchFunc {
(source: string, subString: string): boolean;
}
- Function Types
- param types need to match
var mySearch: ISearchFunc;
mySearch = function(src: string, sub: string) {
var result = src.search(sub);
if (result == -1) {
return false;
}
else {
return true;
}
}
Interfaces
- function 'signature'
interface IStringArray {
[index: number]: string;
}
var myArray: IStringArray;
myArray = ["John", "Doe"];
- Array Types
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) { }
}
- Class Types
Interfaces
property
method
- an interface can't have a constructor signature
- constructor is a static method (class method)
- interfaces are checked against instances
- 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
- Similar to ES6
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
var greeter = new Greeter("world");
Classes
- Inheritance
class Animal {
name:string;
constructor(theName: string) { this.name = theName; }
move(meters: number = 0) {
alert(this.name + " moved " + meters + "m.");
}
}
class Horse extends Animal {
constructor(name: string) { super(name); }
move(meters = 45) {
alert("Galloping...");
super.move(meters);
}
}
var tom = new Horse("Tommy the Palomino");
tom.move(34);
Classes
- Private/Public modifiers
class Animal {
private name:string;
constructor(theName: string) { this.name = theName; }
}
class Cow extends Animal {
constructor() { super("Cow"); }
}
var horse = new Animal("horse");
var cow = new Cow();
horse.name // error
cow.name // error
Classes
- Static properties / functions
class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
var xDist = (point.x - Grid.origin.x);
var yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor (public scale: number) { }
}
var grid1 = new Grid(1.0); // 1x scale
var grid2 = new Grid(5.0); // 5x scale
alert(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
alert(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
Classes
TypeScript: features
Generics
- Allow for creation of components that can work with a variety of types
function echo(arg: any): any {
return arg;
}
- 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;
}
- 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
}
- 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
}
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
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 };
TypeScript: features
Type Compatibility
- based on structural typing
- a way of relating types based solely on their members.
interface Named {
name: string;
}
class Person {
name: string;
}
var p: Named;
// OK, because of structural typing
p = new Person();
Type Compatibility
- Classes
- When comparing two objects of a class type only members of the instance are compared.
- Static members and constructors do not affect compatibility.
class Animal {
feet: number;
constructor(name: string, numFeet: number) { }
}
class Size {
feet: number;
constructor(numFeet: number) { }
}
var a: Animal;
var s: Size;
a = s; //OK
s = a; //OK
Type Compatibility
class Mammal {
private name: string;
constructor(_name: string) {
this.name = _name;
}
}
class Reptile {
private name: string;
constructor(_name: string) {
this.name = _name;
}
}
var cat = new Mammal('Cat');
var lizard = new Reptile('Lizard');
cat = lizard; // error: Animal and Reptile are not compatible
- Classes
- private members - equivalent only when come from same declaration
Type Compatibility
class Mammal {
private name: string;
constructor(_name: string) {
this.name = _name;
}
}
class Dog extends Mammal {
constructor() {
super("Dog");
}
}
var cat = new Mammal('Cat');
var dog = new Dog();
cat = dog; // OK
- Classes
- private members - equivalent only when come from same declaration
Type Compatibility
var x = (a: number) => 0;
var y = (b: number, s: string) => 0;
var z = (s: string) => 0;
y = x; // OK - discarding s: string
x = y; // Error
y = z; // Error
x = z; // Error
z = x; // Error
- Functions
- checks corresponding compatible parameters
- ‘discarding’ parameters is allowed - it is quite common in JS
Type Compatibility
interface Empty<T> {
}
var x: Empty<number>;
var y: Empty<string>;
x = y; // okay, y matches structure of x
- Generics
- type parameters only affect the resulting type
interface NotEmpty<T> {
data: T;
}
var x: NotEmpty<number>;
var y: NotEmpty<string>;
x = y; // error, x and y are not compatible
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
class StringOperations implements Join, Split {
prefix(word) {
return "START" + word;
}
// Join
join: (string[]) => string;
// Split
split: (string) => string;
}
applyMixins(StringOperations, [Join, Split]);
- 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
//
// Runtime library
//
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
})
});
}
TypeScript: features
Declaration merging
- Typescript merges any number of declarations with same name into a single defintion
- Merging interfaces and modules with classes, functions and Enums
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;
}
End
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):
Introduction to ECMAScript 6
By AdapTeach
Introduction to ECMAScript 6
- 1,461