13.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);
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];
2
3
4
5
# 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');
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
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`.
}
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;
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.
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;
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'],
},
};
2
3
4
5
6
7
8
9
10
11
12
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);
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.
# Property Descriptors
When you create an object
in JavaScript, you are using a constructor
function to create a container. That container, through its constructor
function, will have access to a prototype
object. The prototype
object is a container for shared methods and properties.
Every property that you add to your object
will also have its own set of properties, which define a set of abilities or settings for the one property. These are known as property descriptors
. They describe how JavaScript is allowed to interact with that property.
There are two groups of descriptors. Every property in a JavaScript object either has the data
descriptors or the accessor
descriptors. A data descriptor is a property with a value
that may or may not be writable. An accessor descriptor is a property described by a getter-setter pair of functions.
data descriptors
configurable
: a boolean indicating the property may not be deleted, nor can its other descriptors be changed. Defaults tofalse
when created withObject.defineProperty()
.enumerable
: a boolean indicating whether the property will appear in afor..in
loop or call toObject.keys()
. Defaults tofalse
when created withObject.defineProperty()
.value
: the actual value or reference for the property will be held in this descriptor. Defaults toundefined
.writable
: a boolean that indicates whether thevalue
is allowed to be updated with the assignment operator=
.
accessor descriptors
configurable
: a boolean indicating the property may not be deleted, nor can its other descriptors be changed. Defaults tofalse
when created withObject.defineProperty()
.enumerable
: a boolean indicating whether the property will appear in afor..in
loop or call toObject.keys()
. Defaults tofalse
when created withObject.defineProperty()
.get
: a function to use when accessing the value of the property.set
: a function to use when updating the value of the property.
When you create an Object property, if you do not set the property descriptors, then it will, by default, be created as a data descriptor property with all of its default values.
let myObj = {}; //create an empty object
myObj.id = 123; //create a property called id (as a data descriptor property)
myObj['name'] = 'Nandor'; //create a property called name (as a data descriptor property)
//properties created with this syntax will have configurable:true and enumerable: true and writable: true
2
3
4
If you want to define exact values for the property descriptors then you should use the methods Object.defineProperty()
or Object.defineProperties()
. The following snippet is the version of the above code, but using defineProperty
.
let myObj = {};
Object.defineProperty(myObj, 'id', {
configurable: true,
enumerable: true,
value: 123,
writable: true,
});
Object.defineProperty(myObj, 'name', {
configurable: true,
enumerable: true,
value: 'Nandor',
writable: true,
});
2
3
4
5
6
7
8
9
10
11
12
13
If we want to define multiple properties at the same time, we can use the Object.defineProperties
method instead, like the following.
let myObj = {};
Object.defineProperties(myObj, {
id: {
configurable: true,
enumerable: true,
value: 123,
writable: true,
},
name: {
configurable: true,
enumerable: true,
value: 'Nandor',
writable: true,
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Any object that we create can have a mixture of properties that use data
property descriptors and accessor
property descriptors. Each object property must be one or the other, but not all properties in the object have to be the same.
The accessor
version is used when you want to be able to control how the code using your object will be allowed to edit the values of the properties. The real difference between an accessor
property descriptor and a data
property descriptor is that the later has a value
and a writable
boolean for whether or not the value
can be changed, and an accessor
version uses the get
and set
functions to control the value, without actually having a value
descriptor.
Let's create an accessor
example by adding a property called email
to our myObj
object from the previous examples. We will create the get
and set
functions to control editing the value.
let myObj = {};
Object.defineProperties(myObj, {
id: {
configurable: true,
enumerable: true,
value: 123,
writable: true,
},
name: {
configurable: true,
enumerable: true,
value: 'Nandor',
writable: true,
},
});
let em = null; //the variable to hold the value of the email. This can be scoped to isolate it.
Object.defineProperty(myObj, 'email', {
get() {
return em.toLowerCase(); //always return the lowercase version of the email.
},
set(newValue) {
if (!newValue.includes('@') && !newValue === null) throw new Error('invalid email address');
//reject the newValue if it doesn't contain an @ sign or is null.
em = newValue;
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
To use our new email property we would assign or read the value just like any JS object property.
myObj.email = 'SAMPLE@Work.org'; //accepted via the set function
console.log(myObj.email); //returns the lowercase version sample@work.org
myObj.email = 'not a valid email'; //throws an Error.
2
3
These property descriptors will let us have finer control over the properties in our objects when we need to keep things hidden, secured against changes, or control changes to the value.
Learn about property descriptors in this video:
# Iteration and Enumeration
# Property Descriptors Controlling Enumeration
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, //has: .value, .writable, .enumerable, and .configurable
email: `steve@work.org`, //has: .value, .writable, .enumerable, and .configurable
};
2
3
4
If we want to access the values of the descriptors, we can use the method Object.getOwnPropertyDescriptors()
.
const descriptors = Object.getOwnPropertyDescriptors(myObj);
console.log(descriptors.id.value); //12345
console.log(descriptors.id.writable); //true
console.log(descriptors.id.enumerable); //true
console.log(descriptors.email.value); //'steve@work.org'
console.log(descriptors.email.configurable); //true
2
3
4
5
6
So, we can change whether or not properties are enumerable... but what is it?
ENUMERABLE === appears in a
for...in
loop or call toObject.keys()
Basically, it means that when you loop through the properties of an object, then the property either is available or not during the looping.
let obj1 = {
id: 321,
name: 'Vlad',
job: 'impaling people',
};
for (let prop in obj1) {
console.log(prop); // id, name, job will appear
}
let obj2 = {
id: 456,
name: 'ghenkis',
};
Object.defineProperty(obj2, 'job', {
configurable: true,
enumerable: false,
value: 'leading a horde',
});
for (let prop in obj2) {
console.log(prop); // only id and name will appear
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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',
};
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 own 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"
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
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
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);
}
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 Arrays
and 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.`;
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`;
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
}
});
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
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);
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 (now finally supported by Safari :happy: ) 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">
}
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.
}
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
}
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 13.1, 13.2, and 14.1
.