Skip to content

Week 13 - Class syntax, Observers, and more

JavaScript does not have actual “classes” like languages such as Swift, Kotlin, C++, C#, etc. JavaScript uses prototypes to define how object properties and methods are shared through inheritance.

However, because of 20 years of confusion over how the keyword this works, a desire to standardize the syntax for creating new objects, plus due to the number of developers coming to JavaScript from other languages in the last 10 years, a class keyword was added. A syntactic sugar was added to JavaScript that lets developers create objects with a class-like syntax.

Prototypes

To review the idea of prototypes again, every object has a function that creates it. An array is created by new Array(), an object is created with new Object(), and an HTML paragraph element can be created with new HTMLParagraphElement(). When you call document.createElement("p"), then JavaScript calls new HTMLParagraphElement() for you.

Every one of these functions that create objects has an attached object called the prototype object. When there are properties or methods that will be shared with every instance of every object created by the function, then those properties and methods get placed inside the prototype. This way they can be shared.

Every instantiated object automatically has access to the prototype that is attached to the function that created them. So, we could do something like this:

let arr = new Array(74, 47, 49); //creates [74, 47, 49]
//to sort this we want to call Array.prototype.toSorted();
//but we need to tell the method which array to sort.
//call or apply is a good way to do this
let sortedArr = Array.prototype.toSorted.call(arr);
//this gives us a new sorted array.

So, we can write out the path to the toSorted method inside the prototype and use call to pass the array to the method. However, that feels like a lot of extra work.

Instead, JavaScript let us pretend that the toSorted method exists inside our array. We can write our code like this:

let arr = new Array(74, 47, 49); //creates [74, 47, 49]
let sortedArr = arr.toSorted();

Behind the scenes, JavaScript will search up the prototype chain for the Array toSorted method in these places.

  • arr
  • arr.constructor.prototype
  • Object.prototype because this is the next step in the prototype chain above Array.prototype.
  • null if we reach here without finding the method throw an error like: Uncaught TypeError: arr.toSorted is not a function.

As soon as it finds the method, JavaScript stops searching and runs the method.

When we use the class syntax, as we create new properties and methods inside the class, these will all be automatically placed inside the prototype object. See the actual syntax further down the page.

If you want to create an Object in JavaScript there are a number of ways that you can do this.

//an object literal
//just write what you want as the props and use the default values for all property descriptors
let objLiteral = { id: 123, name: 'Steve' };
//internally, this will call new Object()
//use the Object constructor and pass it an object literal
let objInstance = new Object({ id: 123, name: 'Steve' });
//Object.create method
//pass in a prototype object reference and a properties object
let createdObj = Object.create(somePrototypeObjectReference, {
id: { value: 123, enumerable: true },
name: { value: 'Steve', enumerable: true },
});
//A constructor function
//when calling a function with `new` the return value will be your new object
//CANNOT do this with an ARROW function
function myBuilder() {
this.id = 123; //add properties to the object that will be returned
this.name = 'Steve';
}
let constructedObjInstance = new myBuilder();

You can then extend the functionality of any object by using the prototype keyword to add methods to the prototype object. The prototype is the place to put methods that will be shared by all instances of the object type.

//to add something to a prototype, we use the prototype property
//when we have access to the constructor function itself
myBuilder.prototype.someNewMethod = function () {};
//with the object instances we need to get to the constructor object to find the prototype object.
//the object instances are the objects that are built by the constructor function.
//Two ways to access the prototype property.
//1. with the `constructor` property
objLiteral.constructor.prototype.someNewMethod = function () {};
createdObj.constructor.constructor.prototype.someNewMethod = function () {};
objInstance.constructor.prototype.someNewMethod = function () {};
constructedObjInstance.constructor.prototype.someNewMethod = function () {};
// 2. by using the __proto__ as the shortcut for `constructor.prototype`
//this is a non-standard way to do this
objLiteral.__proto__.someNewMethod = function () {};

When ES6 was launched it added the class keyword to JavaScript as a syntactic sugar. This was an attempt to standardize (yet again) the way that objects are created in JavaScript and to make the language appear more familiar to the many developers migrating to JavaScript from other languages.

This does not mean that JavaScript has classes like object oriented languages, such as Java, do. JavaScript STILL uses prototype when creating the inheritance for objects. This is just a different way to write the code to extend and connect objects and their prototypes.

class myObjType {
pie = 3.14; //added to the instance, not the prototype
constructor() {
this.id = 123;
//properties added to the instance, not the prototype
this.name = 'Steve';
}
someNewMethod() {
//this method is added to the prototype
//note the shorthand syntax for defining the function without `function`
//methods are added to the prototype by default
//properties are added to the instance by default
}
}
//create one of your objects, just like with a function plus `new`
let myClassObj = new myObjType(); //calls the constructor() function
//myClassObj will have the two properties - id and name
//the prototype for myClassObj's constructor will hold the `someNewMethod` function

By using the class keyword we can build their constructors, define their properties and define their prototype methods in a more predictable way. This does not stop JS using the prototype chain or change how anything happens internally.

You will still use the Object literal syntax for 80%+ of what you do with objects in JavaScript. The class syntax just gives you an alternative standard to follow when things become more complex.

Object create method

Object assign method

Different ways to create objects

Prototype vs Class

To learn about the newer keyword static and private members (properties) and methods, in the JavaScript class syntax, watch the following video.

JS Classes in 2021

The Observer APIs allow us to create objects that watch selected parts of the DOM. If changes to the DOM elements we can automatically have a callback function run.

The resize observer can be used to duplicate the same functionality as a Media Query, but the real power is being able to watch specific DOM elements instead of the whole page and see if the desired element has changed to meet a dynamic size criteria, then we can do anything we want with JavaScript. We can add or remove elements from the page. We can fetch new content. We can apply new CSS. We can rearrange our page layout entirely.

The basic script works like this:

//create an observer passing in a callback function
let observer = new ResizeObserver(handleResize);
//tell the observer what to watch
observer.observe(document.querySelector('.something'));
//create your callback function
function handleResize(entries) {
//function will be sent an array of elements being observed
//each entry has a `target` property that points to the observed element
let myelement = entries[0].target;
myelement.className.add('hasChanged');
//each entry also has a `contentRect` object with width and height properties
console.log(entries[0].contentRect.width);
console.log(entries[0].contentRect.height);
}

And here is a video that explains the whole process.

resize observer

MDN reference for Resize Observer

The intersection observer is a very common observer. It is used to create effects on the page as the user scrolls. When an observed element intersects with an area of the screen, it triggers the callback and lets you run your script. Generally, the script will do something like add a css class to trigger a transition or animation. However, it could also do something like fetch new content from a remote API or the Cache.

The intersection observers work in a similar way to the resize observer. You create the observer and then tell it what to watch. There will be a callback function that runs when the intersections occur. The callback function will be pass the array of elements being observed with properties about each that you can use in deciding what you want to do.

The intersection observers need some extra options that you define when creating it. The first option is called root, and it defines the viewport that will be used to watch for intersections with the observed element. The second is rootMargin and lets you expand or shrink your viewport when watching for intersections. The final option is threshold and let’s you define a percentage of how much of the observed element must be intersecting with the viewport before calling the callback function.

//set up the options
let opts = {
root: null, //null means the whole screen. Otherwise it can be another element as the viewport
rootMargin: '0px -50px', //top-bottom and left-right values.
//positive means bigger than viewport. negative means inset from edges
threshold: 0.5, //percentage of observed element inside defined area. 0.5 == 50%
};
//create the observer with the options and callback function
let observer = new IntersectionObserver(handleIntersect, opts);
//tell it what to observe.
observer.observe(document.querySelector('.somediv'));

If you want to observe many elements then just call observe on each.

There is also an unobserve method that lets you remove an element from the set being observed.

function handleIntersect(entries) {
entries.forEach((entry) => {
//for each observed item report if it is currently intersecting
console.log(entry.isIntersecting); //boolean value
//use an if statement to do whatever you like
});
}

First video is a basic introduction to building an intersection observer and demonstrates effects on paragraphs as the user scrolls.

Intro to Intersection Observer

The second video shows how you could build an infinite scrolling system that dynamically loads new content as the user scrolls.

Infinite Scroll with Intersection Observer

MDN reference for Intersection Observer

The Mutation Observer will let you observe DOM elements and watch for changes to their textContent or attributes or children. It can be a useful observer to do things like highlight areas of the page when new content is added, changed, or removed.

Similar to the Intersection Observer, the Mutation Observer needs a set of options.

//set the options
const opts = {
attributes: true, //report if attributes are changed
attributeFilter: ['src', 'href'], //optional list of attributes to watch
attributeOldValue: false; //optional. if true old value will be saved for callback function
childList: true, //report if children are changed
characterData: false, //optional. if true, will save the text for the callback function
characterDataOldValue: false; //optional. if true old text value will be saved for callback function
subtree: false, //report if elements further down in the descendent tree are changed
//this last one, pluse the characterData ones can come with a performance hit.
};
//create the observer object with callback function and options
let observer = new MutationObserver(handleMutation, opts);
//add the element(s) you want to watch
observer.observe(document.querySelector('.somediv'));

You can add more elements by calling observe() again to add other elements to the observed set. You can always call unobserve() to remove elements from the observed set.

function handleMutation(mutations) {
//entries is the list of _MUTATED_ observed elements
//each will have a `type` property that indicates which type of mutation it is
switch (mutations[0].type) {
case 'childList':
//a child element was mutated
console.log(mutations[0].target);
//old and new values might be available if set in options
break;
case 'attributes':
//attribute was changed
console.log(mutations[0].target);
//we can find out which attribute was mutated
console.log(mutations[0].attributeName);
//plus the old and new values if set in options
break;
default:
//subTree mutation
}
}

Mutation Observer

MDN reference for Mutation Observer

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

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);

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];

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');

Destructuring is similar to the rest and spread syntax but it is more targeted 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

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.

What is Destructuring

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`.
}

Why Destructuring is Awesome for Developers

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;

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.

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'],
},
};

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);

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.

Destructuring with Array Methods and Promises

Destructuring with ES Modules

When working with Promises or other Asynchronous things like event listeners and timers, we can write our code in parallel or serial ways.

Parallel means that we can ask JavaScript to start multiple tasks before any of them complete.

Serial means that we wait for one task to complete before beginning the next.

There are valid reasons to use each. Eg: When a user logs into a website. We want to complete the authentication and have a valid response from the server before we let the user load the private data for the website.

Alternatively, if we need to get some data from the Cache API, and fetch some current data from an online API, read other data from localStorage, and get the users current geolocation, then all of these things could possibly be done at the same time.

//serial processing
function getUserData() {
let url = 'https://example.com/api/userdata';
return fetch(url).then((response) => {
if (!response.ok) throw new Error('No user data');
return response.json();
});
}
function getUserPosts(id) {
let url = `https://example.com/api/user/${id}/posts`;
return fetch(url).then((response) => {
if (!response.ok) throw new Error('No user data');
return response.json();
});
}
function saveUserPosts(posts) {
let cache = caches.open('cachename');
let str = JSON.stringify(posts);
let filename = 'userposts.json';
let file = new File([str], filename, { type: 'application/json' });
let response = new Response(file, { headers: { 'content-type': file.type, 'content-length': file.size } });
let request = new Request(`/${filename}`, { method: 'GET' });
return caches.open('cachename').then((cache) => {
return cache.put(request, response);
});
}
function init() {
//do these in order
getUserData()
.then((userDataJson) => {
return userDataJson.id;
})
.getUserPosts(id)
.saveUserPosts(posts)
.then(() => {
//completed all the steps
})
.catch((err) => {
//handle error from any step
});
}

By writing functions that return Promises, we can chain them together and be sure that they will run in order as each promise resolves. If any of the fail, they will all be redirected to the catch.

For parallel processing of tasks, we call all the functions separately and have a then() condition for each task. This way they can be handled as each completes. If one task takes longer it does not impact the others.

//parallel tasks
function getUserPosts(id) {
let url = `https://example.com/api/user/${id}/posts`;
return fetch(url).then((response) => {
if (!response.ok) throw new Error('No user data');
return response.json();
});
}
function getUserLocation() {
return new Promise((resolve, reject) => {
let options = {
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 600000,
};
navigator.geolocation.getCurrentPosition(resolve, reject, options);
});
}
function getPreviousTheme() {
return new Promise((resolve, reject) => {
let local = localStorage.getItem('theme');
if (local) {
let localObj = JSON.parse(local);
resolve(localObj.theme);
} else {
reject('No saved theme');
}
});
}
function init() {
let userid = 2342343;
//do all three of these things in parallel
getUserPosts(userid)
.then(handleUserPosts) //user posts passed in
.catch((err) => {
//handle failure to fetch user posts
});
getPreviousTheme()
.then(handleTheme) //theme object passed in
.catch((err) => {
//handle lack of saved theme
});
getUserLocation()
.then(handleLocation) //location object passed in
.catch((err) => {
//handle failure to get location
});
}

Each of the parallel function calls will have its own then and catch. Each of the parallel functions will have a return value, when successful, which will be automatically passed to the function inside the then().

Property Descriptors Controlling Enumeration

Section titled “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
};

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

So, we can change whether or not properties are enumerable… but what is it?

ENUMERABLE === appears in a for...in loop or call to Object.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
}

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',
};

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.

Iterable vs Enumerable

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"

Iterators and Generators

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

Custom Iterators and for of Loops

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.

Async Iterators for BigData

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 to false when created with Object.defineProperty().
  • enumerable: a boolean indicating whether the property will appear in a for..in loop or call to Object.keys(). Defaults to false when created with Object.defineProperty().
  • value: the actual value or reference for the property will be held in this descriptor. Defaults to undefined.
  • writable: a boolean that indicates whether the value 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 to false when created with Object.defineProperty().
  • enumerable: a boolean indicating whether the property will appear in a for..in loop or call to Object.keys(). Defaults to false when created with Object.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

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,
});

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,
},
});

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;
},
});

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.

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:

Property Descriptors

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.

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

MDN reference for Set

ES6 Set DataType

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

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);
}

MDN reference for Map

ES6 Map Datatype

Differences between Maps and Sets

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.

Records and Tuples

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.`;

The tag function takes, as its first argument, an array that will hold all the string segments inside the backtick characters. Split the whole string wherever 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`;

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.

Tagged Template Literals

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
}
});

Media Query Matching in JS

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.

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.

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

You can use the CSSStyleSheet object reference page for a full list of possible errors and issues.

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);

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.

Importing CSS and JSON with ES Modules

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.

truthy and falsey

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">
}

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 hexadecimal 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.
}

Double Bang Operators

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
}

Unary plus operator

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.

Datatype Casting and Conversion