Mod 1 - Week 1 Study Guide

Table of Contents

Node

What is Node?

  • Two places to run JavaScript: Node and the Browser
  • Run .js files using Node with the terminal command node <filename.js>

Modules

  • A module is a file that exports code to be reused in other files.
  • A .js file can export values by assigning them to module.exports
// math-utilities.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
const SIMPLE_PI = 3.14;

// export these values in an Object
module.exports = {
  add, subtract, SIMPLE_PI
}
  • Another .js file can import those values using the require() function and providing a relative path.
// index.js

// destructure as "named" imports
const { add, subtract, SIMPLE_PI } = require('./math-utilities');

// or import the entire object as a "default" import
const mathUtils = require('./math-utilities');

console.log(add(5, 3)) // 8
console.log(mathUtils.add(5, 3)) // 8
  • Often, we will destructure a module's exports immediately. This is called a "named export/import".

Q: What are the benefits or putting our code into modules?

Answer

"Modularizing" our code enables us separate the concerns of our application for better organization and maintainability. It also allows us to easily re-use exported logic, minimizing repetitive code.

Node Package Manager

  • A package is a group of one or more modules published on the internet for use by other developers.
  • Node Package Manager makes it easy to install and manage third-party packages.
  • Packages published on NPM all have a package.json file that lists the package's dependencies, scripts, and metadata.
{
  "name": "1.0.0-assignment",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "preinstall": "cp hooks/pre-commit .git/hooks/pre-commit",
    "play": "node playground.js",
    "start": "node index.js",
    "lint": "eslint .",
    "test": "jest --coverage",
    "test:w": "jest --watchAll"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "eslint": "^8.34.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-config-airbnb-base": "^15.0.0",
    "eslint-plugin-import": "^2.27.5",
    "jest": "^29.4.3",
    "score-tests": "^1.0.0"
  }
}
  • A dependency is a package relied upon by another package. They can be installed using the npm i command
npm init          # creates a package.json file
npm i prompt-sync # installs prompt-sync as a dependency
npm i -D jest     # installs Jest as a devDependency
npm i -g nodemon  # installs nodemon as a global dependency
  • When importing modules installed from NPM, require() only needs the package name (not a relative path)
// npm module import
const prompt = require('prompt');

// local module import
const mathUtils = require('./math-utilities');
  • When just the name is given, require() knows to look for NPM modules instead of local modules.

Q: When installing a package via npm, where does it go?

Answer

Into a folder called node_modules/ in your project's root directory (or wherever the package.json file is).

Scripts

  • NPM Scripts are defined in a package's package.json file and help you quickly execute a terminal command
"scripts": {
  "preinstall": "cp hooks/pre-commit .git/hooks/pre-commit",
  "play": "node playground.js",
  "start": "node index.js",
  "lint": "eslint .",
  "test": "jest --coverage",
  "test:w": "jest --watchAll"
},
  • To run a script, enter npm run <script name>
    • Example: npm run play
  • Some designated scripts can omit the "run" argument, such as:
    • npm start
    • npm test

Q: The "lint" script is not a designated script name. How would you run it? What command is executed by that script?

Answer

npm run lint which executes the command eslint .

Jest & Testing

  • Jest is a testing framework for JavaScript.
  • All testing files end in .spec.js.
  • A test in Jest is made by invoking it() with a test name and callback. The callback has expect() statements that all must be true for the test to pass.
it('exports a named function called `add` from math-utilities.js', () => {
  const { add } = require('./math-utilities');
  expect(typeof add).toEqual('function');
});

it('returns the sum of two input numbers', () => {
  const { add } = require('./math-utilities');
  expect(add(5, 3)).toEqual(8);
  expect(add(1, 2)).toEqual(3);
  expect(add(100, 200)).toEqual(300);
});
  • Every test should test one piece of functionality, though it may require multiple expect() statements to do so.

  • A collection of tests is called a test suite

  • Run npm test to see the results of a test file. Below is a test suite with all tests passing

  • A failing test will show the expect() statement that failed as well as a comparison of what it "expected" vs. what it "received":

Q: In the failing test above, what mistake did the programmer make?

Answer

It seems as though the programmer forgot to put a comma , after the provided name as well as a question mark ? at the end of the string.

Vars, Functions, Strings

Variables

  • A variable "binds" a piece of data in memory to an "identifier" (the variable's name)
  • Use let or const to declare a new variable (don't use var)
let age = 18;     // let declares a reassignable variable
const PI = 3.14;  // const declares a non-reassignable variable

age = 19;       // Allowed
PI = 3.14159;   // TypeError: cannot reassign const variables
  • Variables must be referenced AFTER they are declared. Otherwise, a ReferenceError will be thrown.

Q: You are writing a program that involves a variable called clickCount that will keep track of how many times a user clicks on a button. Should clickCount be declared with let or const?

Answer

clickCount should be declared with the let keyword because we can assume that it will be reassigned each time the user clicks on the button.

Data Types:

  • Variables can store data of any kind data type: String, Number, Boolean, Undefined, Null, Function, or Object.
  • Primitive Data Types are String, Number, Boolean, undefined and null.
// Strings are text wrapped in 'single quotes', "double quotes",or `backticks`
const phrase = "Hello World!";
const name = 'ben';
const message = `My name is ${name}. I say ${phrase}`; // backticks allow for "String Interpolation"

// Numbers can be positive, negative, or include a decimal point
const PI = 3.14159;
const age = 18;
const countdown = -3;

// Booleans are either `true` or `false`
const canDrive = true;
const canFly = false;

// Null is a non-value that is explicitly set by the programmer
let toBeDetermined = null;

// Undefined is a non-value that is automatically set for variables that are not assigned a value:
let emptyVariable; 

Q: You are building a social media app where users have an email, a follower count, and an isAdmin status. Which data type would you use to represent each of these values?

Answer
  • email: String
  • follower count: Number
  • isAdmin: Boolean

Q: What are the similarities and differences between null and undefined?

Answer

null and undefined both represent non-values but null must be explicitly assigned while undefined is automatically assigned to variables/parameters without a value.

Functions (Arrow Functions)

  • A function is a block of code bound to a variable.
    • An arrow function is written (parameter) => { code block } and is stored in a variable.
  • After a function is declared, it can be invoked by ptting () after the function's name. Doing so causes the function's code block to execute.
const add = (a, b) => { // arrow function definition
  const sum = a + b;
  return sum;
}
const sum1 = add(5,3); // function invocation
const sum2 = add(1,2);
  • A parameter is a variable serving as a placeholder for data passed into a function.
  • An argument is a value "passed" to a function during a function invocation.
  • One argument must be provided for each parameter in the function definition.
  • When a function is invoked, the invocation will evaluate to a value determined by the function's return statement (or it will return undefined if not specified).

Q: What will be printed to the console after executing this program? What value will result hold?

const greet = (name) => {
  const message = `Hello ${name}, how are you?`;
  console.log(message);
}

const result = greet('ben');
Answer

result will hold the value undefined because greet doesn't have a return statement. As a result, the function will return undefined by default when it is invoked.

As a side-effect, the invocation of greet('ben') will print "Hello ben, how are you?" to the console.

Function Declarations

  • A function can also be created using the function declaration syntax. It can be identified by the use of the function keyword.
greet(); // this weirdly works
function greet(name) {
  console.log(`Hello ${name}, how are you?`);
}
  • Functions created using this syntax are hoisted (like variables declared with var) and are thus invocable before their declaration appears in the file.
  • For consistency, this should be avoided

Variable Scope & Hoisting

let - reassignable, hoisted, block scoped const - not reassignable, not hoisted, block scoped var - reassignable, hoisted, function scoped

  • Scope refers to where a variable can be referenced
  • let and const variables are block scoped — they are reference-able only within the code block where the variable is declared (loops, if/else statements, functions)
let a = "A globally scoped variable"
const myScopedFunction = () => {
  let b = "I can be seen ANYWHERE in this function";
  if (true) {
    let c = "I'm only visible inside this if statement";
    console.log(a, b, c); // We can reference all 3 values here
  }
  console.log(a, b); // but we can't reference c here
}
console.log(a); // and we can only reference a here
  • var variables are function scoped — they are reference-able anywhere within the function where the variable is declared (including nested blocks)
  • var variables are "hoisted to the top of the function scope" - you can reference the variable "before" it is declared (but it will hold undefined until it is assigned)
const foo = () => {
  console.log(a); // undefined, no error
  if (true) {
    var a = 'hi';
  } 
  console.log(a); // hi
}
  • Variables declared without any keyword are globally scoped, regardless of where they are declared. They are not hoisted. This should always be avoided.

Q: Why is scope important?

Answer
  1. Scope helps reduce memory usage by effectively "throwing away" a variable once we leave the scope of that variable.
  2. Scope can also reduce variable name conflicts. We will often use common variable names for repeated tasks, for example: declaring the variable i for a for loop. Without scope, every for loop would need to use a different variable name to avoid conflicts.

Q: Consider the code below. Where are x, a, b, and c available?

const myFunc = () => {
  console.log(a, b, c, x); // 1
  if (true) {
    const a = '5';
    let b = 10;
    var c = true;
    x = 'hi'
    console.log(a, b, c, x); // 2
  }
  console.log(a, b, c, x); // 3
}
myFunc();
console.log(a, b, c, x); // 4
Answer
  • a and b are only available at position 2 (within the if statement) because they are block-scoped.
  • c is function scoped and hoisted so it is available at position 1 (with the value undefined) and positions 2 and 3 with the value true.
  • x is a globally scoped variable and is available at positions 2, 3, and 4 (it is not hoisted to position 1)

String Properties and Methods

  • A string is a series of characters wrapped in quotations ('', "", or ``)
  • Each character in a string has an index - a number indicating the character's position in the string, starting at 0.
  • Bracket Notation can be used to read the character at the given index:
console.log("Hello");     // Hello
console.log("Hello"[0]);  // H
console.log("Hello"[4]);  // o
console.log("Hello"[5]);  // undefined
  • Every string has a .length property which returns the number of characters in the string:
console.log("Hello".length);  // 5
const name = 'Ben';
const lastChar = name[name.length - 1];
  • Every string has methods for manipulating the string. A method is a function that is invoked direclty "on" a data value:
console.log("Hello".indexOf("H"));      // 0
console.log("Hello".indexOf("o"));      // 4
console.log("Hello".includes("ell"));   // true
console.log("Hello".includes("h"));     // false
console.log("Hello".toUpperCase());     // HELLO
console.log("Hello".toLowerCase());     // hello
console.log("Hello".slice(1, 3));       // el 
console.log("Hello".slice(1));          // ello
console.log("Hello".slice());           // Hello
  • These are some of the most common String methods.

Q: Write a function called startsWith that takes in a string and a character and returns true if the string starts with that character.

Answer
const startsWith = (str, char) => {
  return str[0] === char;
  // or 
  // return str.startsWith(char);
}

Q: Write a function called capitalize that takes in a string and returns a copy of that string with the first letter capitalized and the rest in lowercase.

Answer
const capitalize = (str) => {
  return str[0].toUpperCase() + str.slice(1).toLowerCase();
}

Control Flow

Conditional Statements

  • A conditional statement is a block of code that will or will not run depending on the outcome of a Boolean expression (an expression that resolves to true or false)
  • Boolean Expressions can be created using comparison operators: < <= > >= === !==
  • A conditional statement begins with an if statement, can be followed by 0 or more else if statements, and can conclude with an else statement.
const greet = (name, tone) => {
  if (tone === 'happy') {
    console.log(`Hey ${name}, it is great to see you!`);
  } else if (tone === 'grumpy') {
    console.log(`Oh, it's you ${name}...`);
  } else if (tone === 'country') {
    console.log(`Howdy ${name}!`);
  } else {
    console.log(`Hi ${name}.`);
  }
}
greet('ben', 'happy');    // Hey ben, it is great to see you!
greet('ben', 'grumpy');   // Oh, it's you ben...
greet('ben', 'country');  // Howdy ben!
greet('ben');             // Hi ben.
  • Only the first statement in a chain of if/else if/else statements whose condition evaluates to true will be executed.
  • An else statement is only executed if all the prior if and else if statements all evaluate to false.
  • Truthy and Falsey values are non-Boolean values that will evaluate to true / false when placed in an if statement.
  • Falsey values are "empty" or "blank" or "non values":
    • An empty string ""
    • The number 0
    • The number NaN
    • undefined
    • null
  • All other values are truthy.

Q: Write the classic fizzBuzz function! It should accept a number and log to the console "fizz" if the number is divisible by 3, "buzz" if divisible by 5, and "fizzbuzz" if divisible by both. If not divisible by any of these, print the input number itself. Do not return anything.

Answer
const fizzBuzz = (num) => {
  if (num % 3 === 0 && num % 5 === 0) {
    console.log("fizzbuzz");
  } else if (num % 3 === 0) {
    console.log("fizz");
  } else if (num % 5 === 0) {
    console.log("buzz");
  } else {
    console.log(num);
  }
}

We can also take advantage of the "falsey-ness" of the number 0 to rewrite the conditions in this function:

const fizzBuzz = (num) => {
  if (!(num % 3) && !(num % 5)) {
    console.log("fizzbuzz");
  } else if (!(num % 3)) {
    console.log("fizz");
  } else if (!(num % 5)) {
    console.log("buzz");
  } else {
    console.log(num);
  }
}

If the remainder is 0, then we flip its truthyness using !.

Guard Clauses

  • A guard clause is an if statement used at the start of a function that returns immediately.
  • Guard clauses are used to "fail fast" — if the current state (data) does not allow the rest of the function to operate properly, we exit early.
  • Consider this example:
const add = (a, b) => {
  if (typeof a !== 'number' || typeof b !== 'number') { // guard clause
    return NaN;
  } else { // do we need this else statement?
    return a + b;
  }
}
  • In this example, we want to avoid adding non-number values. Strings, for example, will concatenate with the + operator, which we'd like to avoid.
  • We can refactor (rewrite) this function without the else statement:
const add = (a, b) => {
  if (typeof a !== 'number' || typeof b !== 'number') {
    return NaN;
  }
  return a + b;
}
  • If the guard clause's return statement is executed, the function will exit. Therefore, we are not at risk of accidentally executing return a + b.

Q: Write a function called getType that takes in a value and returns the type of that value as a string, including the types "NaN", "Array", and "null"

Answer
const getType = (value) => {
  if (Array.isArray(value)) {
    return "array";
  } 
  if (Number.isNaN(value)) {
    return "NaN";
  } 
  if (value === null) {
    return "null";
  } 
  return typeof value;
}

Rather than using a chain of else if statements, this solution uses a series of guard clauses.

Loops

For Loops

  • Iteration is the repetition of a process, getting closer to some result each time.

  • for loops are best used to repeat a process a known number of times.

    • The initialization is where the loop begins
    • The condition must be true for the loop to continue
    • The increment determines what to change after each loop
    • The body is what gets executed with each iteration of the loop.
const string = "hello";
for (let i = 0; i < string.length; i++) {
  console.log(string[i], i);
}
// h 0
// e 1
// l 2
// l 3
// o 4
  • An infinite loop is one in which the condition is ALWAYS true

Q: Write an infinite loop

Answer

There are many ways to make an infinite loop:

// decrementing when we should be incrementing
for (let i = 0; i < 10; i--) {
  console.log("it's infinite!");
}

// a condition that is always true!
for (let i = 0; i >= 0; i++) {
  console.log("it's infinite!");
}

// or the weirdest of them all. This condition is always true too!
for ( ; true; ) {
  console.log("it's infinite!");
}

While Loops

  • while loops are best used to repeat a process an unknown number of times
  • break prematurely breaks out of a loop
  • continue prematurely goes to the iteration step of the loop
const prompt = require("prompt-sync")(); // for later

while (true) {
  const input = prompt("Enter a number or q to quit: ");
  if (input === "q") {
    console.log("Bye!");
    break; // exit the loop immediately
  }
  if (Number.isNaN(Number(input))) {
    console.log("please enter a number");
    continue; // loop again immediately
  }
  console.log(`${input}? That's a great number!`);
}

Q: What is an example of a program that would use a while loop? Hint: one example has to do with Node.

Answer

The Node Read-Eval-Print-Loop (REPL) uses a while loop to continuously ask for user input. Run this program by entering the command node without any arguments into your terminal.

Nested Loops

  • A nested loop is a loop written inside the body of another loop. For each iteration of the outer loop, the inner loop will complete ALL of its iterations.
for (let i = 0; i < 2; i++) {
  for (let j = 0; j < 5; j++) {
    console.log(`${i} - ${j}`);
  }
}

Q: What does the nested loop above print to the console?

Answer
0 - 0
0 - 1
0 - 2
0 - 3
0 - 4
1 - 0
1 - 1
1 - 2
1 - 3
1 - 4

Arrays

Array Basics

  • An Array is a type of object that stores data in an ordered list.
  • An Array is created using square brackets with comma-separated values:
const family = ['Wendy', 'Jon', 'Daniel'];
const scores = [95, 87, 79, 88];
const marcyStaff = [
  { name: 'ben', isInstructor: true },
  { name: 'gonzalo', isInstructor: true },
  { name: 'travis', isInstructor: false },
];
  • Arrays have indexes and .length like strings
    • The first index is 0
    • The last index is array.length - 1
  • To access a single value, use bracket notation: array[index]
  • Unlike Strings, Arrays are mutable - their contents can be modified.
    • We can use bracket notation to reassign individual indexes in an Array
    • We can reassign the .length property to "shorten" the Array
const family = ['Wendy', 'Jon', 'Daniel'];
console.log(family[0]); // Wendy
console.log(family.length); // 3
console.log(family[family.length - 1]); // Daniel

family[0] = 'William';
family[1] = 'Michael';
console.log(family); // ['William', 'Michael', 'Daniel'];

family.length = 1;
console.log(family); // ['William']

family.length = 0;
console.log(family); // []
  • We can iterate through the contents of an Array with a for loop:
const family = ['Wendy', 'Jon', 'Daniel'];
for (let i = 0; i < family.length; i++) {
  const familyMember = family[i];
  console.log(`I am related to ${familyMember}`);
}
// I am related to Wendy
// I am related to Jon
// I am related to Daniel

Q: Clearly explain why array.length-1 is always the index of the last element in an Array

Answer

Every value of an Array has an index, starting with 0. The first value in an Array is at index 0. The second value in an Array is at index 1. So, the nth value in an Array is at index n-1.

If there are 5 values in an Array, then the last value would be at index 5-1 or 4.

const letters = ['a', 'b', 'c', 'd', 'e']
console.log(letters[4]);                // 'e'
console.log(letters.length);            // 5
console.log(letters[letters.length-1]); // 'e'

If an Array has arr.length values in it, then the index of the last value will be arr.length - 1

Mutating Array Methods

  • Arrays have a number of methods that mutate the contents of the Array:
const nums = [1, 2, 3]
nums.push(4);     // adds to the end (the "tail")
console.log(nums); // [1, 2, 3, 4]

nums.unshift(0);  // adds to the beginning (the "head")
console.log(nums); // [0, 1, 2, 3, 4]

nums.pop();   // removes the last value
console.log(nums); // [0, 1, 2, 3]

nums.shift(); // removes the first value
console.log(nums); // [1, 2, 3]

nums.splice(2, 1); // starting at index 2, removes 1 value
console.log(nums); // [1, 2];

nums.splice(1, 0, 'hi'); // starting at index 1, removes 0 values and inserts 'hi'
console.log(nums); // [1, 'hi', 2]
  • Arrays have many more methods, many of which are similar to String methods such as:
    • arr.includes()
    • arr.indexOf()

Pass by Reference vs. Pass by Value

  • When an Array is stored in a variable, a reference to the memory location of the Array is stored in the variable, not the values of the Array itself.
  • When a variable holding an Array or Object is assigned to another variable or function parameter, the reference is passed along, not the values.
// My partner and I share our kitchen supplies
let myKitchenSupplies = ["pot", "pan", "rice cooker"];
let partnerKitchenSupplies = myKitchenSupplies; // Pass-by-reference

partnerKitchenSupplies.push("spatula"); // Adding a value to the shared Array reference
console.log(myKitchenSupplies);         // ["pot", "pan", "rice cooker", "spatula"];
console.log(partnerKitchenSupplies);    // ["pot", "pan", "rice cooker", "spatula"];

myKitchenSupplies = [];                 // reassignment breaks the association
console.log(myKitchenSupplies);         // [];
console.log(partnerKitchenSupplies);    // ["pot", "pan", "rice cooker", "spatula"];
  • In this situation, there is only one Array reference that is held by two different variables.
  • If you mutate the Array reference, both variables will "see" those changes
const addValueToEnd = (arr, newVal) => {
  arr.push(newVal);
}

const letters = ['a', 'b', 'c'];
addValueToEnd(letters, 'd');
console.log(letters); // ['a', 'b', 'c', 'd']
  • Keep this in mind as you create functions that take in and mutate Arrays.

Q: Why is it important to know that Arrays/Objects are passed by reference?

Answer

If you were to copy an Array from one variable to another, you might think that there are now two separate Arrays. You may then accidentally mutate the Array using one variable, thinking that the Array referenced by the other variable will not be affected.

This would likely cause bugs.

Pure Functions

  • Functions that mutate values outside of their scope are considered impure functions. This is called a side effect.
  • A pure function is one that has no side-effects and produces the same outputs when called with the same inputs.

Q: Are pure functions "better" than impure functions? What benefits do pure functions provide?

Answer

Pure functions provide the benefit of being predictable. They are not necessarily "better" than impure functions though. Sometimes, functions need to have side effects.

Making Shallow Copies of Arrays

  • Making a copy of an Array is an effective way to avoid mutating the original Array when using mutating methods.
  • There are two common ways to make a shallow copy of an Array
const nums = [1,2,3];
const copy1 = nums.slice();
const copy2 = [...nums]; // "spread" syntax

copy1.push(4);
copy2.pop();

console.log(copy1); // [1,2,3,4]
console.log(copy2); // [1,2]
console.log(nums);  // [1,2,3]

Q: Refactor this function such that it does not mutate the input Array

const doubleValues = (arr) => {
  for (let i = 0; i < arr.length; i++) {
    arr[i] = arr[i] * 2;
  }
  return arr;
}
Answer

There are a few ways to do this but here is one way:

const doubleValues = (arr) => {
  const copy = [...arr]
  for (let i = 0; i < copy.length; i++) {
    copy[i] = copy[i] * 2;
  }
  return copy;
}

Objects

Object Basics

  • Objects are a data type that can store multiple pieces of data as key:value pairs called properties
  • Objects are best used for collections of data where each value needs a name
// This object has two properties with the keys "name" and "maker"
const car = {
  name: "Camry",
  maker: "Toyota",
};
  • Object values can be accessed and/or modified using dot notation or bracket notation
// add key/value pairs
car.color = "blue"; // dot notation
car["model-year"] = 2018; // bracket notation

// access values
console.log(car.color); // blue
console.log(car["model-year"]); // 2018
  • Delete properties by using the delete keyword and dot/bracket notation
// delete values
delete car.color;
delete car["model-year"];

Q: How are Arrays and Objects similar. How are they different? When would you use one or the other?

Answer
  • Arrays and Objects both can store multiple data values.
  • While values in an Object have a key (a String), values in an Arary have an index (a Number).
  • Arrays and Objects both use bracket notation to access values but only Objects use dot notation.
  • Use an Array when storing similar data or when the data needs to be in a numbered order (a list of todos)
  • Use an Object when storing related data or when the data needs to be referenced by name (a collection of user data)

Dynamic Properties

When the key name of a property is not known until you run the program, we can add "dynamic" properties using bracket notation (we can't use dot notation)

const prompt = require("prompt-sync")();

const car = {
  name: "Corolla",
  maker: "Toyota",
};

const key = prompt("What do you want to know about your car?");
const value = car[key];

console.log(`The value for the key ${key} is ${value}`);

Q: Why won't car.key work in the example above?

Answer

When using dot notation, we are effectively "hard coding" which property we are accessing. If we write car.key, then we are saying we want to access the "key" property of the object car, but there is no property with a key named "key".

When using bracket notation, the value held by the variable key will be resolved first, and we'll access the property who key name matches the value held by the variable key.

Object.keys() and Object.values()

  • The Object.keys(obj) static method returns an Array containing the keys of the provided Object obj. The keys will be Strings.
  • The Object.values(obj) static method returns an Array containing the values of the provided Object obj.
const user = {
  name: 'ben',
  age: 28,
  canCode: true,
}

const keys = Object.keys(user);
const values = Object.values(user);

console.log(keys);   // ['name', 'age', 'canCode'];
console.log(values); // ['ben', 28, true];
  • Notice that this is not user.keys() or user.values() but instead is a method of the Object object.
  • This is for flexibility as some objects may implement their own .keys() or .values() method

Q: Write a function called getLongestKey that takes in an Object and returns the key with the longest name.

Answer
const getLongestKey = (obj) => {
  let longestKeySoFar = '';
  const keys = Object.keys(obj);
  for (let i = 0; i < keys.length; i++) {
    if (keys[i].length > longestKeySoFar.length) {
      longestKeySoFar = keys[i];
    }
  }
  return longestKeySoFar;
}

Destructuring

  • Destructuring is a method of declaring a list of variables and assigning them values from an Array or Object.
const coords = [30, 90];
const [lat, long] = coords;
console.log(lat, long); // 30 90

const family = ['Wendy', 'Jon', 'Daniel'];
const [mom, , brother] = family; // skips the value 'Jon'
console.log(mom, brother); // Wendy Daniel

When destructuring Arrays:

  • Surround the list of variables with [] and assign to them the entire Array.
  • The names of the variables are up to you.
  • The order of the variables matters: the first variable gets the first value in the Array; the second variables gets the second value in the Array; and so on...
  • You can skip values by leaving a blank space
const user = {
  name: 'ben',
  age: 28,
  canCode = true;
}
const { name, age, canCode } = user;
console.log(name, age, canCode); // ben 28 true

const car = {
  make: "chevy",
  model: "cobalt",
  year: 2007
}
const { year, make } = car;
console.log(year, make); // 2007 chevy

When destructuring Objects:

  • Surround the list of variables with {} and assign to them the entire Object.
  • The names of the variables must match the property names of the Object.
  • The order of the variables does not matter
  • You can omit any unneeded properties

Q: Consider the array below. How would you use destructuring to create three variables called apple, banana, and orange?

const fruits = ['apple', 'banana', 'orange']
Answer
const [apple, banana, orange] = fruits;

Q: Consider the function below. How would you rewrite it such that it uses destructuring?

const introduceSelf = (person) => {
  console.log(`Hello! My name is ${person.name} and I am ${person.age} years old.`);
};
Answer

There are couple ways of doing this. You could destructure the person parameter in the function body:

const introduceSelf = (person) => {
  const { name, age } = person;
  console.log(`Hello! My name is ${name} and I am ${age} years old.`);
};

But the best way is to destructure the parameter:

const introduceSelf = ({ name, age }) => {
  console.log(`Hello! My name is ${name} and I am ${age} years old.`);
};

Callbacks & Higher Order Functions

First, A Normal Function

const greet = (person, msg, volumeLevel) => {
  let result = '';
  if (volumeLevel === 'loud') {
    result = `${person} said, "${msg.toUpperCase()}!"`
  } else if (volumeLevel === 'quiet') {
    result = `${person} said, "...${msg.toLowerCase()}..."`
  }
  console.log(result)
  return result;
}

greet('Maya', 'Hello', 'loud')
greet('Zo', 'Bye', 'quiet')

Q: What are the limitations of this function?

Answer
  • There is repetitive code (the if statements)
  • Each option must be hard-coded
  • Has a little too much control over HOW the greeting is formatted

Callbacks & Higher Order Functions

  • A callback is a function that is given to another function as an argument
  • A higher order function (HOF) is a function that takes in and/or returns a function
  • HOFs invoke callbacks
  • "We" do not invoke callbacks
// These are callbacks
const yell = (msg) => `${msg.toUpperCase()}!!`;
const whisper = (msg) => `...${msg.toLowerCase()}...`;

// This is a "Higher-Order" function
const greetBetter = (person, msg, voiceCallback) => {
  const result = `${person} said, "${voiceCallback(msg)}"`
  console.log(result);
}

greetBetter('Maya', 'Hello', yell); // Maya said, HELLO!!
greetBetter('Zo', 'Bye', whisper);  // Zo said, ...bye...

/* we can use "inline arrow functions" */
greetBetter('Ben', 'Hello World', (msg) => `${msg}?`) // Ben said, Hello World?
  • When we refactor this function to use callbacks we:
    • aren't repeating ourself
    • no longer need to specify HOW the person is greeting
    • aren't limited by the options we hard-coded
  • Inline arrow functions allow us to define single-use callbacks without storing them in a variable first.

Array Higher Order Methods

Declarative vs. Imperative Code

  • Imperative code provides explicit instructions for exactly HOW to complete a task.
    • High control (you write every single line)
    • High effort (you write every single line)
const doubleAllNums = (arr) => {
  const newArr = [];
  for (let i = 0; i < arr.length; i++) {
    const result = arr[i] * 2
    newArr.push(result)
  }
  return newArr;
}
const doubledNumsOld = doubleAllNums([1, 5, 10, 20]);
  • Declarative code provides the desired solution without specifying HOW to get there.
    • Low control
    • Low effort
const doubledNums = [1, 5, 10, 20].map((num) => num * 2);

Array Higher Order Methods

  • All Arrays have built-in higher-order methods for quickly iterating through the contents of the Array.
  • The important ones to know are:
Name When To Use
.forEach(callback) To iterate but not create a new Array
.map(modify) To make a copy of the Array with modified values
.filter(test) To make a copy of the Array with filtered values
.find(test) To get a single value from an Array
.findIndex(test) To get the index of a single value from an Array
.reduce(accumulator, startingValue) To derive a single value from an Arary
.sort(compare) To sort the contents of an Array in place
  • Invoke these directly ON the Array
  • These are Higher Order Methods because they are functions stored within an Array "Object", not as a stand-alone function.
const nums = [1,2,3,4,5];
const doubleNums = nums.map((num) => num * 2);
const evens = nums.filter((num) => num % 2 === 0);
const firstEven = nums.find((num) => num % 2 === 0);
  • Each Higher Order Method takes in a callback function
  • The first parameter of the callback should be named a singular version of the array name (num for an array of nums, letter for an array of letters, etc...)

Examples:

const letters = ['a', 'b', 'c'];
const names = ['wendy', 'jon', 'daniel'];
const nums = [1, 3, 5, 7, 9];

// Generic Callback (callback doesn't return anything)
nums.forEach((num) => console.log(num));
nums.forEach((num, i, arr) => console.log(num, i, arr));
nums.forEach(console.log);

// Modify Callback (callback returns a new version of the value)
const doubled = nums.map((num) => num * 2);
const capsLetters = letters.map((letter) => letter.toUpperCase());
const firstLetter = names.map((name) => name[0]);

// Test Callback (callback tests the value, returns true/false)
const lessThanSeven = nums.filter((num) => num < 7); // get All values that pass the test
const seven = nums.find((num) => num < 7); // get the first value that passes the test
const indexOfSeven = nums.findIndex((num) => num === 7); // get the index of the first value that passes the test
const areAllOdd = nums.every((num) => num % 2 === 1); // do all the values pass the test?
const hasSomeMultiplesOfThree = nums.some((num) => num % 3 === 0); // does at least one value pass the test?

// Accumulator Callback (callback returns the next value of the accumulator)
const sumOfNums = nums.reduce((total, num) => total + num, 0);

Q: You are given an Array of letters. You need to make a copy of the Array but with all the letters capitalized. What Array higher order method do you use? How would you use it?

Answer
letters.map((letter) => letter.toUpperCase())

Q: You are given an Array of numbers. You need to make a copy of the Array but with only values less than 5. What Array higher order method do you use? How would you use it?

Answer
numbers.filter((num) => num < 5);

Q: You are given an Array of numbers. You need the sum of those numbers. What Array higher order method do you use? How would you use it?

Answer
numbers.reduce((total, num) => total + num, 0);

Regex

Regex Basics

  • Regex is short for Regular Expression
  • A regular expression is a sequence of characters (or tokens) that specifies a match pattern in text.

Blue highlights show the match results of the regular expression pattern: /h[aeiou]+/g (the letter h followed by one or more vowels)

  • A regular expression is written between a pair of forward slashes / /

  • The letter g following the second / is a flag which alters the behavior of the regular expression. Important flags include:

    • g - the global flag matches ALL matches, not just the first one.
    • i - the case-insensitive flag disregards upper/lowercase when searching for matches
  • Use https://regexr.com/ to test out your regular expressions!

Character Sets

  • Character Sets (or Character Class) are a grouping of characters, any of which may be included in a matching pattern.
  • They are written inside of square brackets[]

match all instances of b followed by a vowel followed by a t

Match all instances of b followed by a vowel followed by a t

  • Characters sets can include ranges of letters/numbers too.
    • [0-9] - match any digit between 0 and 9
    • [2-7] - match any digit between 2 and 7 (inclusive)
    • [a-g] - match any lowercase letter between a and g
    • [a-zA-Z] - match any lowercase or uppercase letter

Match all instances of the numbers 1-9 (not including 0

Match all instances of the numbers 1-9 (not including 0)

  • To match anything NOT in a character set, put ^ inside of the [] at the start:

Match all instances of characters that are NOT a digit 1-9

Match all instances of characters that are NOT a digit 1-9

Special Character Sets

  • Common character sets (like all digits or all "word" characters) are represented with special characters/tokens
  • They all begin with \ and are followed by a single letter
  • There is often an inverse version of each
    • \d - Match any digit (0-9)
    • \D - Match any NON digit
    • \w - Match any "word" character (alpha-numeric or _)
    • \W - Match any NON-word character
    • \s - Match any whitespace character (spaces, tabs, line breaks);
    • \S - Match any NON-whitespace character
    • \b - Match any word-boundary position between a word character and a non-word character
    • \B - Match any NON-word-boundary
    • . - Matches any character except line breaks
    • \. - Matches a "." character

Match all sequences of two word characters. Notice that punctuation and spaces are not included.

Match all sequences of two word characters. Notice that punctuation and spaces are not included.

Quantifiers

  • Quantifiers allow us to specify the quantity of a particular character/character set
    • ? - 0 or 1 of the preceding token
    • * - 0 or more of the preceding token
    • + - 1 or more of the preceding token
    • {3} - 3 of the preceding token (can be any number)
    • {3,} - 3 or more of the preceding token
    • {3,5} - 3-5 of the preceding token

Match all sequences of 1 or more non-vowels

Match all sequences of 1 or more non-vowels

Anchors

  • Anchors match the beginning and/or end of a string.
    • ^ matches the beginning of the string, or the beginning of a line if the multiline flag (m) is enabled. This matches a position, not a character.
    • $ matches the end of the string

Matches the beginning of any line that starts with a vowel (case insensitive) and is followed by any number of word characters.

Matches the beginning of any line that starts with a vowel (case insensitive) and is followed by any number of word characters.

Creating Regular Expressions in JavaScript

  • A regular expression is a type of Object and can be stored in a variable:
  • Create a regular expression using the RegExp constructor:
const regEx = new RegExp('ab', 'g');
const greet = new RegExp(`Hi ${name}`, 'g');
console.log(typeof regEx); // 'object'
  • This is a bit clunky but you can make dynamic regular expressions.
  • When using the new RegExp syntax, you must escape all \:
const brokenRegEx = new RegExp(`\w\d`, 'g');
console.log(brokenRegEx);
// /wd/g - the \ are ignored

const goodRegEx = new RegExp(`\\w\\d`, 'g');
console.log(goodRegEx);
// /\w\d/g
  • Literal syntax is written with forward slashes. It is quicker to write but can't be used to generate dynamic regular expressions.
const literal = /[^a-z]/g;
const wrong = /`${name}`/; // won't be dynamic

Testing Patterns in JavaScript

  • Every Regex object has a .test() method which takes in a string and returns true if there is a match between the Regex and the given string.
const regex = /hi/;
const hasPattern = regex.test("I think Regex is the best!");
console.log(hasPattern); // true

Finding Matches

  • Every string has a .match() method which takes in a Regex object and returns an Array of matches between the string and the regular expression.
    • If the g flag is used, all results matching the complete regular expression will be returned, but capturing groups are not included.
    • If the g flag is not used, only the first complete match and its related capturing groups are returned.
const str = 'this is my cat, "thing 1"';
const firstMatch = str.match(/hi/);
console.log(firstMatch);
// [
//   'hi',
//   index: 1,
//   input: 'this is my cat, "thing 1"',
//   groups: undefined
// ]

const allMatches = str.match(/hi/g);
console.log(allMatches);
// [ 'hi', 'hi' ]