Intermediate JavaScript
Andrew Smith
[email protected]
@andrewsouthpaw
https://andrewsouthpaw.com
Overview
Introduction to This Course Overview Andrew Smith 2 / 712
What’s In Store - Day 1
1. JavaScript Language
2. Functional Programming - Part 1
3. Document Object Model
4. Browser APIs
5. Asynchronous Programming
Introduction to This Course Overview Andrew Smith 3 / 712
What’s In Store - Day 2
1. Forms
2. Object Oriented Programming (Classical)
3. Object Oriented Programming (Prototypal)
4. Functional Programming - Part 2
5. Managing UI
Introduction to This Course Overview Andrew Smith 4 / 712
What’s In Store - Day 3
1. JavaScript Tooling
2. Testing with Jest
3. Debugging
4. Intro to Web Security
Introduction to This Course Overview Andrew Smith 5 / 712
Modern JS Syntax
JavaScript Language Modern JS Syntax Andrew Smith 6 / 712
ECMAScript 2015
JavaScript Language ECMAScript 2015 Andrew Smith 7 / 712
ES2015 Summary
• New keywords: let, const, class, import, export, etc.
• Arrow functions () => {}
• New function parameters syntax
• Destructuring
• Data structures
• Lots more
JavaScript Language ECMAScript 2015 Andrew Smith 8 / 712
const Keyword
Block-level variable that can’t be reassigned.
const x = 42
x = 1 // TypeError: Assignment to constant variable
JavaScript Language ECMAScript 2015 Andrew Smith 9 / 712
const Does Not Prevent Mutation
Not protected against mutation.
const x = { message: 'Hello' }
x.message = 'Goodbye' // OK
x = { message: 'Invalid' } // Error
const xs = []
xs.push(1) // OK
xs = [] // Error
JavaScript Language ECMAScript 2015 Andrew Smith 10 / 712
let Keyword
Block-level variable that can be reassigned.
let x = 42
x = 1 // OK
let x = 5 // OK
JavaScript Language ECMAScript 2015 Andrew Smith 11 / 712
Looping with let
Always use let for a for loop.
// Good times
for (let i = 0; i < 3; i++) {}
// Bad times
for (var i = 0; i < 3; i++) {}
Just trust me on it.
JavaScript Language ECMAScript 2015 Andrew Smith 12 / 712
The Holy War: const vs let
• It doesn’t really matter; cohesion over “correctness”
• Personal style:
• let for variables that get reassigned
• const for everything else
JavaScript Language ECMAScript 2015 Andrew Smith 13 / 712
Object Function Shorthand
// Before
const person = {
sayHello: function () {
console.log('Hello')
},
}
// After
const person = {
sayHello() {
console.log('Hello')
},
}
JavaScript Language ECMAScript 2015 Andrew Smith 14 / 712
Object Property Shorthand
// Before
const name = 'Andrew'
const person = { name: name }
// After
const person = { name }
JavaScript Language ECMAScript 2015 Andrew Smith 15 / 712
Trailing Commas
const arr = [
1,
2, // trailing comma
]
Objects OK in ES5:
const person = {
name: 'Andrew', // NEW trailing comma
}
JavaScript Language ECMAScript 2015 Andrew Smith 16 / 712
Arrow Functions
// Before
items.map(function (item) {
// ...
})
// After
items.map((item) => {
// ...
})
JavaScript Language ECMAScript 2015 Andrew Smith 17 / 712
Arrow Functions
Gets rid of unexpected hoisting.
foo() // OK ?!
function foo(a, b) {
// ...
}
bar() // Error
const bar = (a, b) => {
// ...
}
JavaScript Language ECMAScript 2015 Andrew Smith 18 / 712
Implicit return for Arrow Expressions
Omit curly braces to implicitly return value of a single expression.
// Before
function addOne(x) {
return x + 1
}
// After
const addOne = x => x + 1
JavaScript Language ECMAScript 2015 Andrew Smith 19 / 712
Default Parameters
Parameters can have default values
const add = (a, b = 2) => a + b
add(1, 3) // 4
add(1) // 3
When argument is undefined, it gets the default value
add(1, undefined) // 3
add(1, null) // 1
JavaScript Language ECMAScript 2015 Andrew Smith 20 / 712
Default Parameters
Default parameter is evaluated at call time
let id = 1
const nextId = () => id++
const print = (id = nextId()) => {
console.log(id)
}
print() // 1
print() // 2
print(42) // 42
print() // 3
JavaScript Language ECMAScript 2015 Andrew Smith 21 / 712
Rest Parameters
Argument name with ... gathers up remaining arguments.
const remainder = (first, ...args) => args.length
remainder(1) // 0
remainder(1, 2) // 1
remainder(1, 2, 3, 4) // 3
You can stop using arguments, it leads to sadness.
The rest argument must be last:
const foo = (...rest, last) => {} // SyntaxError
JavaScript Language ECMAScript 2015 Andrew Smith 22 / 712
Array Spread
Add ... to an array to “spread out” its arguments.
Sort of like taking each element and writing it out comma-separated
...[1, 2] // generates: 1, 2
JavaScript Language ECMAScript 2015 Andrew Smith 23 / 712
Array Spread
Spread an array to be arguments in a function:
const add = (a, b) => a + b
const nums = [1, 2]
add(nums) // bad times: '1,2undefined'
add(...nums) // 3
Spread an array into another array:
const arr1 = [1, 2]
const arr2 = [3, 4]
const combined = [...arr2, ...arr1] // [3, 4, 1, 2]
JavaScript Language ECMAScript 2015 Andrew Smith 24 / 712
Array Copying
Use spread to immutably copy an array:
const arr1 = [1, 2]
const arr2 = [...arr1]
arr1 // [1, 2]
arr2 // [1, 2]
arr1 === arr2 // false
JavaScript Language ECMAScript 2015 Andrew Smith 25 / 712
Array Destructuring
Pull out positional values into named variables:
const nums = [1, 2, 3, 4]
const [first, second, ...rest] = nums
console.log(first) // 1
console.log(second) // 2
console.log(rest) // [3, 4]
JavaScript Language ECMAScript 2015 Andrew Smith 26 / 712
Array Destructuring
Can destructure in function parameters too!
const tail = ([head, ...rest]) => rest
const nums = [1, 2, 3, 4]
tail(nums) // [2, 3, 4]
JavaScript Language ECMAScript 2015 Andrew Smith 27 / 712
Classes
JS inheritance with the class keyword
class Person {
constructor(name) {
this.name = name
}
getName() {
return `My name is ${this.name}`
}
}
const person = new Person('Andrew')
person.getName() // 'My name is Andrew'
More discussion on inheritance later.
JavaScript Language ECMAScript 2015 Andrew Smith 28 / 712
Copying Objects Properties
const o1 = { a: 1, b: 2 }
const o2 = {}
Object.assign(o2, o1) // MUTATES
console.log(o2) // { a: 1, b: 2 }
We’ll learn a better way to copy objects later. . .
JavaScript Language ECMAScript 2015 Andrew Smith 29 / 712
ES Modules (Export)
Export named functions, objects, and values
export const foo = ...
// also works, less popular
const bar = ...
export { bar }
Modules have a “default” export as well:
export default 42
JavaScript Language ECMAScript 2015 Andrew Smith 30 / 712
ES Modules (Import)
Import named exports:
import { foo, bar } from './module'
Group all named exports into a single object:
import * as module from './module'
module.foo
Default import:
import magicNumber from './module'
magicNumber // 42
JavaScript Language ECMAScript 2015 Andrew Smith 31 / 712
ES Modules Tips
• Use inline export statements
• Destructured imports only pull in what’s necessary
• Avoid default exports
• They make renames painful
JavaScript Language ECMAScript 2015 Andrew Smith 32 / 712
Object Destructuring
Can destructure an Object, just like an Array
const obj = { a: 1, b: 2, c: 3, d: 4 }
const { a, b } = obj
console.log(a) // 1
console.log(b) // 2
JavaScript Language ECMAScript 2015 Andrew Smith 33 / 712
Object Destructuring
You can destructure function parameters too!
const getId = ({ id }) => id
const person = { id: 42, name: 'Andrew' }
getId(person) // 42
JavaScript Language ECMAScript 2015 Andrew Smith 34 / 712
Object Destructuring
Destructuring function parameters are based on their position.
const getName = ({ name }, { upcase } = {}) => (
upcase ? name.toUpperCase() : name
)
const person = { id: 42, name: 'Andrew' }
getName(person) // 'Andrew'
getName(person, { upcase: true }) // 'ANDREW'
JavaScript Language ECMAScript 2015 Andrew Smith 35 / 712
Object Destructure Renaming
You can rename the destructured key to something else:
const myObj = { foo: 42 }
const { foo: bar } = myObj
console.log(bar) // 42
JavaScript Language ECMAScript 2015 Andrew Smith 36 / 712
Object Rest Property
Collect the remaining props onto a separate object:
const obj = { a: 1, b: 2, c: 3, d: 4 }
const { a, b, ...rest } = obj
console.log(a) // 1
console.log(b) // 2
console.log(rest) // { c: 3, d: 4 }
JavaScript Language ECMAScript 2015 Andrew Smith 37 / 712
Object Spreading
Copying an object into another object:
const z = { c: 3 }
const x = { a: 1, b: 2, ...z }
console.log(x) // { a: 1, b: 2, c: 3 }
JavaScript Language ECMAScript 2015 Andrew Smith 38 / 712
Object Copying
Use spread to immutably copy an object:
const obj1 = { foo: 'bar' }
const obj2 = { ...obj1 }
obj1 // { foo: 'bar' }
obj2 // { foo: 'bar' }
obj1 === obj2 // false
JavaScript Language ECMAScript 2015 Andrew Smith 39 / 712
Object Spreading
Spreading performs a shallow copy, so objects and arrays are copied by
reference:
const z = { a: 1, b: [] }
const copy = { ...z }
copy.a = 42
copy.b.push('foo')
z.a // ?
z.b // ?
JavaScript Language ECMAScript 2015 Andrew Smith 40 / 712
Object Spreading
Spreading performs a shallow copy, so objects and arrays are copied by
reference:
const z = { a: 1, b: [] }
const copy = { ...z }
copy.a = 42
copy.b.push('foo')
z.a // 1
z.b // ['foo']
JavaScript Language ECMAScript 2015 Andrew Smith 41 / 712
Exercise
1. Open src/www/js/es-syntax/syntax.test.js
2. Follow directions in the it statements
3. Go through each test and follow instructions
$ cd src
$ yarn jest syntax.test.js --watch
JavaScript Language ECMAScript 2015 Andrew Smith 42 / 712
this
JavaScript Language this Andrew Smith 43 / 712
What’s this anyway
this is (generally) one of two things:
• the global object (window for browsers)
• a function execution’s context
JavaScript Language this Andrew Smith 44 / 712
Global Object
// In a browser, outside any function
console.log(this === window) // true
a = 37
console.log(window.a) // 37
this.b = 'Hello'
console.log(window.b) // 'Hello'
console.log(b) // 'Hello'
JavaScript Language this Andrew Smith 45 / 712
Execution context
What comes to the left of a function’s invocation?
• obj.foo() <– obj is the context
• foo() <– nothing, so it’s the global object
const obj = {
foo() { return this }
}
obj.foo() === obj // true
const bar = function() { return this }
bar() === window // true
JavaScript Language this Andrew Smith 46 / 712
this is not loyal
this inside a function is not loyal to where it was written.
const obj = {
foo() { return this }
}
const myFoo = obj.foo
obj.foo() === obj // ?
myFoo() === obj // ?
JavaScript Language this Andrew Smith 47 / 712
this is not loyal
this inside a function is not loyal to where it was written.
const obj = {
foo() { return this }
}
const myFoo = obj.foo
obj.foo() === obj // true
myFoo() === obj // false
When you refer to a function without (), the context is given up.
JavaScript Language this Andrew Smith 48 / 712
this is not loyal
That means the functionality can be shared among different objects.
const getName = function() {
return this.name
}
const person = { name: 'Andrew', getName }
person.getName() // 'Andrew'
const pet = { name: 'Fido', givenName: getName }
person.givenName() // 'Fido'
JavaScript Language this Andrew Smith 49 / 712
Forcing this to be loyal
You can bind the context for a function:
const obj = {
foo() { return this }
}
const myFoo = obj.foo.bind(obj)
obj.foo() === obj // true
myFoo() === obj // true
JavaScript Language this Andrew Smith 50 / 712
A Function Changes The Context
const person = {
name: 'Andrew',
delayedSayName() {
setTimeout(
function() { // new context!
console.log(this.name)
},
1000
)
}
}
person.delayedSayName() // ... nothing
JavaScript Language this Andrew Smith 51 / 712
Arrow Functions Preserve Lexical Context
Arrow functions () => {} preserve prior context
const person = {
name: 'Andrew',
delayedSayName() {
setTimeout(
() => { // not new context!
console.log(this.name)
},
1000,
)
},
}
person.delayedSayName() // ... Andrew
JavaScript Language this Andrew Smith 52 / 712
Arrow Functions Preserve Lexical Context
That means they don’t jive nicely on objects.
const getName = () => this.name
const person = {
name: 'Andrew',
getName: getName,
}
person.getName() // ?
JavaScript Language this Andrew Smith 53 / 712
Arrow Functions Preserve Lexical Context
That means they don’t jive nicely on objects.
const getName = () => this.name
const person = {
name: 'Andrew',
getName: getName,
}
person.getName() // undefined >:-(
JavaScript Language this Andrew Smith 54 / 712
Arrow Functions Preserve Lexical Context
This also doesn’t work:
const person = {
name: 'Andrew',
getName: () => this.name,
}
person.getName() // undefined >:-(
JavaScript Language this Andrew Smith 55 / 712
Object Methods
Stick with object function shorthand when using this:
const person = {
name: 'Andrew',
getName() { return this.name },
}
JavaScript Language this Andrew Smith 56 / 712
Instance Fields to the Rescue
class Person {
constructor(name) {
this.name = name
}
getName = () => this.name
}
const person = new Person('Andrew')
console.log(person.getName())
Enabled via @babel/plugin-proposal-class-properties
JavaScript Language this Andrew Smith 57 / 712
Object Methods
Handy for frameworks like React:
class Person extends React.Component {
getName() {
console.log(this.props.name) // error
}
render() {
return (
<button onClick={this.getName}>
Say Name
</button>
)
}
}
JavaScript Language this Andrew Smith 58 / 712
Object Methods
Handy for frameworks like React:
class Person extends React.Component {
getName = () => {
console.log(this.props.name) // 'Andrew'
}
render() {
return (
<button onClick={this.getName}>
Say Name
</button>
)
}
}
JavaScript Language this Andrew Smith 59 / 712
Exercise
1. Open src/www/js/this/this.test.js
2. Follow directions in the it statements
3. All tests should keep passing
$ cd src
$ yarn jest this.test.js --watch
JavaScript Language this Andrew Smith 60 / 712
Variable Scope
JavaScript Language Variable Scope Andrew Smith 61 / 712
Variable Scope
• Scope: where can we access a variable?
• Two types of scope: global and local
• If you don’t use const/let/function/var, then variables are global
• When you set/get global variables, explicitly use window
JavaScript Language Variable Scope Andrew Smith 62 / 712
Function Scopes
• Functions create separate scopes
• Variables defined inside a scope inaccessible from outside
JavaScript Language Variable Scope Andrew Smith 63 / 712
Function Scopes
function foo() {
const a = 1
console.log(a)
}
function bar() {
console.log(a) // ?
}
JavaScript Language Variable Scope Andrew Smith 64 / 712
Function Scopes
function foo() {
const a = 1
console.log(a)
}
function bar() {
console.log(a) // ReferenceError: a not defined
}
JavaScript Language Variable Scope Andrew Smith 65 / 712
Nested Scopes
function outerFunction() {
const outer = 'Outer variable'
function innerFunction() {
const inner = 'Inner variable'
console.log(outer) // ?
}
console.log(inner) // ?
}
console.log(outer) // ?
JavaScript Language Variable Scope Andrew Smith 66 / 712
Nested Scopes
https://css-tricks.com/javascript-scope-closures/
JavaScript Language Variable Scope Andrew Smith 67 / 712
Lexical Scoping
Lexical Scoping: scope is based on position in code.
Inner scopes include scope of parent scopes.
JavaScript Language Variable Scope Andrew Smith 68 / 712
Nested Scopes
function outerFunction() {
const outer = 'Outer variable'
function innerFunction() {
const inner = 'Inner variable'
console.log(outer) // ?
}
console.log(inner) // ?
}
console.log(outer) // ?
JavaScript Language Variable Scope Andrew Smith 69 / 712
Nested Scopes
function outerFunction() {
const outer = 'Outer variable'
function innerFunction() {
const inner = 'Inner variable'
console.log(outer) // Outer variable
}
console.log(inner) // ReferenceError
}
console.log(outer) // ReferenceError
JavaScript Language Variable Scope Andrew Smith 70 / 712
Shadowing
Variables in inner scopes temporarily override
or shadow variables by the same name in outer scopes
JavaScript Language Variable Scope Andrew Smith 71 / 712
Shadowing
function foo() {
const a = 10
function inner() {
const a = 2
console.log(a) // ?
}
console.log(a) // ?
}
JavaScript Language Variable Scope Andrew Smith 72 / 712
Shadowing
function foo() {
const a = 10
function inner() {
const a = 2
console.log(a) // 2
}
console.log(a) // 10
}
JavaScript Language Variable Scope Andrew Smith 73 / 712
Challenge Exercise
d = 15
function foo(b) {
e = 42
function myFn(f) {
const c = 2
return f + c + b
}
console.log(c) // ?
return myFn
}
console.log(d) // ?
console.log(e) // ?
console.log(foo(2)(4)) // ?
JavaScript Language Variable Scope Andrew Smith 74 / 712
Challenge Exercise
d = 15
function foo(b) {
e = 42
function myFn(f) {
const c = 2
return f + c + b
}
console.log(c) // ReferenceError
return myFn
}
console.log(d) // 15
console.log(e) // 42
console.log(foo(2)(4)) // 8
JavaScript Language Variable Scope Andrew Smith 75 / 712
Challenge Exercise 2
d = 15
function foo() {
const c = 10
return d === c ? 'error' : 'yay'
}
console.log(foo()) // ?
d = 10
console.log(foo()) // ?
JavaScript Language Variable Scope Andrew Smith 76 / 712
Challenge Exercise 2
d = 15
function foo() {
const c = 10
return d === c ? 'error' : 'yay'
}
console.log(foo()) // 'yay'
d = 10
console.log(foo()) // 'error'
JavaScript Language Variable Scope Andrew Smith 77 / 712
Block Scope
const and let variables have block scoping.
This basically means any { ... } defines a new scope.
function blockScope() {
if (Math.random() > 0.5) {
const a = 1
var b = 2
}
console.log(a) // ?
console.log(b) // ?
}
JavaScript Language Variable Scope Andrew Smith 78 / 712
Block Scope
const and let variables have block scoping.
This basically means any { ... } defines a new scope.
function blockScope() {
if (Math.random() > 0.5) {
const a = 1
var b = 2
}
console.log(a) // ReferenceError
console.log(b) // It depends...
}
JavaScript Language Variable Scope Andrew Smith 79 / 712
Block Scope
You can even use just a plain { } to create a scope namespace.
function blockScope() {
{
const a = 1
}
console.log(a) // ?
}
JavaScript Language Variable Scope Andrew Smith 80 / 712
Block Scope
You can even use just a plain { } to create a scope namespace.
function blockScope() {
{
const a = 1
}
console.log(a) // ReferenceError
}
JavaScript Language Variable Scope Andrew Smith 81 / 712
Top-Down Code
Does this work?
function first() {
second()
}
function second() {
console.log('Hello')
}
second() // ?
JavaScript Language Variable Scope Andrew Smith 82 / 712
Top-Down Code
Does this work?
function first() {
second()
}
function second() {
console.log('Hello')
}
second() // 'Hello'
JavaScript Language Variable Scope Andrew Smith 83 / 712
Top-Down Code
How about this?
const first = () => {
second()
}
const second = () => {
console.log('Hello')
}
second() // ?
JavaScript Language Variable Scope Andrew Smith 84 / 712
Top-Down Code
How about this?
const first = () => {
return second()
}
const second = () => {
return 'Hello'
}
second() // 'Hello'
JavaScript Language Variable Scope Andrew Smith 85 / 712
What’s Your Function
Functional Programming What’s Your Function Andrew Smith 86 / 712
Defining a Function
Function statement
function foo() {}
Function expression
const foo = function() {}
Named function expression
const foo = function myFooFn() {}
Arrow function (MOST COMMON)
const foo = () => {}
Functional Programming What’s Your Function Andrew Smith 87 / 712
Function Invocation
Parentheses are mandatory in JavaScript for function invocation.
Otherwise, you’re looking at the function.
const foo = (a, b, c) => {
console.log(a, b, c)
}
foo(1, 2, 3) // 1, 2, 3
foo // [Function: foo]
Functional Programming What’s Your Function Andrew Smith 88 / 712
Function Invocation
Extra arguments won’t be bound to a name
Missing arguments will be undefined
const foo = (a, b, c) => {
console.log(a, b, c)
}
foo(1, 2, 3, 4, 5) // 1, 2, 3
foo(1) // 1, undefined, undefined
Functional Programming What’s Your Function Andrew Smith 89 / 712
Function Arity
A function’s arity is the number of arguments it expects.
Accessed via length property.
const foo = (x, y, z) => { /* ... */ }
foo.length // ?
Functional Programming What’s Your Function Andrew Smith 90 / 712
Function Arity
A function’s arity is the number of arguments it expects.
Accessed via length property.
const foo = (x, y, z) => { /* ... */ }
foo.length // 3
Functional Programming What’s Your Function Andrew Smith 91 / 712
Function Arity
Optional arguments aren’t considered “expected”
const foo = (x, y, z = 42) => { /* ... */ }
foo.length // ?
Functional Programming What’s Your Function Andrew Smith 92 / 712
Function Arity
Optional arguments aren’t considered “expected”
const foo = (x, y, z = 42) => { /* ... */ }
foo.length // 2
Functional Programming What’s Your Function Andrew Smith 93 / 712
Function Arity
Rest arguments also aren’t considered “expected”
const foo = (x, y, ...remaining) => { /* ... */ }
foo.length // ?
Functional Programming What’s Your Function Andrew Smith 94 / 712
Function Arity
Rest arguments also aren’t considered “expected”
const foo = (x, y, ...remaining) => { /* ... */ }
foo.length // 2
Functional Programming What’s Your Function Andrew Smith 95 / 712
Higher-Order Functions
Functional Programming Higher-Order Functions Andrew Smith 96 / 712
Functions as Data
Functions are values and can be passed around.
const add = (a, b) => a + b
const otherAdd = add
otherAdd(1, 2) // 3
Functional Programming Higher-Order Functions Andrew Smith 97 / 712
Functions as Data
Functions can be passed into other functions
const repeatThreeTimes = (action) => {
action()
action()
action()
}
const sayHello = () => { console.log('Hello') }
repeatThreeTimes(sayHello)
// 'Hello' 'Hello' 'Hello'
Functional Programming Higher-Order Functions Andrew Smith 98 / 712
Higher-Order Functions
repeatThreeTimes is an example of a higher-order function.
Functions that take a function as an argument are called higher-order
functions.
Functional Programming Higher-Order Functions Andrew Smith 99 / 712
Themes
Common themes of higher-order functions:
1. Use the callback in a predefined way
2. Inject data
Functional Programming Higher-Order Functions Andrew Smith 100 / 712
Higher-Order Functions: Behavior
Another example of using a callback in a predefined way:
when: if the predicate is true, run the callback.
const when = (pred, cb) => (
pred ? cb() : null
)
const user = {/* ... */ }
const registration = when(
isNotRegistered(user),
() => registerUser(user),
)
Functional Programming Higher-Order Functions Andrew Smith 101 / 712
Higher-Order Functions: Inject Data
const withUser = (id, cb) => (
cb(getUser(id))
)
withUser(42, (user) => {
// do something with `user`...
})
Functional Programming Higher-Order Functions Andrew Smith 102 / 712
Higher-Order Functions
Can anyone name higher-order functions they’ve encountered?
Functional Programming Higher-Order Functions Andrew Smith 103 / 712
Functions Return Other Functions
Functions can even return other functions.
const always = (value) => {
return () => value
}
const getMagicNumber = always(42)
getMagicNumber() // 42
Functional Programming Higher-Order Functions Andrew Smith 104 / 712
Functions Return Other Functions
Functions that return functions are also higher-order functions
(This creates a closure which we’ll discuss later.)
Functional Programming Higher-Order Functions Andrew Smith 105 / 712
Higher-Order Functions
Log the arguments to a function:
const logArgs = (f) => (
(...args) => {
console.log('calling with', args)
const result = f(...args)
console.log('returned', result)
return result
}
)
const add = (a, b) => a + b
const loggedAdd = logArgs(add)
loggedAdd(3, 5)
Functional Programming Higher-Order Functions Andrew Smith 106 / 712
Eliminating Unnecessary Anonymous Functions
Some anonymous functions are redundant.
Rather than wrapping, pass the function directly.
const repeatThreeTimes = (action) => { /* ... */ }
const sayHello = () => { console.log('hello') }
repeatThreeTimes(() => { sayHello() }) // is the same as...
repeatThreeTimes(sayHello)
Functional Programming Higher-Order Functions Andrew Smith 107 / 712
Eliminating Unnecessary Anonymous Functions
Same goes for ones that receive arguments.
const invokeWith42 = (fn) => fn(42)
const add1ToNum = num => 1 + num
invokeWith42((num) => add1ToNum(num)) // is the same as...
invokeWith42(add1ToNum)
Functional Programming Higher-Order Functions Andrew Smith 108 / 712
Exercise
1. Open src/www/js/functional/higher-order.test.js
2. Follow directions in the it statements
3. All tests should keep passing
$ cd src
$ yarn jest higher-order.test.js --watch
Functional Programming Higher-Order Functions Andrew Smith 109 / 712
Higher-Order Array Functions
Functional Programming Higher-Order Array Functions Andrew Smith 110 / 712
Common Patterns of Array Processing
• Do something for each value
• Filter array values
• Query about each value
• Transform each value
• Reduce all values into a single value
Functional Programming Higher-Order Array Functions Andrew Smith 111 / 712
Do Something: #forEach
forEach on an array invokes the given callback with each element:
const printIncrementedArg = (arg) => {
console.log(arg + 1)
}
const arr = [1, 2]
arr.forEach(printIncrementedArg) // 2 3
Functional Programming Higher-Order Array Functions Andrew Smith 112 / 712
forEach
forEach
cb
source array
’a’ ’b’ ’c’ ’c’ cb(’a’) undefined
cb(’b’)
cb(’c’)
cb(’d’)
Functional Programming Higher-Order Array Functions Andrew Smith 113 / 712
forEach
Array#forEach invokes the given callback once for each array element
and passes in:
• the element
• the index
• the current array
['hello', 'goodbye'].forEach(console.log)
// ?
// ?
Functional Programming Higher-Order Array Functions Andrew Smith 114 / 712
forEach
Array#forEach invokes the given callback once for each array element
and passes in:
• the element
• the index
• the current array
['hello', 'goodbye'].forEach(console.log)
// hello 0 ['hello', 'goodbye']
// goodbye 1 ['hello', 'goodbye']
Functional Programming Higher-Order Array Functions Andrew Smith 115 / 712
A Humble Comparison
Which one reads more like English?
const registerUser = () => {/* ... */ }
const users = [/* ... */ ]
for (let i = 0; i < users.length; i++) {
registerUser(users[i])
}
// vs...
users.forEach(registerUser)
Functional Programming Higher-Order Array Functions Andrew Smith 116 / 712
An Invitation
You could never write another ‘for‘ loop again.
Functional Programming Higher-Order Array Functions Andrew Smith 117 / 712
Filter: #filter
[1, 2, 3, 4].filter(n => n > 2)
filter
n > 2
source array output array
1 2 3 4 1 > 2 3 4
2 > 2
3 > 2
4 > 2
Functional Programming Higher-Order Array Functions Andrew Smith 118 / 712
#filter
const nums = [1, 7, 3, 2, 4, 8, 6, 5]
const lessThan5 = x => x < 5
const filteredNums = nums.filter(lessThan5)
nums // [1, 7, 3, 2, 4, 8, 6, 5]
filteredNums // [1, 3, 2, 4]
Functional Programming Higher-Order Array Functions Andrew Smith 119 / 712
Query: #every, #some
#every - true if the function (predicate) returns true for all elements:
const nums = [1, 2, 3]
nums.every(num => num > 0) // true
nums.every(num => num > 2) // false
#some - true if function (predicate) returns truefor any element:
const nums = [1, 2, 3]
nums.some(num => num > 2) // true
nums.some(num => num > 4) // false
Functional Programming Higher-Order Array Functions Andrew Smith 120 / 712
Transforming: #map
[1, 2, 3, 4].map(n => n + 2)
map
n + 2
1 + 2
source array output array
1 2 3 4 2 + 2 3 4 5 6
3 + 2
4 + 2
Functional Programming Higher-Order Array Functions Andrew Smith 121 / 712
map
const shout = word => word.toUpperCase()
;['apple', 'dog', 'banana'].map(shout)
// ['APPLE', 'DOG', 'BANANA']
Functional Programming Higher-Order Array Functions Andrew Smith 122 / 712
Reducing to Single Value: #reduce
It’s like making a reduction out of your array.
Cooking down
Functional the contents into
Programming something
Higher-Order different.
Array Functions Andrew Smith 123 / 712
#reduce
Reduce takes a function and invokes it with every element in the array.
Additionally it gives the function an “accumulator”
A place to store the “rolled up” (or reduced) values of the array.
const nums = [1, 2, 3]
let sum = 0
nums.reduce((_, num) => { sum += num })
sum // 6
Functional Programming Higher-Order Array Functions Andrew Smith 124 / 712
reduce
Rather than keeping the accumulator outside the function. . .
We pass it as an argument to the function.
The function returns what will be the next accumulator.
const nums = [1, 2, 3]
const sum = nums.reduce((accumulator, num) => {
return accumulator + num // this will be passed as
// `accumulator` param in
// the next invocation
})
sum // 6
Functional Programming Higher-Order Array Functions Andrew Smith 125 / 712
Visualizing reduce
http://reduce.surge.sh/
Functional Programming Higher-Order Array Functions Andrew Smith 126 / 712
reduce
The last argument of reduce is the initialValue
for the accumulator. It defaults to the first value
of the array.
nums.reduce((acc, num) => acc + num) // 6
nums.reduce((acc, num) => acc + num, '') // '123'
Functional Programming Higher-Order Array Functions Andrew Smith 127 / 712
#map vs #reduce
What’s the difference between
map and reduce?
Functional Programming Higher-Order Array Functions Andrew Smith 128 / 712
#map vs #reduce
map: 1 : 1 transformation
reduce: M : 1 transformation
e.g. `map(double)`
e.g. `reduce(add)`
[ 1,
[ 1, --> [ 2,
2,
2, --> 4,
3] --> 6
3] --> 6]
Functional Programming Higher-Order Array Functions Andrew Smith 129 / 712
#reduce: The Transform Swiss Army Knife
When you need to transform an Array that isn’t 1:1
or a filter, then it’s reduce.
// e.g. to an object
const pairs = [['id', 42], ['name', 'Andrew']]
const fromPairs = (pairs) => pairs.reduce((acc, [key, value])
acc[key] = value
return acc
}, {})
const person = fromPairs(pairs)
person // { id: 42, name: 'Andrew' }
Functional Programming Higher-Order Array Functions Andrew Smith 130 / 712
Exercise: Arrays and Functional Programming
1. Open src/www/js/functional/array.test.js
2. Follow directions in the it statements
3. All tests should keep passing
$ cd src
$ yarn jest array.test.js --watch
Functional Programming Higher-Order Array Functions Andrew Smith 131 / 712
Closures
Functional Programming Closures Andrew Smith 132 / 712
Closures: Basics
• One of most powerful concepts in JavaScript
• Concept shared by most FP languages
• Every time a function gets created, a closure is born
Functional Programming Closures Andrew Smith 133 / 712
Closures: Definition
A function bundled (enclosed)
with surrounding state (lexical environment)
const init = () => {
const name = 'Andrew' // `name` is enclosed
const sayName = () => { console.log(name) }
sayName()
}
Functional Programming Closures Andrew Smith 134 / 712
Closures: Definition
Closures allow functions to refer to
surrounding state even after function has returned.
const init = () => {
const name = 'Andrew'
return () => {
console.log(name)
}
}
const sayMyName = init()
sayMyName() // 'Andrew'
Functional Programming Closures Andrew Smith 135 / 712
Demonstrating Closures: An Example
const init = () => {
const name = 'Andrew'
return () => {
console.log(name)
}
}
(Open src/examples/js/closure.html and play in the debugger.)
Functional Programming Closures Andrew Smith 136 / 712
Creating Private Variables
Using closures to create truly private variables in JavaScript:
const makeCounter = (startingValue) => {
let n = startingValue
return () => n += 1
}
(Open src/examples/js/closure.html and play in the debugger.)
Functional Programming Closures Andrew Smith 137 / 712
Creating Private Variables
Using closures to create truly private variables in JavaScript:
const createContainer = () => {
let privateVar = 42
return {
getPrivateVar: () => privateVar,
setPrivateVar: (n) => { privateVar = n },
}
}
const x = createContainer()
x.privateVar // undefined
x.getPrivateVar() // 42
Functional Programming Closures Andrew Smith 138 / 712
Objects and arrays are like data containers
• When a closure references an Object or Array. . .
• They refer to the container, not the contents at the time
(Demo)
Functional Programming Closures Andrew Smith 139 / 712
Exercise
1. Open src/www/js/functional/closure-private.test.js
2. Follow directions in the it statements
3. All tests should keep passing
$ cd src
$ yarn jest closure-private.test.js --watch
Functional Programming Closures Andrew Smith 140 / 712
Creating Specialized Behavior
Perhaps most powerful aspect of closures:
Specializing behavior of a general function.
const prop = (key) => (obj) => obj[key]
Functional Programming Closures Andrew Smith 141 / 712
Creating Specialized Behavior
// General
const prop = (key) => (obj) => obj[key]
// Specialized
const getId = prop('id')
const getName = prop('name')
const person = { id: 1, name: 'Andrew' }
getId(person) // 1
getName(person) // 'Andrew'
Functional Programming Closures Andrew Smith 142 / 712
Creating Specialized Behavior
What about getting prop off a list of objects?
Something like. . .
const getNames = pluck('name')
getNames(users) // ['Andrew', 'Billy', ...]
Functional Programming Closures Andrew Smith 143 / 712
Creating Specialized Behavior
Starting point:
const getNames = (items) => {
const vals = []
for (let i = 0; i < items.length; i++) {
vals.push(items[i].name)
}
return vals
}
Functional Programming Closures Andrew Smith 144 / 712
Creating Specialized Behavior
End point:
const pluck = (key) => (
(xs) => (
xs.map(prop(key))
)
)
Functional Programming Closures Andrew Smith 145 / 712
Exercise
1. Open src/www/js/functional/closure-specialize.test.js
2. Follow directions in the it statements
3. All tests should keep passing
$ cd src
$ yarn jest closure-specialize.test.js --watch
Functional Programming Closures Andrew Smith 146 / 712
Manually Setting Context
Functional Programming Manually Setting Context Andrew Smith 147 / 712
Function.prototype.call
Calling a function and explicitly setting this:
let x = {color: "red"};
let f = function() {console.log(this.color);};
f.call(x); // this.color === "red"
f.call(x, 1, 2, 3); // `this' + arguments.
Functional Programming Manually Setting Context Andrew Smith 148 / 712
Function.prototype.apply
The apply method is similar to call except that additional arguments are
given with an array:
let x = {color: "red"};
let f = function() {console.log(this.color);};
f.apply(x); // this.color === "red"
let args = [1, 2, 3];
f.apply(x, args); // `this' + arguments.
Functional Programming Manually Setting Context Andrew Smith 149 / 712
Function.prototype.bind
The bind method creates a new function which ensures your original
function is always invoked with this set as you desire, as well as any
arguments you want to supply:
let x = {color: "red"};
let f = function() {console.log(this.color);};
x.f = f;
let g = f.bind(x);
let h = f.bind(x, 1, 2, 3);
g(); // Same as x.f();
h(); // Same as x.f(1, 2, 3);
Functional Programming Manually Setting Context Andrew Smith 150 / 712
Document Object Model (DOM)
The Document Object Model Document Object Model (DOM) Andrew Smith 151 / 712
What is the DOM?
• The browser’s API for the HTML document
• Allows you to read and manipulate a website
• Browser parses HTML and builds a tree data structure
The Document Object Model Document Object Model (DOM) Andrew Smith 152 / 712
The Document Structure
• The document object provides access to the document
• Two primary node types:
• Element
• Text
The Document Object Model Document Object Model (DOM) Andrew Smith 153 / 712
HTML Tree
<html>
<body>
<h1 id="title">Welcome</h1>
<p>Cool <span class="loud">Site!</span></p>
</body>
</html>
The Document Object Model Document Object Model (DOM) Andrew Smith 154 / 712
Parsed HTML Tree
html
[0]
body
[0] [1]
h1
p
id: "title"
[1] [0]
span
"Welcome" "Cool"
className: "loud"
"Site!"
The Document Object Model Document Object Model (DOM) Andrew Smith 155 / 712
Element Nodes
• The HTML:
<p id="name" class="hi">My <span>text</span></p>
• Maps to:
{
tagName: "P",
childNodes: NodeList,
className: "hi",
innerHTML: "My <span>text</span>",
id: "name",
// ...
}
The Document Object Model Document Object Model (DOM) Andrew Smith 156 / 712
Querying
The Document Object Model Querying Andrew Smith 157 / 712
Querying the DOM
It all starts with the document
The Document Object Model Querying Andrew Smith 158 / 712
Getting Single Elements
.getElementById('root') Gets by ID
.querySelector('p span') Gets first element by CSS selector.
Depth-first pre-order traversal.
The Document Object Model Querying Andrew Smith 159 / 712
Getting Multiple Elements
These return a NodeList
.getElementsByTagName('a') All elements of that tag
.getElementsByClassName('highlight') All elements with that class
.querySelectorAll('p span') All elements match by CSS selector
The Document Object Model Querying Andrew Smith 160 / 712
The NodeList
Array-like object that isn’t actually an Array.
Common methods like .map don’t exist.
const alerts = document.getElementsByClassName('alert')
console.log(alerts.length) // 4
alerts.map(() => {}) // Error: map undefined
The Document Object Model Querying Andrew Smith 161 / 712
The NodeList
Convert to an Array with Array.from() or [...nodeList]
const alerts = document.getElementsByClassName('alert')
const values1 = Array.from(alerts).map(/* */ ) // OK
const values2 = [...alerts].map(/* */ ) // OK
The Document Object Model Querying Andrew Smith 162 / 712
Refining the Search
These selectors can be done on any node.
const app = document.getElementById('app')
const form = app.getElementsByTagName('form')
const checkboxes = form.getElementsByClassName('checkbox')
The Document Object Model Querying Andrew Smith 163 / 712
Traversing
The Document Object Model Traversing Andrew Smith 164 / 712
Traversal
You can traverse from any element in the DOM.
The Document Object Model Traversing Andrew Smith 165 / 712
Traversal
These methods exclude text nodes.
• parentElement
• children
• firstElementChild
• lastElementChild
• previousElementSibling
• nextElementSibling
The Document Object Model Traversing Andrew Smith 166 / 712
Traversal
These methods include element and text nodes.
• parentNode
• childNodes
• firstChild
• lastChild
• previousSibling
• nextSibling
The Document Object Model Traversing Andrew Smith 167 / 712
Manipulating
The Document Object Model Manipulating Andrew Smith 168 / 712
Creating New Nodes
document.createElement('a') Returns a new node without inserting it
into the DOM.
document.createTextNode('hello') Returns a new text node.
Once you have an element, you can append it anywhere.
parent.appendChild(newChild) Appends newChild.
The Document Object Model Manipulating Andrew Smith 169 / 712
Creating New Nodes
Adding some text. . .
const message = document.createElement('p')
message.appendChild(document.createTextNode('Hello'))
document.body.appendChild(message)
Adding an image. . .
const image = document.createElement('img')
image.src = 'https://www.placecage.com/200/300'
document.getElementById('root').appendChild(image)
The Document Object Model Manipulating Andrew Smith 170 / 712
Creating New Nodes
Adding children to children for a list. . .
const list = document.createElement('ul')
const todo1 = document.createElement('li')
todo1.appendChild(document.createTextNode('Learn DOM'))
list.appendChild(todo1)
document.getElementById('root').appendChild(list)
The Document Object Model Manipulating Andrew Smith 171 / 712
Appending Multiple Times
appendChild on existing node moves it.
const moveToEnd = () => {
const list = document.getElementsByTagName('ul')[0]
const todo = list.children[0]
list.appendChild(todo)
}
The Document Object Model Manipulating Andrew Smith 172 / 712
Other Manipulations
Methods on a parent element:
.insertBefore(newChild, existingChild) Inserts newChild before
existing existingChild.
.replaceChild(newChild, existingChild) Replaces existingChild
with newChild.
.removeChild(existingChild) Removes existingChild.
The Document Object Model Manipulating Andrew Smith 173 / 712
Node Content
The Document Object Model Node Content Andrew Smith 174 / 712
HTML
element.innerHTML: Gets/sets HTML markup in an element
document.body.innerHTML = '' +
'<ul>' +
' <li>Learn DOM</li>' +
' <li>Practice innerHMTL</li>' +
'</ul>'
console.log(document.body.innerHTML)
The Document Object Model Node Content Andrew Smith 175 / 712
Text
element.innerText: Gets/sets Human-visible text content
const title = document.getElementById('title')
console.log(title.innerText) // 'Welcome'
title.innerText = 'Goodbye'
console.log(title.innerText) // 'Goodbye'
(.textContent also works; it’s more performant but less smart)
The Document Object Model Node Content Andrew Smith 176 / 712
Form Values
inputElement.value: Gets/sets form input value
const input = document.createElement('input')
input.value = 'I love tea'
console.log(input.value) // 'I love tea'
document.body.appendChild(input)
The Document Object Model Node Content Andrew Smith 177 / 712
Form Values
inputElement.checked: Gets/sets form input checkbox/radio
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.checked = true
console.log(checkbox.checked) // true
document.body.appendChild(checkbox)
The Document Object Model Node Content Andrew Smith 178 / 712
Attributes
Elements can have attributes that specify behavior or store information.
<img src="photo.jpg">
Element methods:
• .getAttribute(name)
• .setAttribute(name, value)
• .hasAttribute(name)
• .removeAttribute(name)
The Document Object Model Node Content Andrew Smith 179 / 712
Classes
Element methods:
• .classList.add(name)
• .classList.remove(name)
• .classList.toggle(name)
• .classList.contains(name)
const title = document.getElementById('title')
title.classList.contains('hidden') // false
title.classList.toggle('hidden')
title.classList.contains('hidden') // true
title.classList.toggle('hidden')
title.classList.contains('hidden') // false
The Document Object Model Node Content Andrew Smith 180 / 712
Exercise
1. Open http://localhost:3000/js/dom/exercise.html
2. Open these files:
1. src/www/js/dom/exercise.js
2. src/www/js/dom/exericse.html (read only)
3. Follow the instructions
The Document Object Model Node Content Andrew Smith 181 / 712
Event Handling and Callbacks
The Document Object Model Event Handling and Callbacks Andrew Smith 182 / 712
Events Overview
• Events can be click, hover, key press, focus, form submit, etc.
• Events are received in a synchronous “event loop”:
• JS runtime hangs out, quietly waiting for events
• Events fire and trigger registered handler functions
• Handler functions run synchronously
The Document Object Model Event Handling and Callbacks Andrew Smith 183 / 712
Most Common Events
• click
• change
• keydown
• submit
(Example: src/examples/js/events.html)
The Document Object Model Event Handling and Callbacks Andrew Smith 184 / 712
Slightly More Comprehensive List
• UI: load, unload, error, resize, scroll
• Keyboard: keydown, keyup, keypress
• Mouse: click, dblclick, mousedown, mouseup, mousemove
• Touch: touchstart, touchend, touchcancel, touchleave, touchmove
• Focus: focus, blur
• Form: input, change, submit, reset, select, cut, copy, paste
The Document Object Model Event Handling and Callbacks Andrew Smith 185 / 712
Handling Events
1. Get the element to monitor
• document.getElementBy...
2. Register a function for an event on that element
• .addEventListener('event', handler)
The Document Object Model Event Handling and Callbacks Andrew Smith 186 / 712
Event Registration
const button = document.getElementById('greet')
button.addEventListener('click', () => {
alert('Hello!')
})
The Document Object Model Event Handling and Callbacks Andrew Smith 187 / 712
Event Bubbling
• Events propagate from the target node upwards
• Called bubbling
<form onclick="console.log('form')">FORM
<div onclick="console.log('div')">DIV
<p onclick="console.log('p')">P</p>
</div>
</form>
Clicking <p>: P DIV FORM
Clicking <div>: DIV FORM
The Document Object Model Event Handling and Callbacks Andrew Smith 188 / 712
Event Bubbling
• Bubbling can be prevented with event.stopPropagation
<body onclick="console.log('Will not be reached')">
<button>Click me</button>
</body>
button.addEventListener('click', (event) => {
event.stopPropagation()
console.log('button clicked')
})
Clicking <button>: button clicked
The Document Object Model Event Handling and Callbacks Andrew Smith 189 / 712
Sidenote
We’re using onclick for terse examples
Generally only use addEventListener
The Document Object Model Event Handling and Callbacks Andrew Smith 190 / 712
Browser Default Action
• The browser has a default action for many events, e.g.
• submit will POST to the form action url
• Clicking a link will load a new page
• Default action prevented with event.preventDefault
The Document Object Model Event Handling and Callbacks Andrew Smith 191 / 712
Event Delegation
<ul>
<li>Option 1</li>
<li>Option 2</li>
<li>Option 3</li>
</ul>
Repeated registered handlers. :-(
Array.from(document.getElementsByTagName('li'))
.forEach((li, i) => {
li.addEventListener('click', () => {
console.log(`Clicked option ${i + 1}`)
})
})
The Document Object Model Event Handling and Callbacks Andrew Smith 192 / 712
Event Delegation
What happens if another li is added dynamically
and then clicked?
(Example: src/examples/js/events.html)
The Document Object Model Event Handling and Callbacks Andrew Smith 193 / 712
Event Delegation
Event handler event knows two things:
• currentTarget: Where it’s registered
• target: Who triggered the event
The Document Object Model Event Handling and Callbacks Andrew Smith 194 / 712
Event Delegation
How can we use this to our advantage?
The Document Object Model Event Handling and Callbacks Andrew Smith 195 / 712
Event Delegation
Put the handler on the parent
const ul = document.getElementsByTagName('ul')[0]
ul.addEventListener('click', (e) => {
console.log(ul === e.currentTarget) // true
console.log(`Clicked ${e.target.innerText}`)
})
The Document Object Model Event Handling and Callbacks Andrew Smith 196 / 712
Functions Given Context of Event
Event handlers are given context of that element.
document.getElementsByTagName('button')
.addEventListener('click', function() {
console.log(this) // the button that was clicked
})
Arrow functions won’t work here.
document.getElementsByTagName('button')
.addEventListener('click', () => {
console.log(this) // window :-(
})
The Document Object Model Event Handling and Callbacks Andrew Smith 197 / 712
Functions Given Context of Event
Easy solution is to use event.currentTarget.
document.getElementsByTagName('button')
.addEventListener('click', (event) => {
console.log(event.currentTarget) // the button
})
The Document Object Model Event Handling and Callbacks Andrew Smith 198 / 712
Functions Given Context of Event
Moral of the story: this can be more expressive
. . . but mostly it causes confusion. Avoid it when possible.
The Document Object Model Event Handling and Callbacks Andrew Smith 199 / 712
Exercise: Simple User Interaction
1. Open the following files in your text editor:
• src/www/js/events/events.js
• src/www/js/events/index.html (read only!)
2. Open the index.html file in your web browser.
3. Complete the exercise.
The Document Object Model Event Handling and Callbacks Andrew Smith 200 / 712
Browser APIs
Browser APIs Browser APIs Andrew Smith 201 / 712
Timers
Browser APIs Timers Andrew Smith 202 / 712
setTimeout
• Tell a function to run later
• Waiting does not block other execution
• Only runs once
const timeoutInMs = 1000
const doSomethingLater = () => {/* ... */ }
setTimeout(doSomethingLater, timeoutInMs)
Browser APIs Timers Andrew Smith 203 / 712
setTimeout
• The contents of function do not run until after
const doSomethingLater = () => {
console.log('Later')
}
setTimeout(doSomethingLater, 1000)
console.log('Hello')
// what gets printed to console?
Browser APIs Timers Andrew Smith 204 / 712
setTimeout
• The contents of function do not run until after
const doSomethingLater = () => {
console.log('Later')
}
setTimeout(doSomethingLater, 1000)
console.log('Hello')
// what gets printed to console?
// Hello
// Later
Browser APIs Timers Andrew Smith 205 / 712
setTimeout
• Same if you inlined the function
• It’s less obvious, so practice recognizing the pattern
setTimeout(() => {
console.log('Later')
}, 1000)
console.log('Hello')
Browser APIs Timers Andrew Smith 206 / 712
setTimeout cancellation
• Use clearTimeout web API
• setTimeout returns an id value that you use to reference that timer
• Pass the value to clearTimeout to cancel the timer
Browser APIs Timers Andrew Smith 207 / 712
setTimeout cancellation
<p>Live Example</p>
<button onclick="delayedAlert()">Show an alert box after two s
<button onclick="clearAlert()">Cancel alert before it happens<
let timeoutID
const delayedAlert = () => {
timeoutID = setTimeout(() => window.alert('Yo'), 2000)
}
const clearAlert = () => {
clearInterval(timeoutID)
}
Browser APIs Timers Andrew Smith 208 / 712
this darn problem strikes again
• setTimeout changes the execution context (this) of the function
• When the function runs, window will be the execution context
• If you’re using the function syntax and this inside, it won’t be what
you think
Browser APIs Timers Andrew Smith 209 / 712
this darn problem strikes again
const person = {
name: 'Andrew',
sayHello() {
setTimeout(function() {
console.log(`My name is ${this.name}`)
}, 1000)
}
}
person.sayHello()
// what does it print out?
Browser APIs Timers Andrew Smith 210 / 712
this darn problem strikes again
• You can use arrow functions to preserve lexical context
const person = {
name: 'Andrew',
sayHello() {
setTimeout(() => {
console.log(`My name is ${this.name}`)
}, 1000)
}
}
person.sayHello() // My name is Andrew
Browser APIs Timers Andrew Smith 211 / 712
this darn problem strikes again
• Or bind the context
const person = {
name: 'Andrew',
sayHello() {
setTimeout(function() {
console.log(`My name is ${this.name}`)
}.bind(this), 1000)
}
}
person.sayHello() // My name is Andrew
Browser APIs Timers Andrew Smith 212 / 712
setInterval
• Run the function at a regular interval
• Does not block other execution while waiting
let isTick = true
const clock = () => {
console.log(isTick ? 'tick' : 'tock')
isTick = !isTick
}
setInterval(clock, 1000)
Browser APIs Timers Andrew Smith 213 / 712
Canceling setInterval
• Use clearInterval
let isTick = true
const clock = () => {/* */ }
let intervalID
const start = () => {
intervalID = setInterval(clock, 1000)
}
const stop = () => clearInterval(intervalID)
Browser APIs Timers Andrew Smith 214 / 712
Limiting events
• Two key words: throttle and debounce
• throttle: consistently respond to a frequent event
• debounce: eventually respond to a frequent event
Browser APIs Timers Andrew Smith 215 / 712
Limiting events
• Throttle: only respond after X timeout; new events are ignored
• Debounce: wait X timeout before responding; new events extend
timeout
• Both are implemented using setTimeout!
• Visualization 1 (link)
Browser APIs Timers Andrew Smith 216 / 712
Limiting events
• Both are functions that will wrap an event handler
• The returned function is called “throttled” or “debounced”
• You give that handler to an event listener.
const throttledHandler = throttle(handleFn, 300)
document.getElementById('button')
.addEventListener('click', throttledHandler)
Browser APIs Timers Andrew Smith 217 / 712
Limiting events
Example: http://localhost:3000/js/browser/throttling.html
Browser APIs Timers Andrew Smith 218 / 712
Limiting events
• Many application can get away with a simple implementation
• For more advanced applications, use
• lodash.throttle:
https://www.npmjs.com/package/lodash.throttle
• lodash.debounce:
https://www.npmjs.com/package/lodash.debounce
Browser APIs Timers Andrew Smith 219 / 712
Exercise
1. Open http://localhost:3000/js/browser/timers.html
2. Open www/js/browser/timers.js
3. Follow instructions in the JS file
Browser APIs Timers Andrew Smith 220 / 712
Web Storage
Browser APIs Web Storage Andrew Smith 221 / 712
What is Web Storage?
• Allows you to store key/value pairs
• Two levels of persistence and sharing
• Very simple interface
• Keys and values must be strings
Browser APIs Web Storage Andrew Smith 222 / 712
Session Storage
• Lifetime: same as the containing window/tab
• Sharing: Only code in the same window/tab
• 5MB user-changeable limit (10MB in IE)
• Basic API:
sessionStorage.setItem("key", "value");
let item = sessionStorage.getItem("key");
sessionStorage.removeItem("key");
Browser APIs Web Storage Andrew Smith 223 / 712
Local Storage
• Lifetime: unlimited
• Sharing: All code from the same domain
• 5MB user-changeable limit (10MB in IE)
• Basic API:
localStorage.setItem("key", "value");
let item = localStorage.getItem("key");
localStorage.removeItem("key");
Browser APIs Web Storage Andrew Smith 224 / 712
The Storage Object
Properties and methods:
• length: The number of items in the store.
• key(n): Returns the name of the key in slot n.
• clear(): Remove all items in the storage object.
• getItem(key), setItem(key, value), removeItem(key).
Browser APIs Web Storage Andrew Smith 225 / 712
Browser Support
• IE >= 8
• Firefox >= 2
• Safari >= 4
• Chrome >= 4
• Opera >= 10.50
Browser APIs Web Storage Andrew Smith 226 / 712
Exercise
1. Go to http://localhost:3000/js/browser/storage.html
2. Open src/www/js/browser/storage.js and follow instructions
Browser APIs Web Storage Andrew Smith 227 / 712
Web Workers
Browser APIs Web Workers Andrew Smith 228 / 712
Web Workers
• JS is a single threaded environment
• Responses to user interactions (e.g. clicks) run in the same thread as
everything else
• Long-running functions will “lock” the UI
• Click events won’t be processed until the expensive calc finishes
Browser APIs Web Workers Andrew Smith 229 / 712
Web Workers
• Web Workers provide a way to run on a “background thread”
• Computations there do not block the main thread
• Communication occurs with “message passing”
Browser APIs Web Workers Andrew Smith 230 / 712
Demo
• (link)
Browser APIs Web Workers Andrew Smith 231 / 712
Web Workers
• Create a separate JS file that is the worker file e.g. worker.js
• Load it into your app:
const worker = new Worker('worker.js')
• Now you have a worker available!
Browser APIs Web Workers Andrew Smith 232 / 712
Web Workers
• You send messages from the main thread:
worker.postMessage('hello world')
• And listen for messages on the web worker:
onmessage = ({ data }) => {
console.log(data) // hello world
}
Browser APIs Web Workers Andrew Smith 233 / 712
Web Workers
• Passing messages back to the main thread:
postMessage('Worker finished')
• And listen for them on the main thread:
worker.onmessage(({ data }) => {
console.log(data) // Worker finished
})
// alternatively:
worker.addEventListener('message', () => {})
Browser APIs Web Workers Andrew Smith 234 / 712
Data is copied not shared
• If you pass an object to a worker and mutate it. . .
// in main
const person = { name: 'Andrew' }
worker.postMessage(person)
// in worker
person.name = 'Billy'
postMessage('Billy')
The mutation in the worker will not affect the person object
Browser APIs Web Workers Andrew Smith 235 / 712
Loading other scripts
• Load other script files in the worker with importScripts
importScripts('foo.js')
importScripts('//example.com/foo.js') // other domain
Browser APIs Web Workers Andrew Smith 236 / 712
Resources
• MDN (link)
Browser APIs Web Workers Andrew Smith 237 / 712
Exercise
• Go to http://localhost:3000/js/browser/webworkers.html
• Open src/www/js/browser/webworkers.js and follow instructions
Browser APIs Web Workers Andrew Smith 238 / 712
Websockets
Browser APIs Websockets Andrew Smith 239 / 712
Typical HTTPS requests
• Request / response cycle
• Always initiated by the client
• Server can only respond once
Browser APIs Websockets Andrew Smith 240 / 712
Typical HTTPS requests
Browser APIs Websockets Andrew Smith 241 / 712
Getting updates from server
• Polling
• “Long polling”
• Websockets
Browser APIs Websockets Andrew Smith 242 / 712
Websockets
• Upgrades regular TCP connection to “duplex”
• Creates a two-way communication channel
• Each sends messages whenever they want
• Not subject to CORS
Browser APIs Websockets Andrew Smith 243 / 712
Security
• Encrypt traffic and confirm identity
• Never execute foreign JS
Browser APIs Websockets Andrew Smith 244 / 712
Websockets (client)
const ws = new WebSocket('ws://localhost:3030/')
// connection opened
ws.onopen = () => {}
// connection closed
ws.onclose = () => {}
// error occured
ws.onerror = () => {}
// message received
ws.onmessage = ({ data }) => {}
// send message
ws.send('PING')
// close
Browserconnection
APIs Websockets Andrew Smith 245 / 712
Websockets (server)
// `ws` is a popular implementation of websockets
const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 3030 })
// new connection received
wss.on('connection', (ws, req) => {
// send message
ws.send('Welcome!')
// on disconnect
ws.on('close', () => {})
})
Browser APIs Websockets Andrew Smith 246 / 712
Demo
• Visit http://localhost:3000/js/browser/websockets/demo/
Browser APIs Websockets Andrew Smith 247 / 712
Authenticating
• You can send query params along with the WS connection:
ws = new WebSocket(`ws://localhost:3030/?user=${user}`)
• You could use an auth key for real security
• Read the query params on the WS side:
const url = require('url')
wss.on('connection', (ws, req) => {
const user = url.parse(req.url, true).query.user
// check if user is valid...
})
Browser APIs Websockets Andrew Smith 248 / 712
Communicating with other clients
You can access other connected clients: wss.clients
wss.clients.forEach(client => {
client.send('Hello')
})
Browser APIs Websockets Andrew Smith 249 / 712
Communicating with other clients
WS will error if you try to send to a closed client:
Error: WebSocket is not open: readyState 3 (CLOSED)
So check for readiness before sending:
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message)
}
})
Browser APIs Websockets Andrew Smith 250 / 712
Communicating with other clients
You can also skip yourself when iterating across clients:
const sendToClients = (ws, message) => {
wss.clients.forEach(client => {
if (client !== ws) {
// ...
}
})
}
Browser APIs Websockets Andrew Smith 251 / 712
Exercise
1. Go to http://localhost:3000, Browser APIs > WebSockets (link)
2. Open file src/www/js/browser/websockets/exercise/main.js and server.js
Browser APIs Websockets Andrew Smith 252 / 712
JavaScript Runtime
Asynchronous Programming Andrew Smith 253 / 712
Visualizing the Runtime
Demo (link)
Asynchronous Programming Andrew Smith 254 / 712
Promises
Asynchronous Programming Promises Andrew Smith 255 / 712
Callbacks without Promises
$.ajax('/a', (data_a) => {
$.ajax('/b/' + data_a.id, (data_b) => {
$.ajax('/c/' + data_b.id, (data_c) => {
console.log('Got C: ', data_c)
}, () => {
console.error('Call failed')
})
}, () => {
console.error('Call failed')
})
}, () => {
console.error('Call failed')
})
Asynchronous Programming Promises Andrew Smith 256 / 712
Callbacks Using Promises
$.ajax('/a')
.then((data) => {
return $.ajax('/b/' + data.id)
})
.then((data) => {
return $.ajax('/c/' + data.id)
})
.then((data) => {
console.log('Got C: ', data)
})
.then((data) => {
console.log(data)
})
.catch((message) => {
console.error('Something failed:', message)
})
Asynchronous Programming Promises Andrew Smith 257 / 712
Promise Details
• Guarantee that callbacks are invoked (no race conditions)
• Composable (can be chained together)
• Flatten code that would otherwise be deeply nested
Asynchronous Programming Promises Andrew Smith 258 / 712
Visualizing Promises (Composition)
a b → c
a b → c
a b → c
Asynchronous Programming Promises Andrew Smith 259 / 712
Visualizing Promises (Owner)
Asynchronous Programming Promises Andrew Smith 260 / 712
Example: Promise Owner
var delayed = function() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
if (/* some condition */ true) {
resolve(/* resolved value */ 100);
} else {
reject(/* rejection value */ 0);
}
}, 500);
});
};
Asynchronous Programming Promises Andrew Smith 261 / 712
Visualizing Promises (User)
Asynchronous Programming Promises Andrew Smith 262 / 712
Promise Composition Example
// Taken from the `src/spec/promise.spec.js' file.
var p = new Promise(function(resolve, reject) {
resolve(1);
});
p.then(function(val) {
expect(val).toEqual(1);
return 2;
}).then(function(val) {
expect(val).toEqual(2);
done();
});
Asynchronous Programming Promises Andrew Smith 263 / 712
async and await
Asynchronous Programming async and await Andrew Smith 264 / 712
What are async Functions?
Functions marked as async become asynchronous and automatically return
promises:
async function example() {
return 'Hello World'
}
example().then(function (str) {
console.log(str) // "Hello World"
})
Asynchronous Programming async and await Andrew Smith 265 / 712
The await Keyword
Functions marked as async get to use the await keyword:
async function example2() {
let str = await example()
console.log(str) // "Hello World"
}
Question: What does the example2 function return?
Asynchronous Programming async and await Andrew Smith 266 / 712
Example of async/await
async function getArtist() {
try {
const { data: artist } = await axios.get('/api/artists/1')
const { data: albums } = await axios.get('/api/artists/1/a
artist.albums = albums
return artist
} catch (e) {
// Rejected promises throw exceptions
// when using `await'.
}
}
Asynchronous Programming async and await Andrew Smith 267 / 712
An Even Better Example of async/await
async function getArtistP() {
// Kick off two requests in parallel:
const p1 = axios.get('/api/artists/1').then(res => res.data)
const p2 = axios.get('/api/artists/1/albums').then(res => re
// Wait for both requests to finish:
const [artist, albums] = await Promise.all([p1, p2])
artist.albums = albums
return artist
}
Asynchronous Programming async and await Andrew Smith 268 / 712
Exercise
1. Go to http://localhost:3000/js/async/promises.html
2. Open src/www/js/async/promises.js
3. Follow prompts
Asynchronous Programming async and await Andrew Smith 269 / 712
Network Calls
Asynchronous Programming Network Calls Andrew Smith 270 / 712
REST endpoints
• REpresentational State Transfer
• Envisioned in 2000 for a Ph.D. thesis (link)
• Core principles:
• Client-server architecture
• Stateless
• Cacheable
• Uniform interface
Asynchronous Programming Network Calls Andrew Smith 271 / 712
REST endpoints
• Describe routes by the resources involved
• Route w/o an ID refers to the “collection”
• /todos
• /artists
• /articles
• Route w/ an ID refers to a specific entity
• /todos/42
• /artists/33
• /articles/my-blog-article
Asynchronous Programming Network Calls Andrew Smith 272 / 712
REST endpoints
HTTP requests have a method to describe intent
• GET - read
• POST - create
• PUT - update
• DELETE - delete
Links to dive deeper:
(Link 1) (Link 2) (Link 3) (Link 4)
Asynchronous Programming Network Calls Andrew Smith 273 / 712
REST endpoints
GET goes to collection OR member
$ curl -X GET http://localhost:3000/api/todos
[
{ "id": 1, "text": "Learn JS", "done": false },
{ "id": 2, "text": "Learn React", "done": false },
...
]
$ curl -X GET http://localhost:3000/api/todos/1
{ "id": 1, "text": "Learn JS", "done": false }
Asynchronous Programming Network Calls Andrew Smith 274 / 712
REST endpoints
POST goes to the collection
$ curl -d '{ "text": "Practice REST", "done": false }' \
-H "Content-Type: application/json" \
-X POST http://localhost:3000/api/todos
Returns the newly created member:
{
"id": 4,
"text": "Practice REST",
"done": false
}
Asynchronous Programming Network Calls Andrew Smith 275 / 712
REST endpoints
PUT goes to member
$ curl -d '{ "text": "Practice REST", "done": true }' \
-H "Content-Type: application/json" \
-X PUT http://localhost:3000/api/todos/4
Returns the newly updated member:
{
"id": 4,
"text": "Practice REST",
"done": true
}
Asynchronous Programming Network Calls Andrew Smith 276 / 712
REST endpoints
DELETE goes to member
$ curl -X DELETE http://localhost:3000/api/todos/4
{}
Asynchronous Programming Network Calls Andrew Smith 277 / 712
Query Parameters
• Key-value pairs passed on an HTTP request
• Commonly used to specify additional behavior of GET requests
• Query params come at end of URL after ?
• Multiple values joined with &
GET /todos?page=7
GET /calendar?locale=en&date=2020-07-27
Asynchronous Programming Network Calls Andrew Smith 278 / 712
Tinkering with Endpoints
curl is fun and all, but. . .
More positive UI: Postman
Asynchronous Programming Network Calls Andrew Smith 279 / 712
Tinkering with Endpoints
Asynchronous Programming Network Calls Andrew Smith 280 / 712
HTTP Requests in JavaScript
Bare metal: new XMLHttpRequest
More discussion for the curious: (link)
Asynchronous Programming Network Calls Andrew Smith 281 / 712
HTTP Requests with axios
axios is a promise-based library for HTTP requests.
Much better DX than XMLHttpRequest
yarn add axios
Asynchronous Programming Network Calls Andrew Smith 282 / 712
A Quick Aside
• People got aboard the fetch hype train back in 2017 or so
• Similar functionality
• Native web API
• DX not quite as nice as axios
Asynchronous Programming Network Calls Andrew Smith 283 / 712
HTTP Requests with axios
axios.get('/todos')
axios.get('/todos/1')
axios.post('/todos', { text: 'Learn axios', done: false })
axios.put('/todos/4', { text: 'Learn axios', done: true })
axios.delete('/todos/4')
Asynchronous Programming Network Calls Andrew Smith 284 / 712
Handling Responses
Response structure:
{
request: {/* ... */ }, // your request
response: {
data: [], // data from the server
status: 200, // HTTP status
headers: {} // response headers
}
}
Asynchronous Programming Network Calls Andrew Smith 285 / 712
Handling Responses
// Promise-based
axios.get('/todos', { params: { page: 2 } })
.then((res) => {
console.log(res.data) // [...]
})
// Async-await
const { data } = await axios.get(
'/todos',
{ params: { page: 2 } }
)
console.log(data) // [...]
Asynchronous Programming Network Calls Andrew Smith 286 / 712
Handling Errors
try {
const { data } = await api.get('/todos/invalidId')
} catch (error) {
if (error.response) {
// The request was made and the server responded with
// a status code that falls out of the range of 2xx
console.log(error.response.data)
console.log(error.response.status)
} else {
// Something happened in setting up the request
// that triggered an Error
console.log('Error', error.message)
}
}
Asynchronous Programming Network Calls Andrew Smith 287 / 712
Handling Errors
• Network errors do not return a response
try {
const { data } = await api.get('/todos/invalidId')
} catch (error) {
// this will trigger another JS error on network errors
// trying to read `data` of undefined
console.log(error.response.data)
}
Asynchronous Programming Network Calls Andrew Smith 288 / 712
Handling Errors
Checking for network errors:
export const isAxiosNetworkError = (e) => (
!e.response && e.message === 'Network Error'
)
Asynchronous Programming Network Calls Andrew Smith 289 / 712
Configuration
import axios from 'axios'
// create the instance
const api = axios.create({
// specify a base URL other than current domain
baseURL: 'http://localhost:3000/api'
})
// adding auth
api.defaults.headers.common['Authorization'] = AUTH_TOKEN
// making calls with your API instance
api.get(/* ... */ )
Asynchronous Programming Network Calls Andrew Smith 290 / 712
Configuration
Other functionality you can set:
• Transform incoming/outgoing payloads
• Add query params
• Add cookies, set headers
• Specify response type / response encoding
• Cancel a request
Much of it you will learn on a need to know basis.
Docs: (link)
Asynchronous Programming Network Calls Andrew Smith 291 / 712
Exercise
Turn on your local server:
$ cd src
$ yarn start
1. Practice GET, POST, PUT, DELETE using Postman
2. Practice GET, POST, PUT, DELETE using axios
3. Practice using query params (docs link)
• Request the first 2 todos
• Request only uncompleted todos
• Request the first uncompleted todo
Asynchronous Programming Network Calls Andrew Smith 292 / 712
Forms
Forms Forms Andrew Smith 293 / 712
Forms
• Because what website can exist without one?
• Contains interactive controls for submitting information
<form>
{/* ... */ }
</form>
• Common form elements
• Examples?
Forms Forms Andrew Smith 294 / 712
Form Inputs
<form>
Short text:
<input type="text" name="name">
Long text:
<textarea rows="3" name="bio"></textarea>
<input type="submit" value="Submit" />
<button type="submit">Also a submit button</button>
</form>
Forms Forms Andrew Smith 295 / 712
Form Inputs
Dropdown (select)
<select name="pets">
<option value="">Which one is better?</option>
<option value="dog">Dog</option>
<option value="cat">Cat</option>
</select>
Forms Forms Andrew Smith 296 / 712
Form Inputs
Check boxes
<input type="checkbox" name="newsletter"
value="1" id="subscribe">
<label for="subscribe">Sign up for newsletter</label>
Forms Forms Andrew Smith 297 / 712
Form Inputs
Radio buttons
<input type="radio" name="contact" value="email"
id="contact-email">
<label for="contact-email">Email</label>
<input type="radio" name="contact" value="phone"
id="contact-phone">
<label for="contact-phone">Phone</label>
Forms Forms Andrew Smith 298 / 712
Form Inputs
Having checkboxes/radio buttons checked by default:
• set the checked property
<input type="radio" name="contact" value="email"
id="contact-email" checked>
<label for="contact-email">Email</label>
<input type="radio" name="contact" value="phone"
id="contact-phone">
<label for="contact-phone">Phone</label>
Forms Forms Andrew Smith 299 / 712
Submitting Forms
Inside a form:
<form method="post" action="/users">
<input type="submit" value="This will submit">
<button type="submit">Also will submit</button>
<button>Implicitly submit</button>
</form>
Will POST to /users with form data
Forms Forms Andrew Smith 300 / 712
Submitting Forms
Forms Forms Andrew Smith 301 / 712
Submitting Forms
Default is for page to show server response of POST
Forms Forms Andrew Smith 302 / 712
Following Proper HTML Form Patterns
• MDN docs (link)
• General principles
• All inputs have a label with a htmlFor prop that matches the input id
• All inputs have a unique id
• Inputs have a name prop that correspond to form attribute values
• e.g. email, password, etc.
• Prefer HTML5 validation over manual validation
• Leverage native HTML behavior (e.g. submitting forms)
Forms Forms Andrew Smith 303 / 712
Validations
Can add required="required" to most input elements
<input type="text" required="required">
<select required="required">...</select>
<input type="radio" name="foo" value="foo"
required="required">
Forms Forms Andrew Smith 304 / 712
Validations
Can add minlength="..." or maxlength="..." to enforce length
restrictions
Forms Forms Andrew Smith 305 / 712
Validations
Add pattern to match against a RegExp pattern.
Include title to explain the pattern matching.
<input type="text" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
title="Enter digits in the format ###-###-####">
Forms Forms Andrew Smith 306 / 712
Exercise
1. Run $ yarn start or $ npm start from src directory
2. Go to http://localhost:3000/js/forms/basic.html
3. Open src/www/js/forms/basic.html and follow prompts
Forms Forms Andrew Smith 307 / 712
Forms with AJAX
Forms Forms with AJAX Andrew Smith 308 / 712
Submitting Forms
• Default behavior: page loads server response
• Not so great for interactivity
• Or if you want to stay on the same page
Forms Forms with AJAX Andrew Smith 309 / 712
Submitting Forms AJAX
• Here AJAX comes back into play!
• You control the network request and how to respond to it
• Tricky part: sending all the data to the server!
Forms Forms with AJAX Andrew Smith 310 / 712
Submitting Forms AJAX
Listen to submit event, not click on Submit button!
formEl.addEventListener('submit', () => {}) // yep
button.addEventListener('click', () => {}) // nope
Then you get default HTML behavior for free, like pressing Enter in a form
field to submit.
Forms Forms with AJAX Andrew Smith 311 / 712
Submitting Forms AJAX
Two ways to manage form data:
• Controlled
• Uncontrolled
Forms Forms with AJAX Andrew Smith 312 / 712
Controlled Forms
Controlled means you’re tracking form values in JS
• Constantly keeping them in sync.
• When you send data, you just send the JS object to axios
• Send as application/json
// presumably `name` and `email` exist
const user = { name, email }
axios.post('/users', { user })
Forms Forms with AJAX Andrew Smith 313 / 712
Controlled Forms
application/json payload looks like JSON:
{"user":{"name":"Andrew","email":"[email protected]"}}
Forms Forms with AJAX Andrew Smith 314 / 712
Uncontrolled Forms
Uncontrolled means you leverage the form behavior and FormData
• No syncing with JS values
• Doesn’t easily handle complex data (objects, etc.)
• Sends as multipart/form-data for Content-Type
const form = document.querySelector('form')
const user = new FormData(form)
axios.post('/users', user)
Forms Forms with AJAX Andrew Smith 315 / 712
Uncontrolled Forms
multipart/form-data looks like this:
------WebKitFormBoundaryEsLOnUOv8QRFizS0
Content-Disposition: form-data; name="name"
Andrew Smith
------WebKitFormBoundaryEsLOnUOv8QRFizS0
Content-Disposition: form-data; name="email"
[email protected]
------WebKitFormBoundaryEsLOnUOv8QRFizS0--
Forms Forms with AJAX Andrew Smith 316 / 712
Uncontrolled Forms
You can read from elements manually in JS, reading value or checked
attributes:
textEl.value // 'Andrew'
checkboxEl.checked // true
Forms Forms with AJAX Andrew Smith 317 / 712
Submitting Forms AJAX
Controlled
Uncontrolled
• Good for complex data or UI
• Lots of event listeners,
• Good for simple forms or UI
• Can upload files
boilerplate
• Fewer “translation” errors
• Everything are strings
• Dynamic UI based on form
• Dynamic UI based on form
values is hard
values is easy
Forms Forms with AJAX Andrew Smith 318 / 712
Controlled Forms
If you’ve worked with React or Angular, this is probably what you used.
const [name, setName] = useState('')
const handleSubmit = () => {
axios.post('/users', { user: { name } })
}
return (
<form onSubmit={handleSubmit}>
<input type="text"
onChange={e => setName(e.target.value)} />
</form>
)
Forms Forms with AJAX Andrew Smith 319 / 712
Controlled Forms
• When an input is changed (i.e. keyboard input) it will fire an event
• The event is normally an HTMLInputEvent
• Most important thing, getting the input value:
• For input, select, etc.: event.target.value
• For checkbox/radio: event.target.checked
inputEl.addEventListener('change', (e) => {
console.log(e.target.value)
})
Forms Forms with AJAX Andrew Smith 320 / 712
Controlled Forms
In plain JS, controlled forms are pretty uncommon:
let name
let isAdmin
inputEl.addEventListener('change', (e) => {
name = e.target.value
})
checkboxEl.addEventListener('change', (e) => {
isAdmin = e.target.checked
})
Forms Forms with AJAX Andrew Smith 321 / 712
Uncontrolled Forms
FormData (link) offers easy API to access form values.
<form>
<input name="name" type="text" />
<input name="email" type="text" />
</form>
// js
const data = new FormData(formEl)
data.get('name') // Andrew
data.get('email') // [email protected]
Forms Forms with AJAX Andrew Smith 322 / 712
FormData
Disabled inputs are ignored
<form>
<input name="name" disabled="disabled" value="Andrew" />
</form>
const data = new FormData(formEl)
data.get('name') // null
Forms Forms with AJAX Andrew Smith 323 / 712
FormData
Unchecked checkboxes / radio buttons are ignored
<input type="checkbox" name="foo" value="fooVal">
<input type="checkbox" name="bar" value="barVal" checked>
const data = new FormData(formEl)
data.get('foo') // null
data.get('bar') // barVal
Forms Forms with AJAX Andrew Smith 324 / 712
FormData
Can manually set defaults for unchecked checkboxes:
<input type="checkbox" name="foo" value="1">
const data = new FormData(formEl)
data.set('foo', data.has('foo') ? '1' : '0')
data.get('foo') // '0'
Forms Forms with AJAX Andrew Smith 325 / 712
FormData
FormData appends values for keys encountered multiple times
<input type="text" name="foo" value="1">
<input type="text" name="foo" value="2">
const data = new FormData(formEl)
data.get('foo') // '1'
data.getAll('foo') // ['1', '2']
Forms Forms with AJAX Andrew Smith 326 / 712
FormData
Limitation: everything is a string!
<input type="number" name="foo" value="1">
const data = new FormData(formEl)
data.get('foo') // '1' <-- not the number 1
Only USVStrings are allowed (link)
Forms Forms with AJAX Andrew Smith 327 / 712
Manually setting values
const data = new FormData()
data.set('foo', 42)
data.get('foo') // '42'
Forms Forms with AJAX Andrew Smith 328 / 712
Sending Complex Data
With FormData, you follow a bracket convention established by PHP and
Rails:
user[name] translates to name key on user object
So it’ll look like this:
------WebKitFormBoundaryFEU9KykEhpGZ85xE
Content-Disposition: form-data; name="user[name]"
Andrew
Forms Forms with AJAX Andrew Smith 329 / 712
Sending Complex Data
Similar, fields ending with [] indicate array values.
interests[] would collect values into an array:
------WebKitFormBoundaryC1Q7SiFNBIEvO1AX
Content-Disposition: form-data; name="interests[]"
coding
------WebKitFormBoundaryC1Q7SiFNBIEvO1AX
Content-Disposition: form-data; name="interests[]"
tea
Leads to this on server: { interests: ['coding', 'tea'] }
Forms Forms with AJAX Andrew Smith 330 / 712
Sending Complex Data
Combining the two, you could have:
<input type="checkbox" name="user[interests][]"
value="coding" checked>
<input type="checkbox" name="user[interests][]"
value="reading">
<input type="checkbox" name="user[interests][]"
value="tea" checked>
And that would be interpreted as:
{ "user": { "interests": ["coding", "tea"] } }
Forms Forms with AJAX Andrew Smith 331 / 712
Sending Complex Data
• There is no standard for this, it’s just convention.
• Your server may interpret multipart/form-data arrays and objects
differently depending on your framework or parsing library
• For discussion, see: (Link)
Forms Forms with AJAX Andrew Smith 332 / 712
FormData
• Not all servers equipped to handle multipart/form-data
• Can’t handle non-string data types (including objects)
• Not as efficient as application/x-www-form-urlencoded (most
common, default form behavior)
• Could also translate to application/json for most flexibility
Forms Forms with AJAX Andrew Smith 333 / 712
FormData -> JSON
Naive approach:
const data = new FormData(formEl)
const jsonData = Object.fromEntries(data.entries())
Forms Forms with AJAX Andrew Smith 334 / 712
FormData -> JSON
But this overwrites keys encountered multiple times:
<input type="text" name="foo" value="1">
<input type="text" name="foo" value="2">
const data = new FormData(formEl)
const jsonData = Object.fromEntries(data.entries())
// { foo: '2' }
Forms Forms with AJAX Andrew Smith 335 / 712
FormData -> JSON
You could write your own:
<input type="text" name="foo" value="1">
<input type="text" name="foo" value="2">
const data = new FormData(formEl)
const jsonData = formDataToJs(data.entries())
// { foo: ['1', '2'] }
Example: (link)
Forms Forms with AJAX Andrew Smith 336 / 712
FormData -> JSON
Or be really special and be able to parse PHP/Rails-like arrays and object
syntax (link):
<input type="text" name="foo[]" value="1">
<input type="text" name="foo[]" value="2">
<input type="text" name="bar[baz]" value="yo">
const data = new FormData(formEl)
const jsonData = formDataToJs(data.entries())
// { foo: ['1', '2'], bar: { baz: 'yo' } }
. . . but you’d have to implement it yourself.
Forms Forms with AJAX Andrew Smith 337 / 712
FormData -> JSON
Takeaway: unless you need to, just send as multipart/form-data
Forms Forms with AJAX Andrew Smith 338 / 712
Recap
Controlled
• Source of truth: JS values
• send application/json
Uncontrolled
• Source of truth: form / string values
• send application/x-www-form-urlencoded (default)
• send multipart/form-data (FormData)
• send application/json (FormData -> JSON)
Forms Forms with AJAX Andrew Smith 339 / 712
Recap
Which do I choose?!
If you want the most control/flexibility, go with controlled
If you want less boilerplate but sometimes deal with weird shims, missing
checkboxes, type casting mistakes, etc., go with uncontrolled.
If you want to send a file, go with uncontrolled
Forms Forms with AJAX Andrew Smith 340 / 712
Uploads
Forms Uploads Andrew Smith 341 / 712
Uploads
Use the <input type="file"> element
Docs (link)
Forms Uploads Andrew Smith 342 / 712
Uploads
Add enctype="multipart/form-data to form, otherwise it won’t work
<form method="post" enctype="multipart/form-data">
<div>
<label for="file">Choose file to upload</label>
<input type="file" id="file" name="file">
</div>
<button>Submit</button>
</form>
Forms Uploads Andrew Smith 343 / 712
Uploads
Control file types with accept attribute (link):
• Comma separated
• Can be specific file types: .docx, .pdf
• Can be MIME types:
• image/*
• audio/*
• video/*
• Doesn’t work on some phones
<input type="file" id="file" name="file" accept="image/*">
Forms Uploads Andrew Smith 344 / 712
Uploads
Allow multiple with multiple attribute
<input type="file" id="file" name="file" multiple>
Forms Uploads Andrew Smith 345 / 712
File
• Get details about input: fileEl.files[0]
• name
• lastModified
• size (in bytes)
• type
Forms Uploads Andrew Smith 346 / 712
File
• Limiting filesize
• Listen to change events on input
• Can make UI updates when file.size is above a threshold
const oneMBInBytes = 1048576
if (file.size > oneMBInBytes) {
// too big
} else {
// just right
}
Forms Uploads Andrew Smith 347 / 712
Thumbnails
• The File object can be passed to URL.createObjectURL(file)
• Generates a DOMString you can assign to an img.src
const image = document.createElement('img')
image.src = URL.createObjectURL(file)
document.body.appendChild(image)
Forms Uploads Andrew Smith 348 / 712
FileList
• Grab files and turn into array: [...fileEl.files]
• Then you can iterate and interact with files
Forms Uploads Andrew Smith 349 / 712
Uploading with AJAX
• Build a FormData object and #append files into the appropriate key
• In this example, assuming the server accepts files uploaded on the
files field
const data = new FormData()
;[...fileInput.files].forEach((file) => {
data.append('files', file)
})
Forms Uploads Andrew Smith 350 / 712
Common mistakes
• accept isn’t perfect not supported everywhere (link)
• Always validate on server-side
• People upload HEIC and browsers don’t know what to do with them
• Restrict file sizes probably
Forms Uploads Andrew Smith 351 / 712
Resources
• Setting up upload server (link)
• Multer package for express file uploads (link)
Forms Uploads Andrew Smith 352 / 712
Demo
• See
http://localhost:3000/js/forms/upload-demo/upload.html
• (Link)
Forms Uploads Andrew Smith 353 / 712
Exercise
1. Visit http://localhost:3000/js/forms/upload.html
2. Open src/www/forms/uploads.html and
src/www/forms/uploads.js.
3. Run your server from src directory:
1. $ yarn start or $ npm start
Forms Uploads Andrew Smith 354 / 712
Inheritance: Classes
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 355 / 712
JS Gets class
• class keyword introduced in ES2015
• Provides syntactic sugar on actual JS inheritance system
• More familiar to most OOP programmers
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 356 / 712
Getting class-y
class Rectangle {
constructor(width, height) {
this.width = width
this.height = height
}
area() {
return this.width * this.height
}
}
const rect = new Rectangle(10, 20)
rect.area() // 200
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 357 / 712
Inheritance
class Square extends Rectangle {
constructor(width) {
super(width, width)
}
sideSize() {
return this.width
}
}
const sq = new Square(10)
sq.sideSize() // 10
sq.area() // 100
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 358 / 712
Static Methods
Values and functions that belong to the class, not instances.
class User {
static nextId = makeCounter()
constructor(name) {
this.id = User.nextId()
}
}
const user1 = new User('Andrew')
user1.id // 1
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 359 / 712
Static Methods
Values and functions that belong to the class, not instances.
class User {
static defaultProps = { name: 'USER' }
constructor(name) {
this.name = name || User.defaultProps.name
}
}
const user2 = new User()
user2.name // 'USER'
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 360 / 712
Static Methods
Sometimes used to store factories.
class Rectangle {
static with4To6Ratio = width => {
return new Rectangle(width, (width * 6) / 4)
}
// ...
}
const rect = Rectangle.with4To6Ratio(10)
rect.width // 10
rect.height // 15
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 361 / 712
Getters and Setters
class Car {
constructor() {
this._speed = 0
}
get speed() { return this._speed }
set speed(x) {
if (x < 0 || x > 100) throw 'Nope'
this._speed = x
}
}
const toyota = new Car()
toyota.speed = 101 // 'Nope'
toyota.speed = 50
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 362 / 712
Instance Fields
class Person {
constructor(name) { this.name = name }
getName = () => this.name
}
const person = new Person('Andrew')
person.getName() // 'Andrew'
const willNotLoseContext = person.getName
willNotLoseContext() // 'Andrew'
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 363 / 712
Instance Fields
• Enabled via @babel/plugin-proposal-class-properties
• Performance implications
• Mostly used in UI frameworks e.g. React
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 364 / 712
Instance Fields
Also lets you sometimes ditch the constructor:
class Counter {
static nextId = makeCounter()
id = Counter.nextId()
}
const counter = new Counter()
counter.id // 1
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 365 / 712
Private Fields
Privacy used to be indicated by convention:
class Message {
constructor(msg) {
this._message = msg
}
}
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 366 / 712
Private Fields
Stage 3 syntax now supports private fields:
class Counter {
#counter
constructor() {
this.#counter = 1
}
count() { return this.#counter++ }
}
const counter = new Counter()
counter.count() // 1
counter.count() // 2
counter.counter // undefined
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 367 / 712
Private Instance Fields
You can even make private instance fields!
class Counter {
#counter = 1
count() { return this.#counter++ }
}
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 368 / 712
Exercise
1. Open src/www/js/oop/classes.js
2. Make all tests pass
$ cd src
$ yarn jest classes.test.js --watch
Object-Oriented Programming (Classical) Inheritance: Classes Andrew Smith 369 / 712
Introspection and Reflection
Object-Oriented Programming (Classical) Introspection and Reflection Andrew Smith 370 / 712
Simple Introspection Techniques
• The instanceof Operator:
// Returns `true':
[1, 2, 3] instanceof Array;
• The Object.getPrototypeOf Function:
// Returns `Array.prototype':
Object.getPrototypeOf([1, 2, 3]);
• constructor property:
const foo = function foo() {}
const f = new foo()
f.constructor === foo
Object-Oriented Programming (Classical) Introspection and Reflection Andrew Smith 371 / 712
Object Mutability
Object-Oriented Programming (Classical) Object Mutability Andrew Smith 372 / 712
Passing Objects to Functions
JavaScript uses call by sharing when you pass arguments to a function:
const x = {color: "purple", shape: "round"};
function mutator(someObject) {
delete someObject.shape;
}
mutator(x);
console.log(x);
Produces:
{ color: 'purple' }
Object-Oriented Programming (Classical) Object Mutability Andrew Smith 373 / 712
Object.freeze
Object.freeze(obj);
assert(Object.isFrozen(obj) === true);
• Can’t add new properties
• Can’t change values of existing properties
• Can’t delete properties
• Can’t change property descriptors
Object-Oriented Programming (Classical) Object Mutability Andrew Smith 374 / 712
Object.seal
Object.seal(obj);
assert(Object.isSealed(obj) === true);
• Properties can’t be deleted, added, or configured
• Property values can still be changed
Object-Oriented Programming (Classical) Object Mutability Andrew Smith 375 / 712
Object.preventExtensions
Object.preventExtensions(obj);
• Prevent any new properties from being added
Object-Oriented Programming (Classical) Object Mutability Andrew Smith 376 / 712
ImmutableJS
Some libraries support immutability.
const { Map } = require('immutable')
const map1 = Map({ a: 1, b: 2, c: 3 })
const map2 = Map({ a: 1, b: 2, c: 3 })
map1.equals(map2) // true
map1 === map2 // false
We tried it but it’s painful.
Object-Oriented Programming (Classical) Object Mutability Andrew Smith 377 / 712
Moral of the Story
It’s possible to enforce immutability in code. . .
. . . but it’s painful.
. . . and it may be unreliable.
Better option: adopt immutable programming conventions as a team.
Object-Oriented Programming (Classical) Object Mutability Andrew Smith 378 / 712
The Prototype
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 379 / 712
Inheritance in JavaScript
• JavaScript doesn’t use classes, it uses prototypes
• Prototypal inheritance:
• Tends to be smaller
• Less redundant
• Can simulate classical inheritance as needed
• More powerful
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 380 / 712
Inheriting Properties from Other Objects
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 381 / 712
Setting Properties and Inheritance
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 382 / 712
Establishing Inheritance
Object.create creates a new object with the provided object as the
prototype.
const a = { color: 'red', speed: 100 }
const b = Object.create(a)
b.speed // 100
b.color = 'green'
const c = Object.create(b)
c.speed // 100
c.color // 'green'
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 383 / 712
Turtles All the Way Up
a
b c
color: "red"
... color: "green" width: 10
speed: 100
__proto__ __proto__
__proto__
Figure 1:Prototypes
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 384 / 712
Object.create
Object.create creates a new object and sets the __proto__ property.
const a = { color: 'red', speed: 100 }
const b = Object.create(a)
b.speed // 100
b.__proto__ === a // true
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 385 / 712
Turtles All the Way Up
const a = {}
const b = Object.create(a)
const c = Object.create(b)
c.__proto__ // b
b.__proto__ // a
a.__proto__ // Object.prototype
Object.prototype.__proto__ // null
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 386 / 712
Prototypes
• Every object has a __proto__ prototype
• The path of prototype objects is the prototype chain
• Properties not found on an object will be check up the prototype chain
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 387 / 712
Using __proto__
__proto__ is technically non-standard, but de-facto available
The “standard” is Object.getPrototypeOf()
Can be set after the fact with Object.setPrototypeOf()
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 388 / 712
Setting the Prototype
These are very different in performance:
const a = { color: 'green' }
// fast!
const b = Object.create(a)
// sloooooow
const c = {}
Object.setPrototypeOf(c, a)
c.color
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 389 / 712
The Buck Stops Here
You can check if an object (and not one of the prototypes) has the property:
const a = { color: 'green', speed: 100 }
const b = Object.create(a)
b.speed = 100
b.hasOwnProperty('speed') // true
b.hasOwnProperty('color') // false
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 390 / 712
Inheriting Behavior
Function properties further up the prototype chain refer to this the way
you’d expect.
Remember: it’s all about the calling context!
const a = {
name: 'Andrew',
getName() { return this.name },
}
const b = Object.create(a)
b.name = 'Foo'
b.getName() // 'Foo'
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 391 / 712
Exercise
1. Open src/www/js/oop/create.test.js
2. Follow directions in the it statements
3. All tests should pass
$ cd src
$ yarn jest create.test.js --watch
Object-Oriented Programming (Prototypal) The Prototype Andrew Smith 392 / 712
Constructor Functions
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 393 / 712
Constructor Functions
So, Object.create is great and all, but cumbersome.
You can’t set values on creation!
Thus, we have constructor functions.
const colorizer = function(color) {
this.color = color
this.hasColor = true
}
const z = new colorizer('green')
z.color // 'green'
z.hasColor // true
z.constructor === colorizer // true
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 394 / 712
Constructor Functions
Values set inside the constructor function aren’t shared across instances.
const colorizer = function() {
this.color = 'green'
}
const x = new colorizer()
const y = new colorizer()
x.color = 'blue'
x.color // 'blue'
y.color // 'green'
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 395 / 712
The new Operator
The new operator creates a new object, invokes the constructor function
with the new object as its execution context, and sets its __proto__
property to the function’s prototype property.
. . . Yikes.
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 396 / 712
The new Operator
Basically:
const foo = function(a, b, c) {/* ... */ }
let y = new foo(1, 2, 3)
// Is like:
let y = Object.create(foo.prototype)
y = foo.call(y, 1, 2, 3) || y
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 397 / 712
The new Operator
That’s why you can set this.whatever values and it gets set on the
instance.
const ageify = function(age) {
this.age = age
}
const x = new ageify(27)
x.age // 27
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 398 / 712
Function Prototypes
Functions get a prototype property
const f = function() {}
f.__proto__ === Function.prototype // true
Function.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 399 / 712
Function Prototypes
Fun fact:
arrow functions do not get a prototype property
const foo = () => {}
foo.prototype // undefined
Any guesses as to why?
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 400 / 712
Function Prototypes
Shared information goes on the function’s prototype property.
const person = function() {}
person.prototype.species = 'Homo sapien'
const instructor = new person()
instructor.__proto__ // person.prototype
instructor.species // 'Homo sapien'
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 401 / 712
Naming Convention
Constructor functions are generally given a capitalized name.
const Person = function() {}
const person = new Person()
The capitalization doesn’t give special behavior, it’s just convention.
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 402 / 712
Looking at Array Instances
Figure 2:Array and Array.prototype
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 403 / 712
The Prototype Chain
Figure 3:Prototypal Inheritance
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 404 / 712
Another Look at Array Instances
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 405 / 712
Constructor Functions and OOP
const Rectangle = function (width, height) {
this.width = width
this.height = height
}
Rectangle.prototype.area = function () {
return this.width * this.height
}
const rect = new Rectangle(10, 20)
rect.area() // 200
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 406 / 712
Exercise: Constructor Functions
1. Open src/www/js/oop/constructors.test.js
2. Follow directions in the it statements
3. All tests should keep passing
$ cd src
$ yarn jest constructors.test.js --watch
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 407 / 712
Inheritance in Constructor Functions
We want:
f2Instance --> f2.prototype ---> f1.prototype
Set f2’s prototype to have a __proto__ of f1.prototype
Confused yet?
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 408 / 712
Inheritance in Constructor Functions
const foo = function() {}
foo.prototype.fooProp = 'foo prop'
const bar = function() {}
bar.prototype = Object.create(foo.prototype)
const b = new bar()
b.fooProp
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 409 / 712
Inheritance in Constructor Functions
You can also implement super behavior in the constructor:
const Super = function (a) {
this.a = a
}
const Sub = function (a, b) {
Super.call(this, a)
this.b = b
}
const sub = new Sub(1, 2)
sub // Sub { a: 1, b: 2 }
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 410 / 712
Inheritance in Constructor Functions
const Square = function (width) {
Rectangle.call(this, width, width)
}
Square.prototype = Object.create(Rectangle.prototype)
Square.prototype.sideSize = function () {
return this.width
}
const sq = new Square(10)
sq.area() // 100
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 411 / 712
Generic Functions (Static Class Methods)
Functions that are defined as properties of the constructor function are
known as generic functions:
Rectangle.withWidth = function (width) {
return new Rectangle(width, width)
}
const rect = Rectangle.withWidth(10)
rect.area() // 100
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 412 / 712
Property Descriptors
Setting property descriptors:
Object.defineProperty(obj, propName, definition);
• Define (or update) a property and its configuration
• Some things that can be configured:
• enumerable: If the property is enumerated in for .. in loops
• value: The property’s initial value
• writable: If the value can change
• get: Function to call when value is accessed
• set: Function to call when value is changed
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 413 / 712
Property Getters and Setters
function Car() {
this._speed = 0;
}
Object.defineProperty(Car.prototype, "speed", {
get: function() { return this._speed; },
set: function(x) {
if (x < 0 || x > 100) throw "I don't think so";
this._speed = x;
}
});
let toyota = new Car();
toyota.speed = 55; // Calls the `set' function.
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 414 / 712
Object-Oriented Programming: Gotcha
What’s wrong with the following code?
function Parent(children) {
this.children = []
children.forEach(function (name) {
if (name.match(/\S/)) {
this.children.push(name)
}
})
}
const p = new Parent(['Peter', 'Paul', 'Mary'])
// Error: Cannot read property `children` of undefined
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 415 / 712
Accessing this via the bind Function
Notice where bind is used:
function ParentWithBind(children) {
this.children = []
// Add children that have valid names:
children.forEach(function (name) {
if (name.match(/\S/)) {
this.children.push(name)
}
}.bind(this))
}
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 416 / 712
Accessing this via a Closure Variable
Create an alias for this:
function ParentWithAlias(children) {
let self = this
this.children = []
// Add children that have valid names:
children.forEach(function (name) {
if (name.match(/\S/)) {
self.children.push(name)
}
})
}
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 417 / 712
Accessing this Directly via ES2015 Arrow Functions
Using the ES2015 arrow function syntax:
function ParentWithArrow(children) {
this.children = []
// Add children that have valid names:
children.forEach(name => {
if (name.match(/\S/)) {
this.children.push(name)
}
})
}
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 418 / 712
Exercise
1. Open src/www/js/oop/fn-inheritance.js
2. Open src/www/js/oop/fn-inheritance.test.js
3. Follow directions in the it statements
4. All tests should pass
$ cd src
$ yarn jest fn-inheritance.test.js --watch
Object-Oriented Programming (Prototypal) Constructor Functions Andrew Smith 419 / 712
Pure Functions
Functional Programming part 2 Pure Functions Andrew Smith 420 / 712
Pure Functions
1. Total
2. Deterministic
3. No observable side-effects
Functional Programming part 2 Pure Functions Andrew Smith 421 / 712
Total
• One input, one output
const double = (x) => {
if (x === 1) return 2 const double = (x) => {
if (x === 2) return 4 return x * 2
if (x === 3) return 6 }
}
Functional Programming part 2 Pure Functions Andrew Smith 422 / 712
Deterministic
Generally, does not refer to data outside the closure
let sawTwo = false
const doubleTrouble = (x) => {
if (x === 2) sawTwo = true
return sawTwo ? x * 4 : x * 2
}
// vs...
const doubleTrouble = (x, sawTwo) => {
return sawTwo ? x * 4 : x * 2
}
Functional Programming part 2 Pure Functions Andrew Smith 423 / 712
No side effects
• No network calls, DOM updates, console.logs
const addUser = (user, users) => {
console.log(`Saving user ${user.name}`)
api.saveUser(user)
return users.concat(user)
}
// vs...
const addUser = (user, users) => {
return {
state: users.concat(user),
log: `Saving user ${user.name}`,
network: () => { api.saveUser(user) }
}
}
Functional Programming part 2 Pure Functions Andrew Smith 424 / 712
No side effects
• No mutation
const makeDone = (todo) => {
todo.done = true
}
// vs...
const markDone = (todo) => {
return { ...todo, done: true }
}
Functional Programming part 2 Pure Functions Andrew Smith 425 / 712
Immutability
• Avoid any assignment on a dot or bracket accessor
const nums = [1, 2, 3]
nums[1] = 5 // nope
const obj = { a: 1 }
obj.a = 2 // nope
Functional Programming part 2 Pure Functions Andrew Smith 426 / 712
Immutability
• Avoid these Array methods without copying first:
• push / pop
• shift / unshift
• splice
• sort
• reverse
• Avoid these Object methods without copying first:
• assign
Functional Programming part 2 Pure Functions Andrew Smith 427 / 712
Purity Tests
const haveBirthday = (user) => {
user.age += 1
return user
}
Functional Programming part 2 Pure Functions Andrew Smith 428 / 712
Purity Tests
const isOnline = (id) => {
return api.get('/users/${id}')
.then(({ data }) => {
return data.status === 'online'
})
}
Functional Programming part 2 Pure Functions Andrew Smith 429 / 712
Purity Tests
const selectorText = (selector) => {
return document
.querySelector(selector)
.innerText
}
Functional Programming part 2 Pure Functions Andrew Smith 430 / 712
What’s the point
• Easy to test
• Easy to reason about
• No hidden state
• Functional core, imperative shell
Functional Programming part 2 Pure Functions Andrew Smith 431 / 712
Declarative Programming
Functional Programming part 2 Declarative Programming Andrew Smith 432 / 712
Tell me What not How
• “Imperative” style focuses on how
• “Declarative” style focuses on what
Functional Programming part 2 Declarative Programming Andrew Smith 433 / 712
Tell me What not How
Imperative:
const words = ['hello', 'world']
const loudWords = []
for (let i = 0; i < words.length; i++) {
const capitalized = words[i].toUpperCase()
loudWords.push(capitalized)
}
Declarative:
const words = ['hello', 'world']
const loudWords = words.map(x => x.toUpperCase())
Functional Programming part 2 Declarative Programming Andrew Smith 434 / 712
Tell me What not How
Building blocks of declarative array processing:
• map
• reduce
• filter
• some
Q: what does each one do?
Functional Programming part 2 Declarative Programming Andrew Smith 435 / 712
Tell me What not How
Building blocks of declarative array processing:
• map - 1:1 transformation
• reduce - many:1 transformation
• filter - many:some transformation
• some - does any meet condition?
Q: what does each one do?
Functional Programming part 2 Declarative Programming Andrew Smith 436 / 712
Tell me What not How
• Focus on composition of functions
• Express complex transformations of data or business logic
const isNil = x => x === 'undefined' || x === null
const hasProfile = user => !isNil(user.profile)
const usersWithProfiles = users => users.filter(hasProfile)
const profilesPresent = users => users.some(hasProfile)
const receiveUsers = (users) => {
if (profilesPresent(users)) {
const profileUsers = usersWithProfiles(users)
// ...
}
}
Functional Programming part 2 Declarative Programming Andrew Smith 437 / 712
Tell me What not How
• Express intent with named functions
• Can reduce cognitive load reading anonymous functions
const isEven = x => x % 2 === 0
const double = x => x * 2
const doubleEvenStrings = xs =>const
( toString = x => x.toString
xs.filter(x => x % 2 === 0)
.map(x => x * 2) const doubleEvenStrings = xs =>
.map(x => x.toString()) xs.filter(isEven)
) .map(double)
.map(toString)
)
Functional Programming part 2 Declarative Programming Andrew Smith 438 / 712
Tell me What not How
Imperative style:
const doStuff = (str) => { const filtered = []
const lower = str.toLowerCase()
const words = lower.split(' ') for (let i in words) {
if (words[i].length > 3) {
words.reverse() keepers.push(words[i])
}
for (let i in words) { }
words[i] = words[i].trim()
} return keepers.join('')
// ... }
Functional Programming part 2 Declarative Programming Andrew Smith 439 / 712
Tell me What not How
Declarative style:
const doStuff = xs => (
xs
.toLowerCase()
.split(' ')
.map(x => x.trim())
.reverse()
.filter(x => x.length > 3)
.join('')
)
Functional Programming part 2 Declarative Programming Andrew Smith 440 / 712
Resources
• MDN docs: (Link)
Functional Programming part 2 Declarative Programming Andrew Smith 441 / 712
Exercise
1. Open src/www/js/functional/declarative.test.js
2. Follow test descriptions and keep the tests passing
Functional Programming part 2 Declarative Programming Andrew Smith 442 / 712
Currying
Functional Programming part 2 Currying Andrew Smith 443 / 712
Currying
• More than a delicious food
• Developed by Haskell Curry
• Allows a function to have arguments provided at different times
Functional Programming part 2 Currying Andrew Smith 444 / 712
Currying
const add = (a, b, c) => a + b + c
If you didn’t provide all the arguments, it still runs:
add(1) // NaN
Functional Programming part 2 Currying Andrew Smith 445 / 712
Currying
Curried functions wait until the rest of arguments are provided:
const curriedAdd = curry(add) // function
const add1 = curriedAdd(1) // function
const add1And4 = add1(4) // function
const add1And4And5 = add(5) // 9
More generally:
x = f(a, b, c)
x = g(a)(b)(c)
Functional Programming part 2 Currying Andrew Smith 446 / 712
Currying
Let’s see the curried add function without the intermediate variables:
add(1)(2)(3) // 6
add(1, 2)(3) // 6
add(1)(2, 3) // 6
add(1, 2, 3) // 6
Functional Programming part 2 Currying Andrew Smith 447 / 712
Currying
• A very simple implementation, only handles 2 args:
const curry = f => x => y => f(x, y)
Functional Programming part 2 Currying Andrew Smith 448 / 712
Currying
• This way you can create specialized functions
const modulo = curry((x, y) => y % x)
const isOdd = modulo(2)
isOdd(3) // 1, truthy
isOdd(2) // 0, falsey
Functional Programming part 2 Currying Andrew Smith 449 / 712
Currying
• Create expressive transformation functions
const isOdd = modulo(2)
const filter = curry((f, xs) => xs.filter(f))
const getOdds = filter(isOdd)
getOdds([1, 2, 3]) // [1, 3]
Functional Programming part 2 Currying Andrew Smith 450 / 712
Currying
• The data it operates on comes last
const filter = curry((xs, f) => xs.filter(f))
const getOdds = xs => filter(xs, isOdd) // :-(
// versus...
const filter = curry((f, xs) => xs.filter(f))
const getOdds = filter(isOdd) // :-)
Functional Programming part 2 Currying Andrew Smith 451 / 712
Currying
• Default parameters fill in slots
const add = curry((a, b) => a + b)
add(1) // f
const add = curry((a, b = 5) => a + b)
add(1) // 6
add(1, 3) // 4
add(1)(3) // Error: not a function
Functional Programming part 2 Currying Andrew Smith 452 / 712
Exercise
1. Open src/www/js/functional/currying.test.js
2. Follow prompts
Functional Programming part 2 Currying Andrew Smith 453 / 712
Ramda
Functional Programming part 2 Ramda Andrew Smith 454 / 712
Ramda: The Functional JS Toolkit
• RamdaJS (link) gives you many powerful expressions
• Every function is curried by default
• Like Lodash but better
Functional Programming part 2 Ramda Andrew Smith 455 / 712
Ramda: The Functional JS Toolkit
$ npm install --save ramda
$ yarn add ramda
You can mess with a REPL: (link)
Functional Programming part 2 Ramda Andrew Smith 456 / 712
Ramda: The Functional JS Toolkit
• All your usual functions are there: map, reduce, any (aka some),
filter
• BUT, data always goes last
const add3ToAll = R.map(R.add(3))
add3ToAll([1, 2, 3]) // [4, 5, 6
Functional Programming part 2 Ramda Andrew Smith 457 / 712
Usage
• Import everything as R:
import * as R from 'ramda'
R.map(R.prop('name'))
• Or import just what you need
import { map, prop } from 'ramda'
map(prop('name'))
Functional Programming part 2 Ramda Andrew Smith 458 / 712
Comparison with Lodash
• Similar, but putting the data at the end
R.map(R.double, [1, 2, 3])
_.map(_.double, [1, 2, 3])
• Lodash does not auto-curry
Functional Programming part 2 Ramda Andrew Smith 459 / 712
Composition
Functional Programming part 2 Composition Andrew Smith 460 / 712
Composition
• We often do transformations as a sequence of actions
const shout = str => str.toUpperCase()
const sentence = words => words.join(' ')
shout(sentence(['hello', 'world'])) // 'HELLO WORLD'
• First we make the array a sentence, and then shout it
Functional Programming part 2 Composition Andrew Smith 461 / 712
Composition
• compose: A way to say, “do this AFTER you do this” to some data
f(g(x)) // => run "f" on the output from "g(x)"
const compose = (f, g) => x => f(g(x))
const shoutSentence = compose(shout, sentence)
shoutSentence(['hello', 'world']) // 'HELLO WORLD
Functional Programming part 2 Composition Andrew Smith 462 / 712
Composition
• Compose the functions
• Pass in data as a separate invocation
compose(shout, sentence)(['foo', 'bar']) // yep
compose(shout, sentence, ['foo', 'bar']) // nope
Functional Programming part 2 Composition Andrew Smith 463 / 712
Composition
• Reads “right to left”
R.compose(R.reverse, R.join(' '))(['hello', 'world'])
// 'dlrow olleh'
R.compose(R.join(' '), R.reverse)(['hello', 'world'])
// 'world hello'
Functional Programming part 2 Composition Andrew Smith 464 / 712
Composition
• With Ramda compose you can combine an arbitrary number of
transformations
const shout = str => str.toUpperCase()
const sentence = words => words.join(' ')
const reverse = xs => [...xs].reverse()
const reversedLoudSentence = compose(reverse, shout, sentence)
reversedLoudSentence(['hello', 'world']) // 'WORLD HELLO'
Functional Programming part 2 Composition Andrew Smith 465 / 712
Composition
• Commonly used in conjunction with map, filter, etc.
const lt = curry((a, b) => a < b)
const length = x => x.length
const longerWords = R.filter(compose(lt(3), length))
longerWords(['foo', 'bar', 'waffles']) // ['waffles']
Functional Programming part 2 Composition Andrew Smith 466 / 712
Flip
• When the arguments are not in the order you want them, flip them!
const echo = R.compose(R.join(', '), R.repeat)
echo('Hello', 3) // "Hello, Hello, Hello"
const flippedEcho = R.flip(echo)
const echo3Times = flippedEcho(3)
echo3Times('Hello') // "Hello, Hello, Hello"
Functional Programming part 2 Composition Andrew Smith 467 / 712
Pipe
• Like compose but rights left-to-right
• Compose functions, pass in data as separate invocation
const doStuff = pipe(
x => x.toLowerCase(),
x => x.split(' '),
xs => xs.map(x => x.trim()),
xs => [...xs].reverse(),
xs => xs.filter(x => x.length > 3),
xs => xs.join(''),
)
Functional Programming part 2 Composition Andrew Smith 468 / 712
Pipe
• Like compose but rights left-to-right
const doStuff = R.pipe(
R.toLower,
R.split(' '),
R.map(R.trim),
R.reverse,
R.filter(R.compose(R.lt(3), R.length)),
R.join(''),
)
console.log(doStuff('dog Animal Bird cat DINOSAUR'))
// dinosaurbirdanimal
Functional Programming part 2 Composition Andrew Smith 469 / 712
Pipe
Functional Programming part 2 Composition Andrew Smith 470 / 712
Pipe
Imperative style:
const doStuff = (str) => { const filtered = []
const lower = str.toLowerCase()
const words = lower.split(' ') for (let i in words) {
if (words[i].length > 3) {
words.reverse() keepers.push(words[i])
}
for (let i in words) { }
words[i] = words[i].trim()
} return keepers.join('')
// ... }
Functional Programming part 2 Composition Andrew Smith 471 / 712
Exercise
1. Open src/www/js/functional/composition.test.js
2. Make / keep all tests passing
Functional Programming part 2 Composition Andrew Smith 472 / 712
Point-Free Programming
Functional Programming part 2 Point-Free Programming Andrew Smith 473 / 712
Point-Free
• Basically, writing functions without . accessors or anonymous functions
// not point-free
const isOdd = x => R.modulo(x, 2) === 1
const getOdds = xs => xs.filter(isOdd)
// point-free
const isOdd = R.compose(R.equals(1), R.flip(R.modulo)(2))
const getOdds = R.filter(isOdd)
Functional Programming part 2 Point-Free Programming Andrew Smith 474 / 712
Point-Free
• It can be like code golf: clever, but not always legible
• Let’s say you have a list of objects and want to hydrate IDs
const objectsList = [{ id: 1 }, { id: 2 }]
const ids = [1] // turn this into subset of `objectsList`
Functional Programming part 2 Point-Free Programming Andrew Smith 475 / 712
Point-Free
• With an arrow function it’s pretty easy:
ids.map(id => R.find(R.propEq('id', id), objectsList))
• Without, it’s a bit harder to read:
R.map(R.compose(R.flip(R.find)(objectsList), R.propEq('id')),
Functional Programming part 2 Point-Free Programming Andrew Smith 476 / 712
Point-Free
• Use your judgment. Try writing it point-free to practice.
• If it’s too cumbersome, write it with an arrow function.
• Even the authors of Ramda say this (link)
• Understandable >> clever
Functional Programming part 2 Point-Free Programming Andrew Smith 477 / 712
Package Management
Developer Tools Package Management Andrew Smith 478 / 712
Node.js
• JavaScript runtime outside the browser
• Now you can run JavaScript on a server or in your terminal!
• Write whatever you want in JavaScript: servers, CLI, etc.
• Most web development tools these days are written in JavaScript
Developer Tools Package Management Andrew Smith 479 / 712
npm
• Repository of JavaScript packages: libraries, frameworks, and tools
• Manages packages for your project
• Tool to publish new packages to share with the world
• Run scripts or build processes
• 1.2M+ packages available
Developer Tools Package Management Andrew Smith 480 / 712
yarn
• Sprouted as competitor to npm
• Faster, more features, and more natural syntax
Developer Tools Package Management Andrew Smith 481 / 712
Which Should I Use?
• Both are great
• Pick one and stick with it across the project
• My experience: yarn is ~30% better
Developer Tools Package Management Andrew Smith 482 / 712
Managing Packages
• You specify your project’s packages with package.json
• dependencies: packages needed in production
• devDependencies: packages needed for local development
• engines: Specify which Node / npm is required
• Uses semver
Developer Tools Package Management Andrew Smith 483 / 712
Adding Packages
• Create your package.json
• Add dependencies: yarn add ...
• Add devDependencies: yarn add -D ...
• Or add an entry in your package.json
Developer Tools Package Management Andrew Smith 484 / 712
Installing and Managing Packages
• Install: yarn
• This creates a yarn.lock lockfile
• A lockfile ensures subsequent installs use the same exact packages
Developer Tools Package Management Andrew Smith 485 / 712
Starting Over
rm -rf node_modules && yarn
Developer Tools Package Management Andrew Smith 486 / 712
Upgrading Packages
Two approaches to upgrade within semver ranges:
• ALL THE THINGS: yarn upgrade
• Selective: yarn upgrade ...
• Interactive: yarn upgrade-interactive
Developer Tools Package Management Andrew Smith 487 / 712
Upgrading Packages
Upgrading packages outside semver ranges:
• yarn upgrade --latest
• yarn upgrade-interactive --latest
• Update semver number in package.json
• Flip the table: rm yarn.lock && yarn
Developer Tools Package Management Andrew Smith 488 / 712
Upgrading Transitive Dependencies
• Go to entry in yarn.lock and delete it
• yarn
Developer Tools Package Management Andrew Smith 489 / 712
Running Things
• Run with yarn [script|binary]
• scripts: write CLI commands in package.json
• Package binaries (e.g. babel, jest)
Developer Tools Package Management Andrew Smith 490 / 712
Running Things
In package.json:
"scripts": {
"say-hello": "echo 'Hello world'",
},
Then in terminal:
$ yarn say-hello
yarn run v1.22.4
$ echo 'Hello world'
Hello world
Done in 0.07s.
Developer Tools Package Management Andrew Smith 491 / 712
Local Development w/ Dependencies
• Say your project depends on a package you also change a lot
• What happens when you need changes?
• Or you’re debugging an issue with the package that manifests in your
project
Developer Tools Package Management Andrew Smith 492 / 712
Local Development w/ Dependencies
• You could go into node_modules/... and write in there
• What are some dangers with this approach?
Developer Tools Package Management Andrew Smith 493 / 712
Local Development w/ Dependencies
• Better: “link” the package
• yarn link: make this package linkable in other projects
• yarn link [package]: link in the package you want to use for the
current project
Developer Tools Package Management Andrew Smith 494 / 712
Local Development w/ Dependencies
Say you had an issue with react, assuming it’s a sibling of your project:
$ cd react
$ yarn link # makes this package available for linking
$ cd ../my-project
$ yarn link react # link in the package
Developer Tools Package Management Andrew Smith 495 / 712
Clearing the Cache
• Yarn caches downloaded packages for faster installs
• Clear it with yarn cache
Developer Tools Package Management Andrew Smith 496 / 712
Linting Tools
Developer Tools Linting Tools Andrew Smith 497 / 712
Introduction to Linting Tools
• Linting tools parse your source code and look for problems
• The two most popular linters for JavaScript are [JSLint][] and [ESLint][]
• ESLint is about 3x more popular than JSLint
Developer Tools Linting Tools Andrew Smith 498 / 712
About ESLint
• Integrates with most text editors via plugins
• Fully configurable, easy to add custom rules
• Enforce project style guidelines
Developer Tools Linting Tools Andrew Smith 499 / 712
Using ESLint Manually
$ yarn add eslint
$ yarn eslint yourfile.js
Developer Tools Linting Tools Andrew Smith 500 / 712
ESLint Plugins
Plugins available for popular editor environments:
• Webstorm
• Visual Studio Code
• vim
• etc. . .
• Official Integration List
Developer Tools Linting Tools Andrew Smith 501 / 712
Babel
Developer Tools Babel Andrew Smith 502 / 712
Babel: Basics
Developer Tools Babel: Basics Andrew Smith 503 / 712
Before There Was Babel. . .
• Each browser implements JavaScript specification
• New syntax and features show up:
• Arrow functions () => {}
• Spread syntax { ...obj, foo: 'bar' }
• . . . etc.
Developer Tools Babel: Basics Andrew Smith 504 / 712
Every Developer Be Like
Developer Tools Babel: Basics Andrew Smith 505 / 712
Every Browser Be Like
• Chrome, Firefox: Go For It!
• Safari: Maybe I’ll Do This
• IE: I’ll never do this
Developer Tools Babel: Basics Andrew Smith 506 / 712
Browser Compatibility
If you want to support older browsers, you must use "older" JavaScript
Developer Tools Babel: Basics Andrew Smith 507 / 712
Babel
• Write fancy new JavaScript
• Transpiles into “older” JavaScript syntax
• Polyfills missing functionality
• Includes presets to convert from one form of JavaScript to another
• ES2015+ to ES5
• React’s JSX files to ES5
• Vue’s VUE files to ES5
• etc.
Developer Tools Babel: Basics Andrew Smith 508 / 712
What is Babel
• Automated JavaScript restructuring, refactoring, and rewriting
• Parses JavaScript into an Abstract Syntax Tree (AST)
• The AST can be manipulated in JavaScript
Developer Tools Babel: Basics Andrew Smith 509 / 712
Plugins Galore
• Community plugins to automatically transform your code
• Compile TypeScript / Flow -> JavaScript
• Manage feature flags
• Improve module resolution
• Augment language behavior (e.g. implicit returns)
Developer Tools Babel: Basics Andrew Smith 510 / 712
What Does Transpiling Look Like?
Given add, using modern default parameters. . .
// add.js
export const add = (a = 1, b = 2) => a + b
Produces. . .
var add = function add() {
var a = arguments.length > 0 && arguments[0] !== undefined
? arguments[0] : 1;
var b = arguments.length > 1 && arguments[1] !== undefined
? arguments[1] : 2;
return a + b;
};
Which can be read by older browsers!
Developer Tools Babel: Basics Andrew Smith 511 / 712
Adding Babel
$ yarn add -D @babel/core @babel/cli @babel/preset-env
Add a babel.config.js at your root:
module.exports = {
presets: ['@babel/env']
}
Developer Tools Babel: Basics Andrew Smith 512 / 712
Babel Transpiling
$ yarn babel file.js
This transpiles a file and prints it to your console.
Developer Tools Babel: Basics Andrew Smith 513 / 712
Babel Transpiling
$ yarn babel src/add.js
Prints out:
var add = function add() {
var a = arguments.length > 0 && arguments[0] !== undefined
? arguments[0] : 1;
var b = arguments.length > 1 && arguments[1] !== undefined
? arguments[1] : 2;
return a + b;
};
Developer Tools Babel: Basics Andrew Smith 514 / 712
Integrating Babel with Your Build Tools
Integrate with Webpack (discussed next) for automatic transpiling
Developer Tools Babel: Basics Andrew Smith 515 / 712
Babel: Configuration
Developer Tools Babel: Configuration Andrew Smith 516 / 712
Configuration
babel.config.js: config file for Babel
module.exports = {
/* ... */
}
Developer Tools Babel: Configuration Andrew Smith 517 / 712
Presets
Collection of Babel plugins for common environments
Most common ones:
• @babel/preset-env: Transform for browser compatibility
• @babel/preset-flow: Handle Flow type annotations
• @babel/preset-react: Plugins for React development
• @babel/preset-typescript: Handle TypeScript
Developer Tools Babel: Configuration Andrew Smith 518 / 712
preset-env: Browser Compatibility
• Add @babel/preset-env
• Specify your browser targets for compatibility, or use defaults
• Managed with browserslist configuration
• “only last 2 major versions”
• “no IE”
• “>0.5% market share”
Developer Tools Babel: Configuration Andrew Smith 519 / 712
preset-env: Browser Compatibility
Just the defaults
module.exports = {
presets: ['@babel/env']
}
Developer Tools Babel: Configuration Andrew Smith 520 / 712
preset-env: Browser Compatibility
More specific:
[
'@babel/env',
{
targets: '>0.25%, safari >= 8, not ie 11',
},
]
Developer Tools Babel: Configuration Andrew Smith 521 / 712
preset-env: Handling Polyfills
Three options for useBuiltIns:
{
targets: '...',
useBuiltIns: 'entry' | 'usage' | false,
},
Developer Tools Babel: Configuration Andrew Smith 522 / 712
preset-env: Handling Polyfills
entry: Add polyfills to entry file, Babel pulls out those actually
needed
usage: Babel adds polyfills in files where they’re needed
(RECOMMENDED)
false: Don’t add polyfills automatically
Developer Tools Babel: Configuration Andrew Smith 523 / 712
preset-env: Handling Modules
modules: allows you to change how modules are transformed:
• commonjs, amd, umd, etc. . .
RECOMMENDED: false
Don’t transform; Webpack will take care of it for you
Developer Tools Babel: Configuration Andrew Smith 524 / 712
Other Handy Plugins
In addition to preset-env I use:
• preset-react
• Transforms React
• preset-flow (we use Flow)
• Write Flow
• plugin-proposal-class-properties
• Class properties
• plugin-proposal-object-rest-spread
• Object rest / spread syntax: { ...obj }
• plugin-syntax-dynamic-import
• Dynamic imports: () => import('./file')
Developer Tools Babel: Configuration Andrew Smith 525 / 712
Other Plugins
Many plugins to allow experimental syntax from TC39 proposals:
• Do expressions
• Decorators
• Nullish coalescing operating
• Pipeline operator
• Private methods
• etc.
Plugins let you play with the latest JS syntax experiments.
Some of them are really cool.
Also, this is risky. Some proposals may never land in the JS spec.
Developer Tools Babel: Configuration Andrew Smith 526 / 712
Webpack
Developer Tools Webpack Andrew Smith 527 / 712
Before There Was Webpack. . .
Say you had three JS files:
// add.js
const add = (a, b) => a + b
// subtract.js
const subtract = (a, b) => a - b
// index.js
// HOW DO YOU GET add AND subtract?!
console.log('1 + 2 =', add(1, 2))
console.log('2 - 1 =', subtract(2, 1))
Developer Tools Webpack Andrew Smith 528 / 712
Before There Was Webpack. . .
<script src="./add.js" />
<script src="./subtract.js" />
<script src="./index.js" />
Prints out:
1 + 2 = 3 index.js:2
8 - 2 = 6 index.js:3
Developer Tools Webpack Andrew Smith 529 / 712
But This Pollutes Global Scope
typeof(add) // "function"
typeof(subtract) // "function"
Developer Tools Webpack Andrew Smith 530 / 712
Also, Order Mattered
<script src="./index.js" />
<script src="./add.js" />
<script src="./subtract.js" />
Prints out:
index.js:2 Uncaught ReferenceError: add is not defined
Developer Tools Webpack Andrew Smith 531 / 712
Prior “Art”
• Wrap everything in IIFEs for protection
• Concatenate all your JS into a single file
• Hope every individual file was authored correctly
Or. . .
• Roll your own module system
Developer Tools Webpack Andrew Smith 532 / 712
Eventually That Became. . .
Concat All The Things: Grunt, Gulp, Broccoli
Modules: Browserify
Developer Tools Webpack Andrew Smith 533 / 712
Then Along Came Webpack
And showed everyone how it’s done.
Developer Tools Webpack Andrew Smith 534 / 712
What is Webpack?
• A build tool for web applications
• Bundles all JavaScript files into a single, browser-safe JS file
• Can be configured to process files as they’re imported
• Transpiling JS, linting, sizing images, etc.
• Bring content into JS files (CSS, images, JSON, etc.)
Developer Tools Webpack Andrew Smith 535 / 712
Other Benefits
• Dev Server + Live Reloading
• Hot Module Replacement
• Source Maps
• Caching
• Code Splitting + Lazy-loading
• Optimization (minification, tree-shaking, chunking)
Developer Tools Webpack Andrew Smith 536 / 712
Bundling JavaScript Modules
Webpack will:
1. Start with your “entry” JavaScript file
2. Follow all import and require statements
3. Generate a single file containing all JavaScript
The generated file is know as a bundle.
Developer Tools Webpack Andrew Smith 537 / 712
Bundling JS
// index.js
import { subtract } from './subtract'
const add = (a = 1, b = 2) => a + b
console.log(add(1, 2), subtract(8, 2))
// subtract.js
export const subtract = (a, b) => a - b
Developer Tools Webpack Andrew Smith 538 / 712
Bundle Layout
Developer Tools Webpack Andrew Smith 539 / 712
Bundling Images
import logo from './logo.jpg'
const component = () => {
const element = document.createElement('div')
const webpackLogo = new Image()
webpackLogo.src = logo
element.appendChild(webpackLogo)
return element
}
Developer Tools Webpack Andrew Smith 540 / 712
Bundling Images
Developer Tools Webpack Andrew Smith 541 / 712
Bundling Stylesheets
Now you can have localized stylesheets that connect to your JS.
Importing SCSS or CSS. . .
// index.js
import './index.scss'
Developer Tools Webpack Andrew Smith 542 / 712
Bundling Stylesheets
Can inject style sheets directly into the DOM for you.
/***/ "...css-loader/dist/cjs.js!./src/index.scss":
...
eval("// ..."\\n#root {\\n background-color: lightblue;\\n}
...# sourceURL=webpack:///./src/index.scss...);
Other performance optimizations are available.
Developer Tools Webpack Andrew Smith 543 / 712
CSS Modules
Webpack will even help with CSS modules, if that’s your jam.
/* style.css */
.className {
color: green;
}
import styles from "./style.css"
element.innerHTML = '<div class="' + styles.className + '">'
Developer Tools Webpack Andrew Smith 544 / 712
More Power Through Loaders
Webpack becomes a full build tool via loaders.
babel-loader Transpiles JavaScript using Babel
file-loader Load files (JPG, PNG, etc.)
css-loader Process CSS
sass-loader Process and bundle Sass
eslint-loader Lints JavaScript using ESLint
html-loader Bundle HTML templates
Developer Tools Webpack Andrew Smith 545 / 712
Configuring Webpack
Configuration file: webpack.config.js
• entry: Tell Webpack what file is the main JavaScript file
• output: Tell where to put the bundled assets
• module.rules: Specify which files go through which loaders. Each
rule takes
• test: regex to see if it applies to the file
• loader: what loaders to user
Developer Tools Webpack Andrew Smith 546 / 712
Example Module Rules: JS Transpiling
yarn add babel-loader
module: {
rules: [
{
test: /\.(js|jsx)$/, // matches JS or JSX
exclude: /(node_modules)/, // skip node_modules
loader: 'babel-loader' // run through babel-loader
},
]
},
Developer Tools Webpack Andrew Smith 547 / 712
Should I Transpile node_modules?
• Don’t transpile your node_modules, it’ll slow your build 10x
• Transpiling (can) guarantee browser compatibility
• npm libraries usually transpile to ES5 (basically, IE 11)
• That’s not always the case anymore
• Suss out non-ES5-friendly libraries with are-you-es5
• npx are-you-es5 check /your/repo
Developer Tools Webpack Andrew Smith 548 / 712
Example Module Rules: CSS
yarn add style-loader css-loader
module: {
rules: [
{
test: /\.s?css$/,
use: ["style-loader", "css-loader"]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: ['file-loader'],
}
]
},
Developer Tools Webpack Andrew Smith 549 / 712
Example Module Rules: Images
yarn add file-loader
module: {
rules: [
{
test: /\.(png|svg|jpg|gif)$/,
use: ['file-loader'],
}
]
},
Developer Tools Webpack Andrew Smith 550 / 712
Dev Server
• Changes are automatically re-bundled by webpack
• Live Reload: changes trigger a browser refresh
Developer Tools Webpack Andrew Smith 551 / 712
Webpack Demonstration
Developer Tools Webpack Andrew Smith 552 / 712
Exercise
Let’s take a look at a Webpack demonstration application:
1. Open the webpack-babel-starter-project repo
- github.com/AndrewSouthpaw/webpack-babel-starter-project
1. Follow the “Build Your Own” README steps
2. Add the ability to load an image
Developer Tools Webpack Andrew Smith 553 / 712
Resources
• Webpack + Babel starter: webpack-babel-starter-project
• Webpack Docs: https://webpack.js.org/guides/
• Frontend Masters: Webpack Fundamentals
• Frontend Masters: Webpack Performance
Developer Tools Webpack Andrew Smith 554 / 712
Introduction to TypeScript
Developer Tools Introduction to TypeScript Andrew Smith 555 / 712
What is TypeScript
• A language based on ESNEXT
• Compiles to ES5
• Contains the following additional features:
• Types and type inference!
• Generics (polymorphic types)
• Interfaces and namespaces
• Enums and union types
Developer Tools Introduction to TypeScript Andrew Smith 556 / 712
Type Annotations
function add(x: number, y: number): number {
return x + y;
}
Developer Tools Introduction to TypeScript Andrew Smith 557 / 712
Type Checking
// Works!
const sum = add(1, 2);
// error: Argument of type '"1"' is not assignable
// to parameter of type 'number'.
add("1", "2");
Developer Tools Introduction to TypeScript Andrew Smith 558 / 712
Type Inference
// Works!
const sum = add(1, 2);
// error: Property 'length' does not exist
// on type 'number'.
console.log(sum.length);
Developer Tools Introduction to TypeScript Andrew Smith 559 / 712
Additional Examples
Look in the following folder for additional examples:
src/www/js/alternatives/typescript/examples
Developer Tools Introduction to TypeScript Andrew Smith 560 / 712
Testing Overview
Testing Testing Overview Andrew Smith 561 / 712
3 Types of Tests
1. Unit
2. Integration
3. End-to-End (E2E)
Unit and Integration tests can be run without a browser. Faster to run,
sometimes slower to write.
E2E tests simulate user behavior interacting with a browser environment.
Slower to run, sometimes faster to write.
Testing Testing Overview Andrew Smith 562 / 712
Unit and Integration Tests
Most popular framework is Jest.
Other common frameworks are Mocha, Jasmine, AVA, Tape, and QUnit
Testing Testing Overview Andrew Smith 563 / 712
Unit and Integration Tests Categories
There’s two basic categories that JS unit tests fall into:
1. Pure JavaScript
2. JavaScript + Browser
Code that is “just” JavaScript (no browser APIs) is the easiest to test.
Testing code that includes the browser is often challenging and often
requires more mocking.
Testing Testing Overview Andrew Smith 564 / 712
Jest: Basics
Testing Jest: Basics Andrew Smith 565 / 712
What is Jest?
• JS testing framework
• Focus on simplicity and easy configuration
• Easy mocking of modules
• Good for unit and integration tests
Testing Jest: Basics Andrew Smith 566 / 712
Example: Writing Jest Tests
const add = (x, y) => x + y
describe('#add', () => {
it('adds two numbers together', () => {
expect(add(1, 2)).toEqual(3)
})
})
Testing Jest: Basics Andrew Smith 567 / 712
Running Jest Tests
• yarn add jest
• Make a *.test.js file
• Run yarn jest, you’re done!
• Continuously watch: yarn jest --watch
Testing Jest: Basics Andrew Smith 568 / 712
Most Common Matchers
toEqual(val): Most common equality matcher. Compares objects or
arrays by comparing contents, not identity.
toMatch(/hello/): Tests against regular expressions or strings.
Testing Jest: Basics Andrew Smith 569 / 712
Expecting an Error
toThrow(message): Tests the function will throw an error.
describe('#findById', () => {
it('should throw if not a number', () => {
expect(() => findById('invalid'))
.toThrow('Must provide a number')
})
})
Testing Jest: Basics Andrew Smith 570 / 712
Expecting the Opposite
You can chain not to test the opposite
it('test the opposite', () => {
expect(0).not.toEqual(1)
})
Testing Jest: Basics Andrew Smith 571 / 712
Other Matchers Sometimes Used
toContainEqual(x): Expect an array to contain x.
toBe(x): Compares with x using ===.
toBeTruthy(): Should be true true when cast to a Boolean.
toBeFalsy(): Should be false when cast to a Boolean.
arrayContaining(array): Checks it’s a subset (order doesn’t matter).
Testing Jest: Basics Andrew Smith 572 / 712
What Are Spies
• Spies allow you to track calls to a method
• Arguments
• Results
• Passes call through to original implementation
Testing Jest: Basics Andrew Smith 573 / 712
Spies API
Creating a spy:
const spy = jest.spyOn(myObject, 'method')
Removing a spy:
spy.restore()
Testing Jest: Basics Andrew Smith 574 / 712
Spying on a Function or Callback (Call Tracking)
const video = {
play() { return true },
}
it('should play a video', () => {
const spy = jest.spyOn(video, 'play')
const isPlaying = video.play()
expect(spy).toHaveBeenCalled()
expect(isPlaying).toBe(true)
spy.mockRestore()
})
Testing Jest: Basics Andrew Smith 575 / 712
Spying on a Function or Callback (Call Fake)
it('should allow a fake implementation', () => {
const spy = jest.spyOn(video, 'play')
.mockImplementation(() => false)
const isPlaying = video.play()
expect(spy).toHaveBeenCalled()
expect(isPlaying).toBe(false)
spy.mockRestore()
})
Testing Jest: Basics Andrew Smith 576 / 712
Mocks
• Mocks are functions with pre-programmed behavior
• Can be used to replace methods or module dependencies
• Why mock
• Avoid expensive / slow calls (server calls, complex compuations, etc.)
• Simplifies dependencies
• Avoid complex test setup (e.g. dependency requires a bunch of state)
• You follow the “London-TDD” style
Testing Jest: Basics Andrew Smith 577 / 712
Mocks API
Creating a mock:
const mock = jest.fn()
With behavior:
const mock = jest.fn(() => 'yay')
mock() // 'yay'
Testing Jest: Basics Andrew Smith 578 / 712
Mock Functions
Say we’re testing a higher-order function:
const forEach = (items, callback) => {
for (let i = 0; i < items.length; i++) {
callback(items[i])
}
}
Testing Jest: Basics Andrew Smith 579 / 712
Captures Calls
You can create a mock function to capture calls.
const myMock = jest.fn()
Example:
it('capture calls', () => {
const mockCallback = jest.fn()
forEach([0, 1], mockCallback)
expect(mockCallback.mock.calls.length).toEqual(2)
expect(mockCallback.mock.calls).toEqual([[0], [1]])
})
Testing Jest: Basics Andrew Smith 580 / 712
Captures all arguments
const myMock = jest.fn()
myMock('hello', 'world')
myMock(1, 2, 3)
expect(myMock.mock.calls).toEqual([
['hello', 'world'],
[1, 2, 3],
])
Testing Jest: Basics Andrew Smith 581 / 712
Provide Fake Behavior
You can specify static behavior of a mock function.
const getUserName = (id, lookupUser) => {
const user = lookupUser(id)
return user.name
}
it('should specify behavior', () => {
const mockFn = jest.fn(() => ({
id: 1,
name: 'Andrew'
}))
expect(getUserName(1, mockFn))
.toEqual('Andrew')
})
Testing Jest: Basics Andrew Smith 582 / 712
Provide Dynamic Behavior
You can use the arguments to a mock function to create dynamic behavior.
const getUserNames = (ids, lookupUser) => (
map(compose(prop('name'), lookupUser), ids)
// aka: ids.map(lookupUser).map(user => user.name)
)
it('should handle dynamic behavior', () => {
const mockUsers = {
1: { id: 1, name: 'Andrew' },
2: { id: 2, name: 'Billy' },
3: { id: 3, name: 'Charlie' },
}
const mockLookup = jest.fn((id) => mockUsers[id])
expect(getUserNames([1, 3], mockLookup))
.toEqual(['Andrew', 'Charlie'])
})
Testing Jest: Basics Andrew Smith 583 / 712
Mock Return Values
it('should mock return values', () => {
const mock = jest.fn()
.mockReturnValueOnce(42)
.mockReturnValueOnce('hello')
.mockReturnValue('default')
expect(mock()).toEqual(42)
expect(mock()).toEqual('hello')
expect(mock()).toEqual('default')
})
Testing Jest: Basics Andrew Smith 584 / 712
Cleanup per mock
• mockClear: reset calls/results
• mockReset: mockClear + reset return values / implementations
• mockRestore: mockReset + restores original non-mocked
implementation (for spies)
Testing Jest: Basics Andrew Smith 585 / 712
Cleanup in beforeEach
• jest.clearAllMocks
• jest.resetAllMocks
• jest.restoreAllMocks
Testing Jest: Basics Andrew Smith 586 / 712
Cleanup in config
Can provide package.json config to do it for all tests:
{
"jest": {
"clearMocks": true,
"resetMocks": true,
"restoreMocks": true
}
}
Testing Jest: Basics Andrew Smith 587 / 712
Exercise: Using Jest Spies
• Open src/www/js/jest/__tests__/adder.spec.js
• Complete the exercises.
• To test and debug, run
cd src
yarn jest adder.spec.js --watch
Testing Jest: Basics Andrew Smith 588 / 712
Jest: Environment & Life Cycle
Testing Jest: Environment & Life Cycle Andrew Smith 589 / 712
Jest Environment
• Each spec file runs in its own, isolated environment
• setupFilesAfterEnv: Setup files run before each test file
{
"jest": {
"setupFilesAfterEnv": ["<rootDir>/setupTests.js"]
}
}
Testing Jest: Environment & Life Cycle Andrew Smith 590 / 712
Life Cycle Callbacks
Each of the following functions takes a callback as an argument:
beforeEach: Before each it is executed.
beforeAll: Once before any it is executed.
afterEach: After each it is executed.
afterAll: After all it specs are executed.
Testing Jest: Environment & Life Cycle Andrew Smith 591 / 712
Abstracting Life Cycle Callbacks
These functions can be invoked from any module, as long as the calling
context is within a spec file!
// setup.js
const startWithLoggedInUser = () => {
beforeEach(() => {
// set up your app state to simulate a logged-in user
})
afterEach(() => {
// clean up app state...
})
}
Testing Jest: Environment & Life Cycle Andrew Smith 592 / 712
Abstracting Life Cycle Callbacks Use
// todos.js
describe('user todos', () => {
startWithLoggedInUser()
it('should read user todos', () => { /* ... */ })
})
Testing Jest: Environment & Life Cycle Andrew Smith 593 / 712
Pending Tests
Tests can be marked as pending:
it.todo('should do a thing')
Testing Jest: Environment & Life Cycle Andrew Smith 594 / 712
Focusing on Tests
You can mark only one test to be run in the file:
it.only('should do a thing', () => {})
Testing Jest: Environment & Life Cycle Andrew Smith 595 / 712
Jest: Timers
Testing Jest: Timers Andrew Smith 596 / 712
Testing Time-Based Logic (The Setup)
Given a delay function:
const delay = (ms, fn) => {
setTimeout(fn, ms)
}
This won’t work the way you want:
it('will not wait for no one', () => {
const mock = jest.fn()
delay(1000, mock)
expect(mock).toHaveBeenCalled() // FAILS
})
Why?
Testing Jest: Timers Andrew Smith 597 / 712
The Trouble With Time
JavaScript is a single-threaded runtime environment.
Tests run synchronously.
Testing Jest: Timers Andrew Smith 598 / 712
Mocking Time
Set up with
jest.useFakeTimers()
Many ways to manipulate time:
jest.runAllTimers(): Run all timers until there are none left
jest.runOnlyPendingTimers(): Run currently pending timers
jest.advanceTimersByTime(ms): Advance all timers by ms
jest.clearAllTimers(): Clear all pending timers
Testing Jest: Timers Andrew Smith 599 / 712
Running All Timers
jest.useFakeTimers()
it('should all timers', () => {
const mock = jest.fn()
delay(1000, mock)
jest.runAllTimers()
expect(mock).toHaveBeenCalled()
})
Testing Jest: Timers Andrew Smith 600 / 712
Running Pending Timers
Given
const delayInterval = (ms, fn) => {
setInterval(fn, ms)
}
Using jest.runAllTimers() will run forever.
Use jest.runOnlyPendingTimers() instead.
Testing Jest: Timers Andrew Smith 601 / 712
Running Pending Timers (Example)
it('should run pending timers', () => {
const mock = jest.fn()
delayInterval(1000, mock)
jest.runOnlyPendingTimers()
expect(mock).toHaveBeenCalled()
})
Testing Jest: Timers Andrew Smith 602 / 712
Advancing By Time
it('should advance time', () => {
const mock = jest.fn()
delay(1000, mock)
jest.advanceTimersByTime(999)
expect(mock).not.toHaveBeenCalled()
jest.advanceTimersByTime(1)
expect(mock).toHaveBeenCalled()
})
Testing Jest: Timers Andrew Smith 603 / 712
Cleanup
Good idea to use
afterEach(() => {
jest.clearAllTimers()
})
Testing Jest: Timers Andrew Smith 604 / 712
Safer Setup
jest.useFakeTimers impacts all tests in a test file.
Using fake timers can have unforeseen consequences:
• Promises behave unexpectedly
• async/await behaves unexpectedly
Instead, you can tell each test to use real timers and create a way to set up
a fake timer.
Testing Jest: Timers Andrew Smith 605 / 712
Safer Setup (Setup)
export const setupForFakeTimers = () => {
beforeEach(() => {
jest.useRealTimers()
})
return () => jest.useFakeTimers()
}
Testing Jest: Timers Andrew Smith 606 / 712
Safer Setup (Example)
describe('sometimes faking timers', () => {
const useFakeTimers = setupForFakeTimers()
it('normally has real timers', () => {
// jest.runAllTimers() <-- does not work
})
it('should have a fake timer', () => {
useFakeTimers()
jest.runOnlyPendingTimers()
})
})
Testing Jest: Timers Andrew Smith 607 / 712
Mocking
Testing Mocking Andrew Smith 608 / 712
Mocking Modules (Basic)
Say you’re testing a module, and you want to mock its dependency.
// add.js
import { somethingComplicated } from './dependency'
export const weirdMath = (a, b) => (
somethingComplicated() ? a + b : a - b
)
Testing Mocking Andrew Smith 609 / 712
Mocking Modules (Basic)
You can mock all the exports of a module from your test file:
jest.mock('./path/to/module/to/be/mocked')
This turns all exports into jest.fn()
Testing Mocking Andrew Smith 610 / 712
Mocking Modules (Basic)
// add.spec.js
import { weirdMath } from '../add'
import { somethingComplicated } from '../dependency'
jest.mock('../dependency')
somethingComplicated() // undefined
Testing Mocking Andrew Smith 611 / 712
Mocking Modules (Basic)
Provide mock values for the exports:
foo.mockReturnValue(42)
foo.mockReturnValueOnce(42)
foo.mockImplementation(() => 42)
Testing Mocking Andrew Smith 612 / 712
Mocking Modules (Basic)
// add.spec.js
import { weirdMath } from '../add'
import { somethingComplicated } from '../dependency'
jest.mock('../dependency')
it('should add or subtract', () => {
somethingComplicated.mockReturnValue(true)
expect(weirdMath(5, 2)).toEqual(7)
somethingComplicated.mockReturnValue(false)
expect(weirdMath(5, 2)).toEqual(3)
})
Testing Mocking Andrew Smith 613 / 712
Mocking Modules (Basic)
mockImplementation can provide dynamic behavior based on parameters.
const foo = jest.fn()
foo.mockImplementation(n => n + 1)
foo(1) // 2
foo(2) // 3
Testing Mocking Andrew Smith 614 / 712
Mocking Modules (Inline)
jest.mock can also take a function that returns an object to will replace
the exported modules.
I call this “inline” mocks.
jest.mock('../dependency', () => ({
foo: jest.fn(() => 'I am a fake foo'),
bar: jest.fn().mockReturnValue(42)
}))
Testing Mocking Andrew Smith 615 / 712
Mocking Modules (Inline)
It completely replaces what was going to be exported. If you don’t
provide them in the object, they will be missing.
// dependency.js
export const somethingComplicated = () => true
export const foo = () => 'foo'
// add.spec.js
import { foo } from './dependency'
jest.mock('./dependency', () => ({
somethingComplicated: jest.fn(),
}))
foo() // foo is not a function
Testing Mocking Andrew Smith 616 / 712
Mocking Modules (Manual Implementation)
Sometimes you have a more complicated mock behavior you want to
provide.
1. Create a file by the same name in __mocks__ sibling folder
2. Provide mock implementation there
3. Mock the dependency as before, it will use the definition in __mocks__
Testing Mocking Andrew Smith 617 / 712
Mocking Modules (Manual Implementation)
// __mocks__/dependency.js
export const somethingComplicated = () => 'mock!'
// add.spec.js
import { somethingComplicated } from './dependency'
jest.mock('./dependency')
it('should foo', () => {
somethingComplicated() // 'mock!'
})
Testing Mocking Andrew Smith 618 / 712
Mocking Within a Module
How about mocking other members within the same module?
// add.js
const somethingComplicated = () => {/* ... */ }
const add = (a, b) => {
// how do we mock this now?
return somethingComplicated() ? a + b : a - b
}
Testing Mocking Andrew Smith 619 / 712
Mocking Within a Module
Short answer: you can, but don’t. It’s a path of sadness.
If you really need to mock the behavior, pull it into a separate file.
Testing Mocking Andrew Smith 620 / 712
Partially Mocking
You can bypass a module mock with jest.requireActual('./path').
Handy application: partially mocking a module.
Testing Mocking Andrew Smith 621 / 712
Partially Mocking
// thing.js
import { somethingComplicated, foo } from './dependency'
export const complicatedDep = () => somethingComplicated()
export const fooDep = () => foo()
// thing.test.js
import { complicatedDep, fooDep } from './foo'
jest.mock('./dependency', () => ({
...jest.requireActual('./dependency'),
foo: jest.fn().mockReturnValue('mock foo')
}))
it('should partially mock', () => {
expect(complicatedDep()).toEqual('complicated') // real valu
expect(fooDep()).toEqual('mock foo') // mock value
}) Testing Mocking Andrew Smith 622 / 712
Exercise
We have to do this one locally, because Codesandbox doesn’t support
jest.mock.
1. Open src/www/js/jest_mocks/__tests__/coin.test.js. Follow
the instructions there.
2. You can run the spec from the /src directory with:
$ yarn jest coin.test.js --watch
Testing Mocking Andrew Smith 623 / 712
Jest: Async
Testing Jest: Async Andrew Smith 624 / 712
Testing Asynchronous Functions
Given a (fake) server interaction:
const users = {
1: { id: 1, name: 'Andrew' },
2: { id: 2, name: 'Billy' },
}
const getUser = (id) => new Promise((res, rej) => {
process.nextTick(() => (
users[id]
? res(users[id])
: rej('User ID ' + id + ' not found.')
))
})
Testing Jest: Async Andrew Smith 625 / 712
Testing Asynchronous Functions (with async)
You can use an async callback for it:
it('should handle async', async () => {
const user = await getUser(1)
expect(user).toEqual({ id: 1, name: 'Andrew' })
})
Or more tersely with await expect(...).resolves:
it('should handle async', async () => {
return await expect(getUser(1))
.resolves.toEqual({ id: 1, name: 'Andrew' })
})
Testing Jest: Async Andrew Smith 626 / 712
Testing Asynchronous Functions (with Promises)
If async isn’t available, you could return a promise:
it('should handle async', () => {
return getUser(1)
.then((res) => {
expect(res).toEqual({ id: 1, name: 'Andrew' })
})
})
You can make it more terse with expect(...).resolves:
it('should handle async', () => {
return expect(getUser(1))
.resolves.toEqual({ id: 1, name: 'Andrew' })
})
Testing Jest: Async Andrew Smith 627 / 712
Testing Async Dependencies
Say we’re testing a function that uses our async getUser function
indirectly:
const getUserName = async (id) => {
const user = await getUser(id)
return user.name
}
it('can still await with resolves', async () => {
return await expect(getUserName(2))
.resolves.toEqual('Billy')
})
Why does this work?
Testing Jest: Async Andrew Smith 628 / 712
Testing Inaccessible Async Operations
Sometimes we do something async but don’t await its result:
it('is hard to find how to wait!', async () => {
const mockFn = jest.fn()
await loadUserInBackground(1, mockFn) // won't wait!
expect(mockFn)
.toHaveBeenCalledWith({ id: 1, name: 'Andrew' })
})
// Test output FAILURE:
//
// Expected: {"id": 1, "name": "Andrew"}
// Number of calls: 0
Testing Jest: Async Andrew Smith 629 / 712
Testing Inaccessible Async Operations
Easiest way is to force a process tick in the test.
We call it “flushing promises”.
const flushPromises = () => (
new Promise(res => process.nextTick(res))
)
Testing Jest: Async Andrew Smith 630 / 712
Testing Inaccessible Async Operations (Example)
it('can have promises flushed', async () => {
const mockFn = jest.fn()
loadUserInBackground(1, mockFn)
await flushPromises()
expect(mockFn)
.toHaveBeenCalledWith({ id: 1, name: 'Andrew' })
})
This happens all the time in UI unit testing, e.g. with React.
Testing Jest: Async Andrew Smith 631 / 712
Async Error Handling
When you reject a promise and don’t catch it correctly. . .
it('should fail', () => {
return getUser(42)
.then((res) => { expect(1).toEqual(1) })
})
Your test will fail:
Error: Failed: "User ID 42 not found."
Testing Jest: Async Andrew Smith 632 / 712
Async Error Handling (with async)
You can test for error handling with async/await:
it('should catch errors', async () => {
try {
await getUser(42)
} catch (e) {
expect(e).toEqual('User ID 42 not found.')
}
})
Testing Jest: Async Andrew Smith 633 / 712
Async Error Handling (Silent Failures)
Unfortunately, if the promise doesn’t reject, the assertion is never called!
it('does not fail :-(', async () => {
try {
await getUser(1)
} catch (e) {
expect(1).toEqual(0) // Still passes!
}
})
Testing Jest: Async Andrew Smith 634 / 712
Async Error Handling (with rejects)
Safest approach is to use expect(...).rejects:
it('should return error message', async () => {
await expect(getUser(42))
.rejects.toEqual('User ID 42 not found.')
})
Testing Jest: Async Andrew Smith 635 / 712
Async Error Handling (with rejects FTW)
This will correctly fail the test if the promise was not rejected:
it('should fail', async () => {
await expect(getUser(1))
.rejects.toEqual('User ID 42 not found.')
})
// Test output:
//
// Received promise resolved instead of rejected
// Resolved to value: {"id": 1, "name": "Andrew"}
Testing Jest: Async Andrew Smith 636 / 712
Async Error Handling (thrown Errors)
If you throw an error, you must write a different expectation.
const boom = async () => {
throw new Error('kaboom')
}
it('will not match :-(', async () => {
return await expect(boom())
.rejects.toEqual('kaboom')
})
// Test output FAILURE
// Expected: "kaboom"
// Received: [Error: kaboom]
Testing Jest: Async Andrew Smith 637 / 712
Async Error Handling (with toThrow)
Use toThrow instead:
const boom = async () => {
throw new Error('kaboom')
}
it('will match with toThrow', async () => {
return await expect(boom())
.rejects.toThrow('kaboom')
})
Testing Jest: Async Andrew Smith 638 / 712
Quick Note About Fake Async. . .
setTimeout(cb, 0) and process.nextTick(cb) are not the same
thing.
setTimeout “takes longer” than process.nextTick
const flushPromises = () => (
new Promise(res => process.nextTick(res))
)
it('will not work', async () => {
const mockFn = jest.fn()
setTimeout(mockFn, 0)
await flushPromises()
expect(mockFn).toHaveBeenCalled() // Nope.
})
Testing Jest: Async Andrew Smith 639 / 712
Prefer process.nextTick
When possible, mock async behavior with process.nextTick.
Turns out jest.useFakeTimers() messes with setTimeout behavior. . .
const flushPromisesSTO = () => (
new Promise(res => setTimeout(res, 0))
)
Testing Jest: Async Andrew Smith 640 / 712
setTimeout Gets Weird
it('does not work :-(', async () => {
jest.useFakeTimers()
const mockFn = jest.fn()
setTimeout(mockFn, 0)
await flushPromisesSTO()
expect(mockFn).toHaveBeenCalled()
})
// Test output FAILURE:
// Timeout - Async callback was not invoked within
// the 5000ms timeout
Testing Jest: Async Andrew Smith 641 / 712
No Problems with process.nextTick
it('does work', async () => {
jest.useFakeTimers()
const mockFn = jest.fn()
process.nextTick(mockFn)
await flushPromises()
expect(mockFn).toHaveBeenCalled() // Yep!
})
Save yourself the pain and stick with process.nextTick when you can.
Testing Jest: Async Andrew Smith 642 / 712
Exercise: Handling Async Functions
1. Open src/www/js/jest/__tests__/async.spec.js
2. Do the exercises
3. To test and debug, open
cd src
yarn test www/js/jest/__tests__/async.spec.js
Testing Jest: Async Andrew Smith 643 / 712
Testing JS + Browser
Testing Testing JS + Browser Andrew Smith 644 / 712
Testing Browser Interactions in Unit Tests
Sometimes your unit/integration tests will involve browser APIs, e.g.:
• addTodoToDOMList: appends an li element to a ul todos element.
Use jsdom: creates fake browser environment
Testing Testing JS + Browser Andrew Smith 645 / 712
DOM Manipulation
const addTodoToDOMList = (text) => {
const todos = document.getElementById('todos')
const todo = document.createElement('li')
todo.appendChild(document.createTextNode(text))
todos.appendChild(todo)
}
Testing Testing JS + Browser Andrew Smith 646 / 712
Testing DOM Manipulations Setup
Set the browser body each time, it persists between tests.
beforeEach(() => {
// set up the browser DOM
document.body.innerHTML = '<ul id="todos"></ul>'
})
Testing Testing JS + Browser Andrew Smith 647 / 712
Testing DOM Manipulations
it('should add a todo to the todos', () => {
addTodoToDOMList('Learn jsdom')
addTodoToDOMList('Practice DOM changes')
const todos = document.getElementById('todos')
const todosText = Array.from(todos.children)
.map(child => child.textContent)
expect(todosText).toEqual([
'Learn jsdom',
'Practice DOM changes',
])
})
Pure magic.
Testing Testing JS + Browser Andrew Smith 648 / 712
UI Testing Libraries
Testing UI Testing Libraries Andrew Smith 649 / 712
UI Testing Libraries
Makes it easier to write UI tests.
• DOM-only
• @testing-library/dom
• React
• @testing-library/react
• enzyme
• react-test-renderer
• Vue
• @testing-library/vue
Testing UI Testing Libraries Andrew Smith 650 / 712
Unit Testing Best Practices
Testing Unit Testing Best Practices Andrew Smith 651 / 712
Most Importantly
• Practice TDD
1. Red (write a failing test)
2. Green (make the test pass)
3. Refactor (make your code better)
Really. Just do it.
Testing Unit Testing Best Practices Andrew Smith 652 / 712
Be Persistent and Track Your Discoveries
• There are also hard, tricky testing situations. Don’t give up.
• Google, Stack Overflow, ask teammates, ping @andrewsouthpaw, etc.
• Track solutions in test-helpers.js
• e.g.: flushPromises, stubTime
• Keep a living document of testing style and troubleshooting.
Testing Unit Testing Best Practices Andrew Smith 653 / 712
Other Valuable Practices
• Write abstractions to make your test files easier to read
• Make factories to simplify test data creation
• e.g. newTodo, newUser, newAppState, etc.
• Test for error handling on server interactions
• Automate your tests so they run all the time
Testing Unit Testing Best Practices Andrew Smith 654 / 712
Mock Less, Smile More
• Avoid mocking/stubbing as they create implicit interface contracts.
Generally only mock:
1. Server calls
2. Complex functions / behavior
3. Slow / expensive functions
• Mocking reduces confidence in system actually working
• Mocking is often hard to read
Testing Unit Testing Best Practices Andrew Smith 655 / 712
UI Testing Best Practices
• Separate business logic from DOM manipulation
• Interact with UI as a user, not a programmer, e.g.:
• Click the “Save” button, don’t invoke handleSaveClick
• Expect text changes, not <p> elements
Testing Unit Testing Best Practices Andrew Smith 656 / 712
E2E Testing
Testing E2E Testing Andrew Smith 657 / 712
E2E Testing
It simulates a user interacting with your website via a browser.
• PROS: Less mocking –> easier to write
• CONS: Slow to run
Testing E2E Testing Andrew Smith 658 / 712
E2E Testing Frameworks
Popular services/frameworks:
• Cypress
• Nightwatch
• Selenium
Testing E2E Testing Andrew Smith 659 / 712
Compatibility Testing
Testing Compatibility Testing Andrew Smith 660 / 712
Compatibility Testing
Depending on your team’s requirements, you may need to make sure your
site works in all browsers.
Popular services:
• SauceLabs
• BrowserStack
• LambdaTest
These tests are the most expensive to write and maintain.
Testing Compatibility Testing Andrew Smith 661 / 712
Console Debugging
Debugging Console Debugging Andrew Smith 662 / 712
Console Debugging
• Everyone knows console.log
• But there’s a lot more out there!
Debugging Console Debugging Andrew Smith 663 / 712
Console Debugging
• console.log shows object at time of expansion
Debugging Console Debugging Andrew Smith 664 / 712
console.table
• Prettier printouts of your objects and arrays
const people = [
{ id: 1, firstName: 'Andrew', lastName: 'Smith' },
{ id: 2, firstName: 'Billy', lastName: 'Joel' }
]
console.table(people)
Debugging Console Debugging Andrew Smith 665 / 712
console.dir
• Basically the same as console.log, you can ignore
Debugging Console Debugging Andrew Smith 666 / 712
console.assert
• Print out an error when assertion fails
• console.assert(assertion, 'message')
Debugging Console Debugging Andrew Smith 667 / 712
console.count
• Increment a counter
• console.count('label')
Debugging Console Debugging Andrew Smith 668 / 712
console.time
• A handy stopwatch for timing functions
• console.time(label) - start
• console.timeLog(label) - log current time
• console.timeEnd(label) - stop
Without a label:
With a label:
Debugging Console Debugging Andrew Smith 669 / 712
console.trace
• Print out a stack trace from a location in code
• console.trace(label)
Debugging Console Debugging Andrew Smith 670 / 712
console.group
• Create nested printouts
• console.group() / console.groupEnd()
• console.groupCollapsed() / console.groupEnd() - start with
them collapsed
Debugging Console Debugging Andrew Smith 671 / 712
Styling
• Add a %c into a string to start styling with CSS
• Then provide the CSS string as a second argument
• This uses console interpolation
Debugging Console Debugging Andrew Smith 672 / 712
Styling
• Can do this multiple times
• Pass in an empty string to reset styling
Debugging Console Debugging Andrew Smith 673 / 712
Styling
• You could also use it for
• This is how sites like Google and recruiting!
Facebook warn against XSS
attacks
Debugging Console Debugging Andrew Smith 674 / 712
Emoji
• Not actually debugging related, but very important!
• Use the Unicode Emoji List (link)
• Put into String.fromCodePoint(...)
• Hot tip: Alfred users can use the Alfred Emoji pack (Github link)
• Instructions (link)
Debugging Console Debugging Andrew Smith 675 / 712
Exercise
Practice working with:
• console.table
• console.assert
• console.count
• console.time
• console.trace
• console.group
• Styling with %c
Debugging Console Debugging Andrew Smith 676 / 712
Debugger
Debugging Debugger Andrew Smith 677 / 712
DevTools
Drop a debugger statement to open breakpoint in DevTools
const sayHello = () => {
debugger
console.log('Hello')
}
sayHello()
Debugging Debugger Andrew Smith 678 / 712
DevTools
Debugging Debugger Andrew Smith 679 / 712
DevTools
Finding an event handler:
Debugging Debugger Andrew Smith 680 / 712
DevTools
You can look at the handlers on a specific element:
Or with getEventListeners(element)
Debugging Debugger Andrew Smith 681 / 712
DevTools
See what events are happening for an element:
monitorEvents(btnElement)
Debugging Debugger Andrew Smith 682 / 712
DevTools
Pause on exceptions and hop into debugger:
This also works with failed console.assertions!
Debugging Debugger Andrew Smith 683 / 712
VS Code Debugger
Install Debugger for Chrome
1. Open command palette (Cmd+P / Ctrl+P)
2. Choose “Extensions: Install Extensions”
3. Search for “Debugger for Chrome” and install
Debugging Debugger Andrew Smith 684 / 712
VS Code Debugger
Set up Chrome debugger configuration
Then choose “Chrome”
Debugging Debugger Andrew Smith 685 / 712
VS Code Debugger
Two request options:
• launch - starts new Chrome instance
• attach - attaches to existing Chrome instance
We’ll use launch because starting up Chrome with debugger is tedious.
Debugging Debugger Andrew Smith 686 / 712
VS Code Debugger
• url: the URL to open in debugger
• webRoot: how to map URLs from browser to files on disk
• e.g. be able to locate http://localhost:3000/someFolder/foo.js
• in our case, served from ${workspaceFolder}/src/www
Debugging Debugger Andrew Smith 687 / 712
Resources
• Tutorial (link)
Debugging Debugger Andrew Smith 688 / 712
Demo
Debugging Debugger Andrew Smith 689 / 712
Webstorm Debugger
Run > Edit Configurations
Debugging Debugger Andrew Smith 690 / 712
Webstorm Debugger
Set up a new debug run configuration
Debugging Debugger Andrew Smith 691 / 712
Webstorm Debugger
Provide the URL of your app
Debugging Debugger Andrew Smith 692 / 712
Webstorm Debugger
Debugging Debugger Andrew Smith 693 / 712
Webstorm Debugger
• Tutorial (link)
Debugging Debugger Andrew Smith 694 / 712
Resources
• Google Chrome DevTools (link).
• console tricks (link)
Debugging Debugger Andrew Smith 695 / 712
Exercise
1. Go to http://localhost:3000/js/debugging/debugging.html
2. Open src/www/js/debugging/debugging.js
Debugging Debugger Andrew Smith 696 / 712
XSS
Intro to Web Security XSS Andrew Smith 697 / 712
Disclaimer
I am not a security specialist.
Intro to Web Security XSS Andrew Smith 698 / 712
Cross Site Scripting (XSS)
• Why the X? Because CSS was already taken
• Tricking a browser into running malicious code
• Most common form of website vulnerability
• #7 on OWASP (link)
Intro to Web Security XSS Andrew Smith 699 / 712
Cross Site Scripting (XSS)
Whenever you render user input:
<span><%= @user.name %></span>
You’re potentially opening yourself to XSS.
Intro to Web Security XSS Andrew Smith 700 / 712
Cross Site Scripting (XSS)
What if user name is <script>alert('hello')</script>?
<span><script>alert('hello')</script></span>
Now you’re in trouble.
Intro to Web Security XSS Andrew Smith 701 / 712
XSS Attack Types
1. Stored - stored in DB
2. Reflected - transient server messages: “You registered . . . ”
3. DOM Based - no server, via query params or URL
Intro to Web Security XSS Andrew Smith 702 / 712
Likely Places
• Rendering user content
• Embedded content (iframe)
• element.innerHTML = ...
Intro to Web Security XSS Andrew Smith 703 / 712
innerHTML
Script tags added after initial page load via innerHTML do not run and
are harmless (link)
const name = "<script>alert(1)</script>"
el.innerHTML = name // no alert; harmless
But there are other ways to trick innerHTML:
const name = "<img src='x' onerror='alert(1)'>"
el.innerHTML = name // shows the alert
Intro to Web Security XSS Andrew Smith 704 / 712
Updating text
Use innerText instead of innerHTML when possible:
// reading
spanEl.innerText // 'Hello'
// setting
spanEl.innerText = 'Goodbye'
Intro to Web Security XSS Andrew Smith 705 / 712
Never trust user data
Just don’t.
Especially if it shows up to anyone else, which is basically always.
Treat user data as values, not code
Intro to Web Security XSS Andrew Smith 706 / 712
Never inject untrusted data
Doesn’t matter where you put it:
<!-- <%- userData %> -->
<p><%- userData %></p>
<iframe config="<%- userData %>" />
<iframe <%- userData %>="myValue" />
<style><%- userData %></style>
Why?
Intro to Web Security XSS Andrew Smith 707 / 712
Never inject untrusted data
They can always close out valid HTML and create a script.
<!-- <%- userData %> -->
If userData = --> <script>alert(1)</script> <!--
Now we have:
<!-- --> <script>alert(1)</script> <!-- -->
Intro to Web Security XSS Andrew Smith 708 / 712
Sanitizing Data
Before it’s persisted
Before it’s rendered onto the screen.
Usually involves “escaping” characters that facilitate XSS:
<script>alert(1></script>
"%3Cscript%3Ealert(1)%3C%2Fscript%3E"
Intro to Web Security XSS Andrew Smith 709 / 712
Sanitizing Data
Many rendering libraries do this for you: React, Rails, etc.
And you can “opt out” of escaping.
React: <div dangerouslySetInnerHTML={"unescaped"} />
EJS: <%- "unescaped" %>
Vue: {{{ "unescaped" }}}
Angular: <div [innerHTML]="Unescaped" />
Intro to Web Security XSS Andrew Smith 710 / 712
Sanitizing Data
Sometimes you really need custom CSS, HTML, etc. from user
Use sanitizer library like DOMPurify (link) to take out potentially malicious
content.
<!-- before -->
<p>Hello</p>
<img src=x onerror="alert('XSS Attack')">
<!-- after -->
<p>Hello</p>
<img src="x">
Intro to Web Security XSS Andrew Smith 711 / 712
Exercise
1. Open http://localhost:3000/js/security/xss.html
2. Open src/www/js/security/xss.js to start
Intro to Web Security XSS Andrew Smith 712 / 712