Week 13 - Class syntax, Observers, and more
JS Classes
Section titled “JS Classes”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 thislet 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.
Object Creation
Section titled “Object Creation”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 descriptorslet objLiteral = { id: 123, name: 'Steve' };//internally, this will call new Object()
//use the Object constructor and pass it an object literallet objInstance = new Object({ id: 123, name: 'Steve' });
//Object.create method//pass in a prototype object reference and a properties objectlet 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 functionfunction 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 itselfmyBuilder.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` propertyobjLiteral.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 thisobjLiteral.__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
Observers
Section titled “Observers”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.
Resize Observer
Section titled “Resize Observer”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 functionlet observer = new ResizeObserver(handleResize);//tell the observer what to watchobserver.observe(document.querySelector('.something'));
//create your callback functionfunction 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
Intersection Observer
Section titled “Intersection 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 optionslet 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 functionlet 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
Mutation Observer
Section titled “Mutation 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 optionsconst 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 optionslet observer = new MutationObserver(handleMutation, opts);//add the element(s) you want to watchobserver.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
Spread & Rest Syntax
Section titled “Spread & Rest Syntax”The spread
and rest
syntax use the same characters ...
and can be seen as two parts of the same functionality.
Spread
Section titled “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 arraylet numbers = [12, 34, 56];//spread the array out into 3 values when passingf1(...numbers);
It also means that you can easily combine arrays too.
//2. combine arrays into a new arraylet arr1 = ['Luke', 'Leia', 'Han'];let arr2 = ['Chewie', 'C3P0', 'R2D2'];//create a new array that includes arr1 and arr2 valueslet 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
Section titled “Destructuring”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 codelet myArr = ['Prometheus', 'Covenant', 'Alien', 'Aliens'];let myObj = { title: 'Prometheus', year: 2012, director: 'Ridley Scott', starring: 'Noomi Rapace',};
//basic array destructuringlet [first, second, ...therest] = myArr;//we now have 3 variables first=Prometheus, second=Covenant, therest=['Alien', 'Aliens']
//base object destructuringlet { 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 blet 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 objectinfo(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
Parallel and Serial Processing
Section titled “Parallel and Serial Processing”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 processingfunction 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 tasksfunction 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()
.
Iteration and Enumeration
Section titled “Iteration and Enumeration”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); //12345console.log(descriptors.id.writable); //trueconsole.log(descriptors.id.enumerable); //trueconsole.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
Iterators
Section titled “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 4console.log(getSomething()); // output trueconsole.log(getSomething()); //output "what"
Iterators and Generators
for…of loops
Section titled “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.
Custom Iterators and for of Loops
Practical Custom Iterators
Section titled “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.
Async Iterators for BigData
Property Descriptors
Section titled “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 objectmyObj.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 functionconsole.log(myObj.email); //returns the lowercase version sample@work.orgmyObj.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
Maps and Sets
Section titled “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.
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 setnames.add('Tony');names.add('Robert');names.add('Su Cheng');console.log(names.size); //4names.delete('Tony'); //removes Tony from the setnames.delete('Vladimir'); // does nothing because Vladimir is not in the listnames.has('Robert'); //truenames.forEach((item) => console.log(item)); //loops through all the itemsnames.clear(); //empties the whole 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); //1m.has('key1'); // truem.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);}
ES6 Map Datatype
Differences between Maps and Sets
Future DataTypes
Section titled “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.
Records and Tuples
Tagged Template Literals
Section titled “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 literallet age = 157;let str = `I am not ${age} years old`;
//A tagged template literalfunction 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
Media Query Matching
Section titled “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 }});
Media Query Matching in JS
Stylesheet and CSS Methods
Section titled “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 Access Current Stylesheets from JS
Section titled “How to Access Current Stylesheets from JS”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
Section titled “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 pagelet cssList = document.styleSheets;//get the first attached CSSStyleSheet objectlet firstCSS = cssList[0];
//delete the first css rule in the CSSStyleSheet objectfirstCSS.deleteRule(0);
//add a new css rule at position indexlet 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 pagelet 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.
loading new stylesheets
Section titled “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 filedocument.head.append(css);
import css with es modules
Section titled “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.
Importing CSS and JSON with ES Modules
DataType Conversion and Comparison
Section titled “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
.
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 codelet elem = document.querySelector('.someClass');
//somewhere else in your code you want to see if it existsif (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 stringif (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.