Web Apps

12.2 Spread, Destructuring, and Enumeration

# Spread & Rest Syntax

The spread and rest syntax use the same characters ... and can be seen as two parts of the same functionality.

# Spread

When you want to turn an object or array into a series of separate values you can use the spread syntax.

If you have a function that is expecting individual values and you have them in an array, then you can use spread to turn the array into separate values.

//1. a function is expecting 3 arguments.
function f1(a, b, c) {
  //pass in three numbers
}
//but we have the values in an array
let numbers = [12, 34, 56];
//spread the array out into 3 values when passing
f1(...numbers);
1
2
3
4
5
6
7
8

It also means that you can easily combine arrays too.

//2. combine arrays into a new array
let arr1 = ['Luke', 'Leia', 'Han'];
let arr2 = ['Chewie', 'C3P0', 'R2D2'];
//create a new array that includes arr1 and arr2 values
let names = ['Anakin', ...arr1, 'Obiwan', ...arr2];
1
2
3
4
5

You can also extract all the key:value pairs from an Object into an array.

let someObj = {
  id: 123,
  name: 'Fred',
  salary: 45000,
};
//with objects you need to wrap the object you are spreading with curly braces
let arr = { ...someObj };
1
2
3
4
5
6
7

# Rest

The rest syntax will take an unlimited number of arguments that are passed to a function and gather them as a single array.

function doSomething(...values) {
  //values is an array that will hold all the arguments that were passed to the function
}

doSomething(1, 2, 4);

doSomething('hello', 'hei', 'tag', 'hola');
1
2
3
4
5
6
7

# Destructuring

Destructuring is similar to the rest and spread syntax but it is more targetted and has some really helpful uses for developers. You can destructure both Arrays and Objects. It is often done as part of a function declaration where you want to extract from the object or array being passed to your function.

The basic idea behind destructuring is:

  • you declare one or more variables
  • those variables are targeting, interrogating, and extracting specific parts of what is being assigned to your variable(s).
//an array or object from elsewhere in the code
let myArr = ['Prometheus', 'Covenant', 'Alien', 'Aliens'];
let myObj = {
  title: 'Prometheus',
  year: 2012,
  director: 'Ridley Scott',
  starring: 'Noomi Rapace',
};

//basic array destructuring
let [first, second, ...therest] = myArr;
//we now have 3 variables first=Prometheus, second=Covenant, therest=['Alien', 'Aliens']

//base object destructuring
let { title, year } = myObj;
//we created two new variables title and year. We ignored the other props
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

If you are destructuring an array then you use let [] and put your new variable names inside the square brackets. The rest syntax is used to grab everything else from the array, which is not assigned to a variable.

If you are destructuring an object then you use let {} and put the names of the properties that you want to extract. New variables named the same as the properties are created. You can use the rest syntax here too.

We can also do this with function declarations. Let's say that we want to pass our myObj object from above to a function.

function f1({ director, year }) {
  //we are extracting the director and year props
  //from the object being passed to this function
}

function f2({ director, year, rating = 0.0 }) {
  //same as f1 except we are trying to extract a property called `rating`
  //if `rating` doesn't exist then we give it a default value of 0.0
}

function f3({ director, year: released, rating: rate = 0.0 }) {
  //same as f2 except we want to rename a couple props.
  // year will be extracted and renamed as `released`.
  // rating will be extracted and renamed as `rate`.
  //If `rating` was undefined then 0.0 will be assigned to `rate`.
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

There will be times when you have a couple variables and you need to swap the values in them. Normally, you would need to create a temp variable to trade the values. Here is the standard example:

let a = 99;
let b = 66;
//trade the values in the variables a and b
let temp = a;
a = b;
b = temp;
1
2
3
4
5
6

It requires three lines of code including the creation of the temp variable.

With destructuring we can replace those last 3 lines with one line of code.

[a, b] = [b, a];
//no `let` needed because both variables are already declared.
1
2

You can also skip over array values when destructuring. Let's use the myArr again and say that we want the first two values and the last (4th) value.

let [first, second, , fourth] = myArr;
1

The extra comma will skip one value. If you want, you can put multiple commas to skip multiple values.

You can also destructure nested properties. Let's start with a new variable that contains nested values.

let movie = {
  id: 12345,
  title: `Prometheus`,
  director: `Ridley Scott`
  //property with nested array
  cast: [
    `Noomi Rapace`, `Jenny Rainsford`, `Charlize Theron`, `Idris Elba`
  ],
  //property with nested object
  meta: {
    mpaaRating:'R',
    genres: ['Adventure', 'Sci-fi', 'Mystery']
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Now let's have a function that needs to extract the title, the first two cast members, and the mpaa-rating value.

function info({ title, cast: [first, second], meta: { mpaaRating } }) {
  console.log(title); //Prometheus
  console.log(first); //Noomi Rapace
  console.log(second); //Jenny Rainsford
  console.log(mpaaRating); // R
}

//call the function and pass in the movie object
info(movie);
1
2
3
4
5
6
7
8
9

The cast and meta values get extracted from the movie object. The contents of cast get assigned to [first, second], which because of the square brackets, gets destructured again. The first two values from the array assigned to cast get assigned to first and second.

The same thing happens with meta. It's value gets extracted from movie and assigned to {mpaaRating}. Due to the {} the mpaaRating property is destructured from that object.

# Iteration and Enumeration

# Property Descriptors

Every property inside every object has a property descriptor called enumerable. You can think of these as a standard set of properties that exist on every Object property that you create. In the example below we have an Object that has two properties - id and email. Each of those two properties has a set of property descriptors.

const myObj = {
  id: 12345,
  email: `steve@work.org`,
};
1
2
3
4

The Property descriptors are:

Accessor descriptors

  • configurable defaults to false.
  • enumerable defaults to false.

Data descriptors

  • value the actual value held by the property.
  • writable whether the value can be changed. Defaults to true.
  • get a method that will run when the value is being retrieved.
  • set a method that will run when the value is being changed.

The enumerable descriptor can be set to either true or false. If true, it means, when you do a for...in loop on that Object you will see the properties. The other properties are sort of hidden.

ENUMERABLE === appears in for...in loop

Iterable means that the object is similar to a String or an Array. Every character or item inside the object has a specific position. Think about the word hello. If you move any of the letters then it changes the meaning. The h must be in position zero. Arrays and NodeLists are the same. They have a specific order and position for everything inside of them.

Most Objects are NOT iterable. Take this Object as an example:

let obj = {
  a: 4,
  x: true,
  blah: 'what',
};
1
2
3
4
5

ITERABLE === appears in for...of loop

If we were to change the order of the properties inside the object, it would have NO effect on how the Object works or the values of the properties. So, because the properties do NOT have a specific order, they are not iterable. We can ask the Object to give us the first property or the next property because there is no sequence to follow.

# Iterators

Good news. If you want to make your Object iterable then we can now create a custom iterator for the object. The syntax to do this can be a bit scary but there is a feature called a generator which lets us turn a function into an iterator for our object.

Here is a sample of a generator function with its yield keyword.

function* getSomething() {
  yield myObj.a;
  yield myObj.x;
  yield myObj.blah;
  return;
}

let myObj = {
  a: 4,
  x: true,
  blah: 'what',
};

console.log(getSomething()); //output 4
console.log(getSomething()); // output true
console.log(getSomething()); //output "what"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# for...of loops

For all objects that are iterable naturally, or that have a custom iterator, we can use a for...of loop to step through it.

# Practical Custom Iterators

One use case for iterators is in combination with async and fetch. When you are fetching data from really big data sets and you want to grab the data in chunks, then iterators can be used to accomplish this.

# Maps and Sets

Map and Set are some of the newer datatypes in JavaScript. A Set is very similar to an Array. A Map is very similar to an Object. The difference is in the methods that you use to add and remove values, what you are allowed to use as keys, and things that you can do with them.

# Sets

A Set is similar to an Array. The main difference between an Array and a Set is that a Set makes sure that all its values are unique. If you try to add a duplicate then it will be ignored.

You can call new Set() to create an empty set or pass in an Array literal and have it converted to a set. Duplicates will be removed during conversion.

let names = new Set();
names.add('Steve'); //adds Steve to the set
names.add('Tony');
names.add('Robert');
names.add('Su Cheng');
console.log(names.size); //4
names.delete('Tony'); //removes Tony from the set
names.delete('Vladimir'); // does nothing because Vladimir is not in the list
names.has('Robert'); //true
names.forEach((item) => console.log(item)); //loops through all the items
names.clear(); //empties the whole set
1
2
3
4
5
6
7
8
9
10
11

MDN reference for Set (opens new window)

# Maps

A Map is like an object in that it is a collection of key-value pairs.

However, unlike an Object the keys can be ANYTHING. You can use any Primitive or Object as a key. A Map also remembers the order that items were added. A Map has a size property. Unlike Objects, Maps are iterable. A Map cannot be directly converted to JSON due to the non-string keys.

The methods for maps are close to the ones for Set.

let m = new Map();
m.set('key1', 'value'); // adds `value` with the key `key1`
m.get('key1'); // `value`
console.log(m.size); //1
m.has('key1'); // true
m.forEach((val, key) => {
  console.log(key, val); //output the keys and values
});

m.delete('key1'); //removes the value and key at `key1`
m.clear(); // empties the whole map
1
2
3
4
5
6
7
8
9
10
11

Maps and Sets also have keys(), values(), and entries() methods that return an iterator that can be used to loop through the keys, values, or both. Basic usage with a for...of loop is like this:

for (let val of myMap.values()) {
  console.log(val);
}
1
2
3

MDN reference for Map (opens new window)

# Future DataTypes

Tuples and Records are two new datatypes that are currently under consideration for addition into JavaScript. They will give us immutable versions of Arraysand Objects that can be put into JSON and extracted from JSON as well as letting us do deep comparisons between the objects.

# Tagged Template Literals

Template strings, which we have already discussed, allow us to inject variable values into our strings.

There is another feature of them, called tagged template literals which adds the passing of the template string to a function. The function will have access to all the parts of the string and the variables being injected. The functions return value will be the value of the string. The function is what is known as the tag.

//A basic template literal
let age = 157;
let str = `I am not ${age} years old`;

//A tagged template literal
function validateAge(stringParts, prop1) {
  //stringParts will be an array with each of the string segments around the variables
  //prop1 would be the first variable injected in the template string
  if (isNaN(prop1)) {
    //prop1 is not a number
    return 'I am ageless.';
  } else if (parseInt(prop1) > 130 || parseInt(prop1) < 0) {
    //number is too big or too small
    return 'I have an imaginary age.';
  } else {
    //we are good to go. Return what the template literal would have been
    return stringParts[0] + prop1 + stringParts[1];
  }
}
//our function returns one of three possible values to assign to str.
let str = validateAge`I am not ${age} years old.`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

The tag function takes, as its first argument, an array that will hold all the string segements inside the backtick characters. Split the whole string whereever there is a variable injected (interpolated) and whatever is to the left and right of the variable will be values in the array, even if the value is an empty string. Eg:

//the variable is at the start of the string
//so the first value in the array will be ""
let str = `${someVariable} is great`;
1
2
3

The second and subsequent argument values will be any and all variables that were injected into the template string. If there were 7 variables injected then you will have 8 arguments for your function - an array and 7 variables.

# Media Query Matching

In JavaScript there is a window.matchMedia method that allows us to know whether any media query tests would return positive on the current page.

To be clear, we are not looking at the CSS file and seeing if what is written in the CSS file is currently matching.

We are writing a media query in JavaScript and running the window.matchMedia method to see if it would populate the matches property with true or false.

There is no event that makes this test run. You can add any event listeners to your page that you want. The function that runs when that event happens would be able to run the window.matchMedia test.

The string that you pass to the window.matchMedia method can be anything that you would write in your CSS file following @media.

//
document.body.addEventListener('mouseup', (ev) => {
  //this function runs when the user clicks and releases their mouse or touch anywhere on the page
  //unless there is another function that calls ev.stopPropagation();
  let mymedia = 'screen and (orientation: landscape)';
  let match = window.matchMedia(mymedia).matches;
  //match will be either true or false
  if (match) {
    //the page is landscape orientation currently
  } else {
    //the page is portrait orientation right now
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13

# Stylesheet and CSS Methods

The best practice for working with styles of elements is almost always to simply add, remove, or toggle a CSS classname on a parent element. Let the browser's CSS rendering engine do the work of recalculating the CSS Cascade and changing the appearance of the page.

# how to get current stylesheets

The document.styleSheets property will return a StyleSheetList object that contains a list of CSSStyleSheet objects. Each object represents a single CSS file that has been attached to your page.

With the CSSStyleSheet object, you can inspect and modify the set of list of rules inside the stylesheet.

It is important to note that CSS stylesheets from different domains cannot be loaded and modified. A SecurityError will occur if you try.

# how to create new style rules

Once you have a CSSStyleSheet object then you can use the deleteRule(), insertRule(), replace(), or replaceSync() methods to modify the stylesheet.

These methods do NOT alter the original file on the server, just the current copy being used by the page.

//get all the stylesheets for the current page
let cssList = document.styleSheets;
//get the first attached CSSStyleSheet object
let firstCSS = cssList[0];

//delete the first css rule in the CSSStyleSheet object
firstCSS.deleteRule(0);

//add a new css rule at position index
let index = 0;
let rule = `.bigRed { font-size: 7rem; color: hsl(10, 80%, 50%); }`;
firstCSS.insertRule(rule, index);
//this new css rule will be added to the top of the css rules list

//create a new CSS StyleSheet and attach it to the page
let options = {
  baseURL: 'https://mydomain.com/folder', //baseURL used to determine relative urls in the css
  media: 'screen, print',
};
let css = new CSSStyleSheet(options);
//the replace() and replaceSync() methods ONLY WORK on CSSStyleSheet objects that you create yourself
//use these two methods to overwrite the entire contents of the stylesheet
// replace() returns a Promise that resolves to the new CSSStyleSheet object
// replaceSync() will synchronously replace the content of the CSSStyleSheet object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

You can use the CSSStyleSheet object reference page (opens new window) for a full list of possible errors and issues.

# loading new stylesheets

You can use the same DOM methods that we have already been using for weeks to load and attach new CSS files to our webpages. We can use the load event to trigger a function that let's us know that the new CSS has been added to the page.

let css = document.createElement('style');
css.setAttribute('rel', 'stylesheet');
css.href = 'https://www.example.com/new.css';
css.addEventListener('load', (ev) => {
  //the css file has been loaded
});
css.addEventListener('error', (ev) => {
  //there was an error trying to load the css
});
//actually put the new <style> element in the head to trigger the fetching of the css file
document.head.append(css);
1
2
3
4
5
6
7
8
9
10
11

# import css with es modules

A fairly new addition to the possibilities with ES Modules (still not supported by Safari 😡 ) is the ability to import CSS through your JS files.

CSS can be dynamically or statically imported. Dynamically means using the import() method from anywhere in your code. Statically means using the import keyword at the top of your file so it gets triggered on load.

# DataType Conversion and Comparison

When you are working with data that comes from user input, you will always be getting values that are strings. There are also many methods that will return string values when you might be wanting a Boolean or a Number.

Sometimes you want to know whether or not a certain HTML element exists on the page. JavaScript does automatic conversion to truthy or falsey values when you are trying to carry out a logical operation, like if, ternary, switch, and logical short-circuiting.

It is considered a best practice to use === instead of == when comparing two values. === will compare both the value AND the datatype for primitives.

Truthy and falsey will tell you if there is something in your variable, but not WHAT is inside your variable.

//declare a variable and assign it a value somewhere in your code
let elem = document.querySelector('.someClass');

//somewhere else in your code you want to see if it exists
if (elem) {
  //code here runs if there is an element with the css class `someClass`
  //truthy can be useful
}
//However the code inside that if statement would also run if we had done this:
// let elem = 42;

if (elem && typeof elem === 'object' && elem instanceof HTMLParagraphElement) {
  //code here runs if the variable elem contains a truthy value (42 would pass this)
  //AND
  //elem is an object (it is an object not a primitive)
  //AND
  //elem is an HTMLParagraphElement (the object is an HTMLParagraphElement )
  //we are being very specific to make sure that elem exists and it is <p class="someClass">
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

The typeof operator will tell you what kind of primitive a variable is, or if it is an Object.

The instanceof operator will tell you if a specific type of object was used to create an object.

What about numbers? When you start needing to know if a value is numeric or if a numeric value is inside a String, then it is time to start doing conversions. If you want to display a number as a hexidecimal or binary value then you need to convert the number to a String. If you need a truthy or falsey value as a Boolean then it is also time to do conversions.

So, while we may have truthy and falsey as automatic values, what about times when you need to have an actual Boolean? The double bang operator will give you an actual Boolean value instead of a truthy or falsey one.

let name = 'Karla';
if (name) {
  //name is truthy
}
if (name == true) {
  //fails because name is truthy not true
}
let nameIsTrue = !!name;
//converts name to a Boolean
//first ! changes 'Karla' (as a truthy value) into the opposite boolean - false
//second ! changes false into true

if (nameIsTrue == true) {
  //works because it is now a boolean
}
if (nameIsTrue === true) {
  //best practice using ===
  //also works because it is the right datatype and value.
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

The unary addition operator will convert any value into the Number equivalent. This is very useful if you have some user input, which you expect will be numeric, and you need an actual number. It can also be useful if you have a method that returns a string value that you know will be numeric.

let num = '17'; //looks like a number but is actually a string
if (num === 17) {
  //using the best practice of 3 equal signs
  //code here never runs because num is a String
}
if (+num === 17) {
  //now we are converting the String into a number before comparing
  //still using the best practice of === to compare
}
1
2
3
4
5
6
7
8
9

This last video talks about all the various conversions between data types and what happens if you are trying to compare one datatype to another.

# What to do this week

TODO Things to do before next week.

  • Read all the content from Modules 12.1, 12.2, and 13.1.
  • Finish the Hybrid Exercises
Last Updated: 5/31/2023, 8:15:38 PM