Week 3 - Advanced Javascript - CI256

Week 3 - Advanced Javascript

Think of websites like a house:

HTML is the foundation and framing

CSS is the paint and design

Javascript is the light switches - it controls the fixtures in the house

[to be continued…]

Template Literals

A Template literal is basically a “fancy string”. They always start and end with a single “backtick” character. This means you can freely use single and double quotes within them.

let famousQuote = `Ricky's dad once said "If you ain't first, you're last!"`

They also allow you to “interpolate” variables and expressions directly into a string using the syntax ${}.

let x = 5;
let y = 10;
console.log(`The value of x is ${x} and y is ${y}`)

Any valid Javascript expression can go in-between the curly braces ({}).

// multiple two values
console.log(`${x} * ${y} = ${x * y}`)

// call a function
function product(x, y) {
	return x * y;
}
console.log(`${x} * ${y} = ${product(x, y)}`)

Template literals are able to span multiple lines, unlike any other type of string in Javascript. This can be useful for weird situations where you want to include new lines in your final text.

let query = `
query ServicePointsTable($eventId: uuid!) {
    scoring_periods(where: {event_id: {_eq: $eventId}}) {
      event_id
      start
      end
      points (where: {check:{type: {_eq: "service"}}}) {
        check {
          id
          name
          type
        }
        check_id
        points
        interval
      }
    }
  }
`

let yaml = `
todo:
- Clean Dishes
- Fold Laundry
`

Variable Scope

Variables defined with let and const are tightly limited to their scope, while variables defined with var are lifted to the highest available scope.

You can essentially think of scope as the nearest set of curly braces ( { / } )

{
    if(true) {
        const x = 'x';
        let y = 'y';
        var z = 'z';
        console.log(x, y, z)
    }
    
    console.log(z)
    console.log(x)
}

Function Definitions

Traditional function keyword

The traditional method of creating a function gives it a name using the function keyword.

function asdf() {
    return 'traditional asdf'
}

Anonymous Arrow function

Functions are just objects too; they can be assigned to variables. A common pattern in JS is to assign an anonymous function to a variable:

let asdf = () => {
    return 'anonymous asdf'
}

asdf()

There are a few key differences to Arrow functions over traditional functions:

  1. They treat the this scope differently
  2. They are considered “anonymous”, aka, they don’t have a name
  3. They have a shorthand without curly braces

What is this?

TL;DR - you probably don’t want to use this in a callback function, ever.

this is similar to this in Java/C# and self in Python - it is typically used to access the current instance of a class. Since everything in Javascript is an Object, and object is a class, everything in javascript can have a this.

Don’t be fooled - this may seem convenient, but it can change! Whenever you pass a function as a callback, this can be set to the object that is calling the function.

I don’t have any clear examples of this - just be weary of this, and know that an arrow function () => {} may be the solution.

🎵 No-body knows my name🎵

Anonymous functions don’t have a “name”. The only time this really matters is stack traces while debugging - in production, the function names are typically “minified” anyways.

function traditional() {
	throw new Error('Tried to do a thing but there is nothing to do')
}
let arrow = () => {
	throw new Error('Tried to do a thing but there is nothing to do')
}	

console.log(traditional)
console.log(arrow)

No curly braces? What is this, Python?

Arrow functions don’t need to define a function body with {} if their body is a single line. Using this method, the single line is treated as the return value.

// these are equivalent
function asdf() {
	return 'anonymous asdf but shorter'
}

let asdf = () => {
	return 'anonymous asdf but shorter'
}

let asdf = () => 'anonymous asdf but shorter'

asdf()

This is super helpful when we get into higher order functions.

Parameters

Parameters in JavaScript are passed at the beginning of a function

function question1(num1, num2) {
    return Math.sqrt(num1 * num2)
}

const result = question1(5, 10)

Manually inputting values to Javascript code is pretty rare, and we won’t do it in this class.

Default Values

If less than the number of required parameters are passed to a function, the remainder always default to undefined.

function getUsers(organization, active) {
	let msg = `organization=${organization} active=${active}`
    console.log(msg)
    return msg
}

// return ALL users
getUsers()
// return all MVCC users
getUsers('MVCC')
// return all ACTIVE NCAE users
getUsers('NCAE Cyber Games', true)

As of ES6 / ECMAScript2015, parameters may have default values - yay!

function getUsers(organization = null, active = true) {
	/* return all active users by default */
	let msg = `organization=${organization} active=${active}`
	console.log(msg)
	return msg
}

// return all active users
getUsers()

// these are the same
getUsers('NCAE Cyber Games') === getUsers('NCAE Cyber Games', true)

// parameters are always positional
// to return all inactive users, we'll pass null for the organization
getUsers(null, false)

Objects are often passed as “config” objects, allowing parameters to be named instead of positional. This allows for significantly more flexibility

type GetUsersConfig = {
	active: boolean | undefined;
	organization: string | null | undefined;
	createdAfter: Date | undefined;
	group: string | undefined;
}

function getUsers(config = {}) {
	console.log('fetching users with config', config)
}


// all of the following are valid
getUsers()
getUsers({active: false})
getUsers({organization: 'MVCC', active: true})
getUsers({group: 'CI256'})

Rest Operator

The “rest operator” allows us to take an arbitrary number of arguments - the “rest” of the arguments!

The actual variable we receive, numbers, will be an Array.

function product(...numbers) {
	console.log(`numbers is an array of length ${numbers}`, numbers)
    let total = numbers.pop()
    for(let n of numbers) {
        total *= n
    }
    return total;
}

// we can pass as many or as few arguments as we'd like
product(2, 2)
product(1, 2, 3, 4, 5)

The rest operator can co-exist with others parameters, but must appear last.

function sum(numbers) {
	// takes an Array of numbers and returns the sum of all values
    let total = 0
    for(let n of numbers) {
        total += n
    }
    return total;
}

function percentage(total, ...numbers) {
    console.log(`calc % of ${total} with ${numbers.length} numbers`, numbers)
    return `${sum(numbers) / total * 100}%`;
}

percentage(10, 2, 2)

If not enough values are provided, the variable will be an empty array.

// we expect numbers = []
percentage(10)

Spread Operator

The spread operator is the reverse of the rest operator. Instead of creating an Array from multiple parameters, it expands an Array into multiple function parameters.

function product(...numbers) { // same as above
	console.log(`numbers is an array of length ${numbers}`, numbers)
	let total = numbers.pop()
    for(let n of numbers) {
        total *= n
    }
    return total;
}


let values = [5, 2, 54, 5, 2]
product(...values)

More usefully, the spread operator can also be used to create arrays and objects.

let firstHalfPoints = [7, 2, 5]
let secondHalfPoints = [7, 2, 7, 7]

// scores contains the values from both Arrays!
let scores = [
	...firstHalfPoints,
	...secondHalfPoints
]

console.log(`The team scored ${scores.length} times for a total of ${sum(scores)}`)
console.log(scores)

This also works with objects 🤯 A common use of the spread operator with objects is defining default values. Spreads or properties that are defined later will take precedence.

function stepArray(userConfig) {
	let config = {
	    start: 0,
	    end: 100,
	    step: 1,
		...userConfig // any values in userConfig will override values above
	}
	let ret = []; // return value

	// iterate from start to end by step, adding each value to the array
	for(let i = config.start; i < config.end; i += config.step) {
		ret.push(i)
	}
	
	return ret
}

stepArray({step: 5})

stepArray({start: 50, end: 60})

stepArray({step: 100, end: 1000})

Ternary Operator

The ternary operator is an “inline”/short if statement.

The syntax is conditional ? true : false

let user = null;
console.log('user', user ? 'is' : 'is not', 'logged in')

user = {name: 'Billy'}
console.log('user', (user ? `${user.name} is` : 'is not'), 'logged in')

Pure Functions

Objects are passed by reference in Javascript, meaning that their values can be permanently modified.

For this reason, it’s important that we write our functions as “pure functions” - aka, they don’t modify their input parameters. Pure functions are fully deterministic based on their input values

let config = {
  start: 0,
  end: 6,
  step: 2
}
// This is a pure function - the input object is not modified
function step(c) {
    for(let i = c.start; i < c.end; i += c.step) {
        console.log(i)
    }
}
step(config)
console.log(config)


// this is NOT a pure function - it modifies c.start
function step(c) {
    for(c.start; c.start < c.end; c.start += c.step) {
        console.log(c.start)
    }
}

step(config)
console.log(config)

// calling step again the same way produces a different result
step(config)

Dependence on variables out of scope

A Pure function also means no it cannot read or write to a variable outside of it’s scope:

let count = 5;

// This is not a pure function
function increment() {
    return count += 1
}

// even though this may be our desired behavior, each call to increment changes, so this is not a pure function
increment()
increment()
increment()

// the pure version of this function would be
function increment(c) {
    return c + 1
}

// multiple calls to increment return the same value
increment(1)
increment(1)

// multiple
count = increment(count)

Higher Order Functions

Higher order functions are functions that take one or more functions as arguments, or return a function as their result.

Callbacks

A “callback” can be thought of as any function that is passed into a function, then called later within that function. Callbacks are a very common pattern in JavaScript, and all of the examples in this section will use callback functions.

function hello(name) {
	console.debug(`hello(${name})`)
	return `Hello, ${name}`
}

function goodbye(name) {
	console.debug(`goodbye(${name})`)
	return `Goodbye, ${name}`
}

function saySomething(name, formatter) {
  console.log('I say:', formatter(name))
}

saySomething('John', hello)
saySomething('John', goodbye)
saySomething('Kayla', name => `Hey ${name}, nice to meet you.`)

Data Manipulation

The most common higher order functions are used for manipulating data within arrays.

Sort / toSorted

Sort will change the order of items in an array. This does work with no parameters (with caveats), or it can accept a compare function.

Note: Sort will modify the array in place and is therefore not a pure function. Using toSorted is recommended.

let fruits = ['orange', 'apple', 'peach']
console.log(fruits.toSorted())
fruits // observe original array is maintained

// .sort() returns the array...
console.log(fruits.sort())
fruits  // but also modifies it in place

By default, this works by sorting everything as if it was a string. This works well for strings that are all the same case - but fails pretty quickly.

console.log(['Orange', 'apple', 'Peach'].toSorted())
// result: ['Orange', 'Peach', 'apple'] 
// apple isn't first anymore!

console.log([21, 4, 30, 100000, 1].toSorted())
// result: [1, 100000, 21, 30, 4]
// 100000 is largest!
// 4 is smaller than 21 and 30! AHHHHHH!

Instead of relying on the default JS behavior, we can pass a “comparator” function, which should return either 0, a negative, or a positive number.

  • < 0 means “sort b after a
  • 0 means keep the original order
  • 0 means “sort a after b

[21, 4, 30, 100000, 1].toSorted((a, b) => a - b);
// result: [1, 5, 10, 15, 100]
// thats better!
Arrow functions make their triumph return!

Notice the arrow function in that example? This is where they really shine. We could have defined a function and passed it by name, but that would take 4 whole lines for a simple a - b function!

function compareNumbers(a, b) {
	return a - b;
}
[21, 4, 30, 100000, 1].toSorted(compareNumbers);

Instead, we can pass an anonymous arrow function, using that “automatic return value” to our advantage.

- function compareNumbers(a, b) {
- 	return a - b;
- }
+ (a, b) => a - b

Map

Transform an array into a new array one item at a time.

This is most commonly used to extract a particular value from an object, but can be used to remap or create complex objects.

Here’s an example of extracting names from a list of users:

let users = [
    {name: 'Leanne Graham', id: 1, city: 'Rome'},
    {name: 'Ervin Howell', id: 2, city: 'Rome'},
    {name: 'Clementine Bauch', id: 3, city: 'Utica'},
]

// traditional function to perform the same thing as .map()
function mapUsersToId(users) {
    let ids = [];
    for(let u of users) {
        ids.push(u.name)
    }
    return ids;
}

// arrow function shorthand!
users.map(u => u.name)

Going the other direction, here is how we might translate a list of user IDs into a list of full users

// mock function to represent fetching users from an API
function getUserById(userId) {
	let names = ['Sean', 'Brodie', 'Lucas']
    return {'name': names[userId - 1], id: userId}
}

// turn a list of userIds into a list of Users
let userIds = [1, 2, 3]
let users = userIds.map(getUserById)
console.log(users)

Filter

Filter returns a new array, locating any entries that match criteria defined by the callback function. The return value of the callback function must be “truthy” for it to be included.

let users = [
    {name: 'Leanne Graham', id: 1, city: 'Rome'},
    {name: 'Ervin Howell', id: 2, city: 'Rome'},
    {name: 'Clementine Bauch', id: 3, city: 'Utica'},
    {name: 'Brodie', id: 4},
]

// find all users in the city Rome
users.filter(u => u.city === "Rome")

Since the return value must only be “truthy”, we can use filter to easily find items with an attribute defined or missing.

// Find all users with a city defined
users.filter(u => u.city)

// find all users without a city defined
users.filter(u => !u.city)

Find

Find is used to locate first entry in an array that matches some criteria. It is very similiar to filter, except it returns the first value, rather than all values.

// find the user with the id 2
users.find(u => u.id == 2)

// find the first user living in utica
users.find(u => u.city == "Utica")

Reduce

Reduce executes a function on each value in the array to produce a single result.

This function is a little bit different in that the “accumulator” is passed to the function as the first argument, but an “initial value” of the accumulator should be passed as a second argument.

// previous implementation of sum
function sum(numbers) {
	// takes an Array of numbers and returns the sum of all values
    let total = 0;
    for(let n of numbers) {
        total += n;
    }
    return total;
}

// re-implmenting sum function with reduce
function sum(numbers) {
	return numbers.reduce(
		// total (accumulator) will start at 0, 
		// then be equal to the return value of the previous iteration
		(total, currentValue) => total + currentValue,
		0
	)
}

sum([1, 2, 3, 4])

Chaining Functions

It’s very common to “chain” higher order functions together - aka, use the return value from one to define another.

let users = [
    {name: 'Leanne Graham', id: 1, city: 'Rome'},
    {name: 'Ervin Howell', id: 2, city: 'Rome'},
    {name: 'Clementine Bauch', id: 3, city: 'Utica'},
    {name: 'Brodie', id: 4},
]

let userIds = [1, 3, 4];
userIds
	// resolve userIds to real users
	.map(id => users.find(u => u.id === id))
	// only annouce users that have a city defined
	.filter(u => u.city)
	// announce users
	.map(u => {
		console.log(`Welcome ${u.name} of ${u.city}`)
		return u // return user for future chaining
	})

JSON

JSON stands for JavaScript Object Notation JSON is a lightweight format for storing and transporting data JSON is often used when data is sent from a server to a web page JSON is “self-describing” and easy to understand

{
    // Employee Array
    "employees":[
        // Employee Objects
        {"firstName":"John", "lastName":"Doe"},
        {"firstName":"Anna", "lastName":"Smith"},
        {"firstName":"Peter", "lastName":"Jones"},
    ]
}

Different parsers will enforce different rules when reading JSON.

  • Some require double quotes (") and no single quotes '
  • Some require no trailing commas
  • Some require no comments

Common REST API Designs

We’ll be using JSON Placeholder to explore API designs

https://jsonplaceholder.typicode.com/

Request Types

  • GET: retrieve the specified resource
  • POST: Create a new resource or update an existing resource
    • Also commonly used as “perform an action”
  • PATCH: Partially update an existing resource
  • PUT: Entirely replace existing the specified resource
  • DELETE: Delete the specified resource

Typical API design

  • GET /api/posts - return all posts
  • GET /api/posts/:id - return the post with the specified ID
  • GET /api/posts/:id/comments - Return the comments related to the post with the specified ID
  • POST /api/posts - Create a new post
  • POST / PATCH / PUT /api/posts/:id - Update the existing post
    • APIs can be inconsistent with this

Asynchronous vs Synchronous Code

Synchronous code waits for a function or program to complete before executing additional code. Asynchronous code, aka “async”, allows the language to execute other code while waiting on certain operations.

Async code is particularly advantageous when performing Network and Disk IO operations, as those typically have alot of time spent “waiting” that the CPU could be performing other tasks.

Javascript shines when performing async operations. There are two patterns that may be used.

Promises

A Promise in JavaScript is an object representing the eventual completion or failure of an async operation. Promises have 3 states:

  • Pending: The initial state of a Promise. The operation has not completed yet.
  • Fulfilled: The state of a Promise representing a successful operation.
  • Rejected: The state of a Promise representing a failed operation.

When a promise is defined, two callback functions are passed - resolve and reject.

let user = null;
// user = {id: 5, name: 'Billy'}
function getUser() {
    return new Promise((resolve, reject) => {
        if(user) {
	        // fetch was successful
	        // "return" the user object from the promise
            resolve(user)
        } else {
	        // fetch was not successful
	        // "throw an error" from this promise
            reject('User not found')
        }
    })
}

getUser().then(
	// callback function on success
    u => console.log('got a user!', u)
).catch(
	// callback function on error
    e => console.error('failed to fetch user:', e)
)

Fetching API Data

When requesting data from REST APIs, you’ll need to use an async operation.

The most basic form of fetching data is done with fetch.

fetch("https://jsonplaceholder.typicode.com/todos/1")
    .then(response => {
        console.log('fetch response', response)
    })

This returns a response object though, which isn’t immediately useful to us. The thing we actually want is the data, which can be retrieved from response.json(). Unfortunately, this is ALSO an async function!

fetch("https://jsonplaceholder.typicode.com/todos/1")
    .then(response => {
        response.json().then(data => {
            console.log(data)
        })
    })

We’re starting to enter what’s lovingly referred to as “callback hell”.

Promise Chaining

Instead of continuously nesting promises, we can instead “chain” them together. The return value from one promise will be passed as the first parameter to the next promise in the chain.

// instead of this
fetch("https://jsonplaceholder.typicode.com/todos/1")
    .then(response => {
        response.json().then(data => {
            console.log(data)
        })
    })

// we can chain our promises
fetch("https://jsonplaceholder.typicode.com/todos/1")
    .then(response => response.json())
    .then(data => console.log(data))

There’s a better way with async and await!

The keywords async and await build on promises to clean up our syntax a bit and avoid “callback hell”.

When you define a function with async, you’re stating that the function returns a promise rather than a value.

async function getUser(id) { // this actually returns a Promise
	let response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
	let user = await response.json();
    if(user.id) {
        return user;
    } else {
        throw new Error('User not found')
    }
}

When using the keyword await with a promise, you’re saying “once this promise resolves, return the resolved value” as if this was a normal, synchronous function call.

// await getUser
console.log(await getUser(2))

// instead of using .then
getUser(2).then(user => {
	console.log(user)
})

This also means the remainder of a function doesn’t become more nested. Take this example:

let user = await getUser()
if(user.inactive) {
	await activateUser(user.id)
}

await addUserToTeam(user.id)
await notifyUser(user.id)

A purely promise based solution might look more like this

getUser().then(user => {  
  let handleUser = () => {  
    addUserToTeam(user.id).then(team => {  
      notifyUser(user.id, team).then(() => {  
        console.log('success!')  
      })  
    })  
  }  
  
  if(user.inactive) {  
    activateUser(user.id).then()  
  }  
})  
if(user.inactive) {  
  activateUser(user.id).then(handleUser)  
} else {  
  handleUser()  
}

Way more confusing, right?

Error Handling

Throwing an error

The throw keyword in Javascript is used to throw new errors. While we technically can throw anything, we usually want to throw an error class or something inheriting from Error with new Error().

function getUser() {
    throw new Error('AHHHHH something went wrong!')
}

Catching an Error

The try...catch..finally block is used to catch errors in JavaScript. You must specify try and one of catch or finally - you may specify both, but one is required

let obj = null;
try {
    // try operation
    console.log(obj.asdf);
} catch(err) {
    // catch failed operation
    console.log('failed to get obj.asdf', err)
} finally {
    // always do this, on success or fail
    console.log('value of obj is', obj) // debug print - what is obj?
}

The try..catch block will work with await Promise, but if a promise is not await’d, the error will not be caught.

try {
	// this is caught
	console.log(await getUser(999))
} catch(e) {
	console.warn('getUser() failed', e)
}

try {
	// this would NOT be caught
	getUser(999).then(user => console.log(user))
} catch(e) {
	console.warn('getUser() failed', e)
}