Keywords and Operations Overview
Abstract Concepts and Design Overview
Asynchronous JavaScript and XML (AJAX)
Classes in JavaScript Overview
JavaScript used to have updates far and in between. Recently, the European Computer Manufacturers Association (ECMA) has committed to regular, annual updates starting with ES 2015 (a.k.a. ES6). As of 2020, we are on ES 2019 (ES10). This guide outlines much of the new functionality.
JavaScript used to have a single identifier: 'var'. Now, it has three. These are listed in order of ascending preference.
'const' is used to create constants; values that cannot be re-declared. Unlike other languages, 'const' is mutable, but it is not re-assignable. We can declare some array 'const arr' and push to it, but it will forever remain an array.
It is a good practice to use const whenever possible.
'let' is a context specific variable identifier. It respects the block scope that it is in and is not hoisted to the top of the context. This is our most disposable variable identifier.
If const is not appropriate, it is good practice to use let.
'var' gets hoisted to the top of its context. This means, no matter where you declare it in the block, when the script runs, var is treated as though it was declared at the beginning of the block. This can create issues with looping and timeouts. So, what was once the standard and only identifier in JavaScript should now be used the least. We have far more scope and mutability control with 'let' and 'const'.
If let is not appropriate, it is better to refactor code to accommodate const or let, rather than use var, which is similar to declaring a global.
Arrow functions provide a shorthand means of writing functions and anonymous functions.
const foo = () => {
return "bar"
}
const timesTwo = params => params * 2The rest operator, '...', collects the remaining arguments to a function and returns them to us in an array.
Note: When the rest operator is not used as a parameter it is called a spread.
const printArguments = (a, b, ...c) => {
console.log(a); // 1
console.log(b); // 2
console.log(c); // [1, 2, 3, 4, 5]
}
printArguments(1, 2, 3, 4, 5);We may work on problems where we need to iterate over all of the arguments in an array. However, the 'arguments' keyword is not an array. Instead, we can use 'rest' to pass an array of arguments. This makes it far easier to use forEach and forIn loops without further processing.
Previously, we would need to do something like:
const sumArguments = () => {
const total = 0;
for(let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}But now, we can simply use rest to turn n arguments into an array of arguments. We use the new array function, reduce(), to add each value to an accumulator value, which is returned out. We can now pass n number of arguments and have a means of turning it immediately into an array of n elements.
Note: See the reduce() section for more on reduce's functionality.
const sumArguments = (...args) => args.reduce(acc, next) => acc + next);While the rest operator is used in function parameters, spread is used everywhere else. We use it to "spread out the values": The spread operator is used on arrays to spread each value out as a comma separated value. It is useful when you have an array, but what you are working with expects comma separated values.
// ES5
const combined = arr1.concat(arr2).concat(arr3);
// is the same as ES 2015's (ES6's)
const combined = [...arr1, ...arr2, ...arr3];This is useful when we have an array of values but are dealing with a function that accepts values individually. Before ES 2015 we would use apply(). Here, Math.max is looking multiple parameters; not an array of them. We can use spread to separate them into comma delimited values:
const arr = [3, 2, 4, 1, 5];
Math.max(arr); // NaN
Math.max.apply(this, arr); // 5 with ES 5 syntax
Math.max(...arr); // 5 with ES 2015 syntaxBelow, sumValues accepts three values, and we have an array of three values. Instead of passing each element individually we can use the spread operator to delimit the array on entry.
const sumValues = (a, b, c) => {
return a + b + c;
}
let nums = [12, 15, 20];
console.log(sumValues(...nums)); // 47, because each value gets assigned to a, b, cThe spread operator is a means of interacting with an array without mutating it, which in turn creates functional programming opportunities.
var arr1 = [1, 2, 3]
var arr2 = [4, 5, 6]
console.log([...arr1, ...arr2]) // [1, 2, 3, 4, 5, 6]
var places = ['New Haven', 'Northampton', 'Sacramento']
var iLive = ([...arr]) => {
console.log(`I live in ${arr.reverse()[0]}`) // Sacramento
}
var haveLived = ([...arr]) => {
console.log(`But I have lived in ${[arr[0], arr[1]].join(", ")}`) // Northampton, New Haven
}
iLive(places)
haveLived(places)
console.log(`[${[...places].join(", ")}]
// remains in standard order despite the spreadwise reversal.`)The exponentiation operator, double asterisks or **, is used for exponent math.
// ES 2015
let calculatedNumber = Math.pow(2, 4); // 16
// ES 2016
calculatedNumber = 2**4; // 16
calculatedNumber **= 2;It is a reserved keyword that is detrmined by how a function is called; its execution context. We use four rules to determine execution context:
- Global
- Object/Implicit
- Explicit
- New
This is not specific to JavaScript. However, JavaScript gives us some tools to redefine what 'this' is referring to, which is covered below. I have been told that this is not often used in production.
If 'this' is used outside of a declared object, it refers to its global context. In the browser, 'this' refers to the window, which in turn, owns all of the other objects in the DOM. So, one could use this in a global context to find anything in the DOM.
function whatIsThis() { return this }
function variablesInThis() { this.person = "Brandon" }
console.log(person) // Brandon
whatIsThis() // windowIf we write 'use strict' at the top of our script, we prevent use of the global context. This means that if we try to access this in a function context without an object it will be undefined. This prevents us from accidentally creating global variables, but means that we can only use this in the context of a specific object e.g. a JSON.
function whatIsThis() { return this }
function variablesInThis() { this.person = "Brandon" }
console.log(person) // TypeError, can't set person on undefined
whatIsThis() // undefinedWhen 'this' is inside of a declared object, 'this' refers to that object or the closest parent object.
var person = {
name: "Brandon",
hello: () => {return `Hello ${this.firstName}`} }, // "Hello Brandon"
determineContext: () => {return this === person }, // True
pet: {
talk: () => { return `Make noise ${this.name}` // Undefined, because pet doesn't own name.
determineContext: () => {return this === person }, // False, because this is a pet, not a person.
}
}
If we use 'this' in person, we access person. If we use 'this' in pet, we access pet.We can use Call, Apply, and Bind to solve these execution context problems by being explicit about what we want 'this' to refer to.
- Invoked immediately
- call(thisArg, a, b, c, ..., n)
Operations are invoked immediately invoked.
'call' can be used to reduced repetitive code. Both objects below have a hello function that does the same thing. We can reuse the first object's hello function by explicitly declaring what 'this' should be.
var brandon = {
name: "Brandon",
hello: () => {return `Hello ${this.firstName}`} }, // "Hello Brandon"
}
var jenny = {
name: "jenny",
hello: () => {return `Hello ${this.firstName}`} }, // "Hello Brandon"
}
brandon.hello.call(jenny) // 'this' == jenny, and will thus print "Hello Jenny" despite hello's implicit 'this' object being brandon.- Invoked immediately.
- apply(thisArg, an array of args)
Similar to apply, but only takes two arguments; we store additional arguments in an array.
var brandon = {
name: "Brandon",
addNumbers: (a, b, c, d) => {return this.name + " just calculated " + a + b + c + d }
}
var jenny = {
name: "jenny",
}
brandon.hello.apply(jenny, [4, 5, 6, 7]) // Jenny just calculated 22- Not invoked immediately.
- bind(thisArg, a, b, c, ..., n)
When bind is called later, it will know what 'this' should be. This is often used in asynchronous operations and is essential for currying. We can use bind when we are not sure what the arguments will be.
var brandon = {
name: "Brandon",
addNumbers: (a, b, c, d) => {return this.name + " just calculated " + a + b + c + d }
}
var jenny = {
name: "jenny",
}
var jennyCalcs = brandon.addNumbers.bind(1, 2, 3, 4)
jennyCalcs() // Runs when we call it instead of immediately. Returns 10.
var jennyCalcsAgain = brandon.addNumbers.bind(jenny, 1, 2)
jennyCalcsAgain(3, 4) // Again, runs when called, but here we add some additional arguments.
// Takes the initially bound 1,2 and combines it with this 3,4. Returns 10. While setTimeout is inside an object, because of the delay, 'this' no longer refers to brandon. After the delay, 'this' will instead refer to the global 'this'. In a browser, this would be the 'window' object.
However, we can solve this problem with a bind.
var brandon = {
name: "Brandon",
hello: () =>
{setTimeout( ()=> {
() => { return `Hello ${this.firstName }`} // "Hello Brandon"
}, 1000)
},
}We can solve this with bind. Here, we explicitly define what this is.
var brandon = {
name: "Brandon",
hello: () =>
{setTimeout( ()=> {
() => { return `Hello ${this.firstName }`} // "Hello Brandon"
}.bind(this), 1000)
},
}The 'new' keyword is a reserved keyword. It immediately creates a new object and this will also immediately refer to this new object.
function Person(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
var brandon = new Person ("Brandon", "K")Here, despite the shared variable names, 'this' will refer to the object properties; not the parameters. In fact, we have to use the new keyword, or else we will get a type error. We do not want to overwrite the existing Person function (not that we could), but instead, we want to create a brand new Person using this template.
// ==============================================================
// Standard Import and Export Examples
// ==============================================================
import defaultExport from "module-name";
import * as name from "module-name";
import { export1 } from "module-name";
import { export1 as alias1 } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export1 [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
var promise = import("module-name");
// Exporting individual features
export let name1, name2, …, nameN; // also var, const
export let name1 = …, name2 = …, …, nameN; // also var, const
export function functionName(){...}
export class ClassName {...}
// Export list
export { name1, name2, …, nameN };
// Renaming exports
export { variable1 as name1, variable2 as name2, …, nameN };
// Exporting destructured assignments with renaming
export const { name1, name2: bar } = o;
// Default exports
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
// Aggregating modules
export * from …; // does not set the default export
export * as name1 from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;
// ==============================================================
// Node.js Import and Export Examples
// ==============================================================
modules.export = yourModule or yourModule() // Node.js EXPORT
const yourModule = require("yourModule) ' Node.js IMPORTSome of these concepts may or may not be specific to JavaScript. Callbacks are common in every language, while Closures refer specifically to a means of JavaScript data encapsulation.
A singleton is an object and there can only be one instance of this object. In JavaScript, on getInstance, we create an instance if it doesn't exist. If it does exist, we return that instance.
var Singleton = (function () {
var instance
function createInstance() {
var object = new Object("I am the instance")
return object
}
return {
getInstance: function () {
if (!instance) {
instance = createInstance()
}
return instance
}
}
})()
var instance1 = Singleton.getInstance()
var instance2 = Singleton.getInstance()
alert("Same instance? " + (instance1 === instance2))A closure is a function that uses variables defined in outer functions that have previously returned. If the inner function does not make use of any outer variables it is not considered a closure.
function outer() {
var data = "closures are "
return function inner() {
var innerData = "awesome"
return data + innerData
}
}
console.log(outer() ) // Prints the inner function. We still need to treat it like a function, so ...
console.log(outer()() ) // Don't forget the additional '()'!
// Calls the outer, then immediately calls the inner: "closures are awesome"The inner() function can see the data variable, so it concatenates that with innerData and returns it all the way out. Note that we must explicitly call that inner function. outer() returns a function, so calling '()' after will evaluate that return as a function.
Here is another example.
// Addition w/ Closure Example
var outer = (a) => {
return (b) => {
// The inner function is making use of the variable 'a'
// This aws defined in the outer function.
// By the time this is called, that outer function has already returned.
// This inner function is a closure.
return a + b
}
}
console.log(outer(1)(2)) // 3
var storeSum = outer(5)(5) // 10
storeSum(10) // 15var counter = () => {
var count = 0
return () => {
return ++count
}
}
var c = counter()
c() // 1
c() // 2
c() // 3
// We can create a new instance of counter() to track a separate counter.The counter function keeps track of its own count and we are not allowed to access it. However, each time we call it, it increments count by 1. This means our counter remains protected; we have more privacy.
var company = () => {
var employees = ["Brandon", "Jenny"]
return {
getEmployees: () => {
return employees
},
addEmployee: (name) => {
employees.push(name)
return employees
}
}
}
c = company()
c.getEmployees() // Brandon, Jenny
c.addEmployee("Phil") // Phil
c.getEmployees() // Brandon, Jenny, PhilThe list of employees is protected, but we can still interact with it. This getter and setter pattern yields object oriented design encapsulation and privacy benefits.
A callback function is a function that is passed into another function as a parameter and invoked by that function at some time.
const foo = callback => {
console.log("foo")
callback()
}
const bar = () => {
console.log("bar")
}
foo(bar)
// echoes foo bar, because foo() calls bar() as its callbackAccept a callback function as a parameter.
The queue is an ordered list of functions waiting to be placed on the call stack. The Event Loop is functionality in the JavaScript runtime that checks the queue when the stack is empty. If the stack is empty, the front of the queue is placed on the stack.
Consider the following:
function square(n) {
return n * n
}
setTimeout(function() {
console.log("Callback placed on queue.")
}, 0)
console.log(square(2))One would tink that setTimeout's function callback gets placed ahead of the square(2) call, but it does not. function main() gets called, setTimeout() gets called and adds the callback to the queue, square(2) gets called, and then finally we get the callback from the queue despite it's 0 timeout.
A promise is an object that represents a task that will be completed in the future. It is asynchronous. We can use callbacks to handle incomplete and complete promises based on whether or not we were successful. Taking a number at a help counter is like a promise; you are being told you will receive help at some point. The help you receive when it is your turn is like the invocation of the callback.
In sum:
- Promises are a one time guaranteed return of some future value
- When the value is figured out, the promise is resolved/fulfilled or rejected
- A friendly way to refactor callback code
Using the promise constructor is less common. We typically use other libraries, like jQuery, to make promises, and handle them with .then() and .catch() chained on.
const promise1 = new Promise(function(resolve, reject) {
resolve([1, 2, 3, 4]);
reject("Error");
})
promise1.then((arr) => {
console.log("Promise resolved with data: " + arr);
}).catch(() => {
console.log("Promise rejected with data : " + arr);
})We can chain multiple promises together. This is critical when some initial promise relies on the completion of some other promise.
In my app, Backlogged, there are a lot of HTTP POST requests that update the content for various video game properties. For example, if I want to delete a game, I also want to delete all comments associated with that game. First, I search for the game in the database. If that game exists, I proceed to deleting comments associated with that game, if those comments exist. If, at any point, the database does not return some critical information, the promise errors and it is handled. Promises attempt to retrieve all of that critical information before it starts operating.
A method on the Promise constructor. it accepts an array of promises and resolve all of them or rejects them once a single one of the promises has been first rejected (fail fast).
If all of the promises have been fulfilled, Promise.all is fulfilled with an array of the values from the passed-in promises in the same order as the promises passed in.
Promises do not resolve sequentually, but they are always returned in the order they were promised.
// ES 2015 Promises without Promise.all
function.getMovie(title) {
return $.getJSON("https://omdbapi.com?t=${title}&apikey=thewdb");
}
var titanicPromise = getMovie("Titanic");
var shrekPromise = getMovie("Shrek");
var braveheartPromise = getMovie("Braveheart");
// ES 2015 Promises with Promise.all
Promise.all([titanicPromise, shrekPromise, braveheartPromise]).then(function(movies) {
return movies.forEach(function(value) {
console.log(value.Year); // 1997, 2001, 1995
}
});This is a special kind of function that ES2015 provides. Until recently, once a function was executed, it executed through completion. With the addition of Generators, we can pause and resume execution of a function.
- Generators allow us to pause/resume function execution
- They are created with an asterisk * after the function keyword
- When invoked, a generator object is returned with keys: value and done
- value is what is returned from the paused function using the yield keyword
- done is a boolean that returns true when the function completes
function* pauseAndReturnValues(num) {
for(let i = 0; k < num; i++) {
yield i;
}
}
const gen = pauseAndReturnValues(3);
gen.next(); // {value: 0, done: false}
gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: false}
gen.next(); // {value: undefined, done: true}We can place multiple yield keywords inside of a generator function to pause multiple times:
function* printValues() {
yield "First";
yield "Second";
yield "Third";
}
let g = printValues();
g.next().value; // "First"
g.next().value; // "Second"
g.next().value; // "Third"Generators implement a Symbol.iterator so we can iterate over a generator using a for in loop.
function* pauseAndReturnValues(num) {
for(let i = 0; k < num; i++) {
yield i;
}
}
for(val of pauseAndReturnValues(3)) {
console.log(val);
}
// 0
// 1
// 2We can use Generators for async operations in ES 2015. ES 2017 introduced some more streamlined means of doing this, but it is still important to be aware of this generator use, as it is fairly common.
function.getMovie(title) {
console.log("Starting");
yield $.getJSON("https://omdbapi.com?t=${title}&apikey=thewdb");
console.log("Ending");
}
const movieGetter = getMovieData("Titanic");
movieGetter.next().value.then(val => console.log(val));AJAX is not a library, framework, or technology. It's just an approach to web development. AJAX allows us to update a web page by sending/requesting data from the server, in the background, without disturbing the current page. This lead to today's single page applications; we no longer need to navigate to different pages. For example, as you scroll, on social media websites, the feed continues to populate. This data did not load up front. It instead waited for a scroll event to determine if the user needed more content and loaded an appropriate amount.
- XMLHTTP Request
- The Fetch API
- 3rd Party Libraries: jQuery, Axios, etc.
They are both data formats. We send requests to the server and get more content. That content is returned in some format. APIs do not respond with HTML. They respond with pure data; not structure. XML and JSON are pure data structures that return just the basics. If you consider HTML, in includes a lot of meta and structure data that we do not need. When we make a request, we only want what changed or what we need to add to the page; we already have some idea where information belongs. So, when we get our pure data, we just need to parse and place it.
XML is syntacticly similar to HTML but it does not describe presentation like HTML does. It is a more specific markup.
<pin>
<title>Adorable Maine Coon</title>
<author>Michelle K</author>
<num-saves>1800</num-saves>
</pin>JSON looks almost exactly like JavaScript objects. JSON is more popular these days because it works so well JavaScript.
'pin': {
'title': 'Adorable Maine Coon",
'author': 'Michelle K',
'num-saves': 1800
}This is the original form of making requests. Despite widespread support it is fairly verbose.
const getQuote = () => {
const zenXHR = new XMLHttpRequest(); // HTTP is -not- capitalized
zenXHR.onreadystatechange = function() {
if (zenXHR.readyState == 4 && zenXHR.status === 200) {
const quote = document.querySelector('#quoteHeader');
quote.innerHTML = zenXHR.responseText;
} else {
console.log('Error: ' + zenXHR.status);
}
};
zenXHR.open('GET', 'https://api.github.com/zen');
zenXHR.send();
};
const getDogImage = () => {
const dogXHR = new XMLHttpRequest(); // HTTP is -not- capitalized
dogXHR.onreadystatechange = function() {
if (dogXHR.readyState == 4 && dogXHR.status === 200) {
const image = document.querySelector('#dogImage');
const data = JSON.parse(dogXHR.responseText);
image.setAttribute('src', data.message);
} else {
console.log(`Error: ${dogXHR.status}`);
}
};
dogXHR.open('GET', 'https://dog.ceo/api/breeds/image/random');
dogXHR.send();
};
// LOAD
getQuote();
getDogImage();
// LISTEN
const requestButton = document.querySelector('#requestButton');
document.addEventListener('click', function(e) {
getQuote();
getDogImage();
});Fetch API is not supported by all browsers; most notably, Internet Explorer. However, this will become increasingly common.
const url = 'https://api.coindesk.com/v1/bpi/currentprice.json';
fetch(url)
.catch((res) => {
console.log(res); // Bitcoin price information
})
.then((error) => {
console.log(error);
});
fetch('someFakeUrl.app/login', {
method: 'POST',
body: JSON.stringify({
name: 'Purple',
username: 'PurpleTheCat'
})
})
.catch((res) => {
console.log(res); // Log Purple in!
})
.then((error) => {
console.log(error); // This is fake, so it will error
});Having to import jQuery just for the sake of AJAX requests seems excessive but, in lieu of widespread Fetch API support, may be the best, most simplistic alternative if HTTPXmlRequest is too verbose. We can:
- Use $.ajax to create a customized request
- Use $.getJSON if we know we're requesting JSON
- Use
$.getJSON.then((data) => {...}) to use modern Promise syntax instead of the $ .ajax success parameter
Here are some examples of this:
// Defaults to GET
$.ajax({
url: url,
contentType: "application/json",
dataType: 'json',
success: (data) => {
console.log(data);
}
});
$.ajax({
type: "POST",
url: url,
data: data,\
dataType: 'json',
success: (data) => {
console.log("Posted: " + data);
}
});
// We can even use this bootstrapped function if we know it's JSON:
$.getJSON({
url: url,
success: (data) => {
console.dir(data);
}
})
// And we can further simplify if we want to use a .then() to handle the response
$.getJSON(url).then((data) => {
console.dir(data);
});ES 2017 added some new asynchronous JavaScript functions. These are created using the keyword async. The purpose of asynch functions is to simplify writing asynchronous code; specifically, Promises.
Note: Async functions are not supported in every browser. Check compatability.
async function first() {
return "We did it!";
}
first(); // Returns a promise
first.then(val => console.log(val)); // "We did it!"However, the above example is synchronous. We can use the 'await' function to truly make this async friendly.
'await' is a reserved keyword that can only be used inside async functions. It pauses the execution of the asynch function and is followed by a Promise. The await keyword waits for the promise to resolve and then resumes the asynch function's execution. Finally, it returns the resolved value. It is similar to a 'pause' button or to the 'yield' in a generator function.
function.getMovie(title) {
let result = await $.getJSON("https://omdbapi.com?t=${title}&apikey=thewdb");
return result; // Will not return until 'await' is resolved
}
// We no longer need a .then() because await does that for us.
let movie = getMovie("The Neverending Story");We can also add async methods to objects:
let movieCollector = {
data: "The Neverending Story",
async getMovie() {
let response = await $.getJSON("https://omdbapi.com?t=${this.data}&apikey=thewdb");
console.log(response);
}
}
movieCollector.getMovie();As well as ES 2015 classes:
class MovieData {
constructor(name) {
this.name = name;
}
async getMovie() {
let response = await $.getJSON("https://omdbapi.com?t=${this.data}&apikey=thewdb");
console.log(response);
}
}
let m = new MovieData("The Neverending Story");
m.getMovie();We can use a try catch block to handle errors:
async function getMovie() {
try {
let response = await $.getJSON("https://omdbapi.com?t=${this.data}&apikey=thewdb");
console.log(response);
} catch(e) { console.log(`Error: ${e}`); }
}When we make two requests sequentually, the second does not begin until the first is resolved:
async function getMovie() {
try {
let response1 = await $.getJSON("https://omdbapi.com?t=${this.data}&apikey=thewdb");
let response2 = await $.getJSON("https://omdbapi.com?t=${this.data}&apikey=thewdb");
console.log(response1);
console.log(response2);
} catch(e) { console.log(`Error: ${e}`); }
}This defeats the purpose of async. Instead, we can await their resolved promise in parallel instead. This means that, instead of both awaiting the Promise creation and its result, we create the Promise, then only await the result:
async function getMovie(first, second) {
try {
let response1 = $.getJSON("https://omdbapi.com?t=${first}&apikey=thewdb");
let response2 = $.getJSON("https://omdbapi.com?t=${second}&apikey=thewdb");
let data1 = await response1;
let data2 = await response2;
console.log(data1);
console.log(data2);
} catch(e) { console.log(`Error: ${e}`); }
}We can use Promise.all to await multiple resolved promises.
async function getMovie(first, second) {
try {
var results = await Promise.all([
$.getJSON("https://omdbapi.com?t=${first}&apikey=thewdb");
$.getJSON("https://omdbapi.com?t=${second}&apikey=thewdb");
]);
let response1 = results[0];
let response2 = results[1];
console.log(response1);
console.log(response2);
} catch(e) { console.log(`Error: ${e}`); }
}In a non-class setting, instead of colon syntax, we can now declare functions inside objects as we would in other languages:
// ES5
const Person = {
sayHi: function() {
console.log("Hi!");
}
}
// ES 2015
const Person = {
sayHi() {
console.log("Hi!");
}
}We can now add property names while defining our object.
// ES5
const name = "Brandon";
const Person = {};
Person[name] = name;
// ES 2015
const Person = {
[name]: "Brandon"
}The prototype object is a base template that can be used to create new objects. We can also modify or extend the prototype to have additional functionality, though this is less common.
Remember, the new keyword creates objects from constructor functions:
let m1 = new Map() // Valid
let m2 = Map() // Type ErrorWhen we create a 'new' object:
- It creates an object out of thin air
- It sets the value of 'this' to be that object
- It adds a 'return this' to the end of the function
- It creates a link, accessible by __proto__, between the object created and the prototype property of the constructor function
Every constructor function has a property on it called 'prototype'. This is an object.
The prototype object has a property on it called 'constructor' that points back to the Constructor function.
Any time an object is created using the 'new' keyword, a property called '__proto__' gets created, linking the object and the prototype property of the constructor function.
This means that every 'new' object gets the same functionality as the prototype; it serves as a template for our objects.
________ ____________
(fn/class) --> .prototype --> [ Person obj ]
( Person ) <-- .constructor <-- [ .prototype ]
(______) [____________]
/.__proto__\
/ \
obj1 obj2
These are the two ways of defining an Object in JavaScript. The latter, with class syntax, is more modern. Regardless, accessing both oldPerson.prototype and Person.prototype yield a return value despite no explicit declaration; we get them for free.
Once we create a new Person, that new instance of Person gets its own property, __proto__, linking back to class Person's 'prototype' property:
function oldPerson(name) {
this.name = name;
}
class Person {
constructor(name) {
this.name = name;
}
}
oldPerson.prototype;
Person.prototype;
let p = new Person("Brandon");
p.__proto__;
p.__proto__ === Person.prototype; // True
Person.prototype.constructor === Person; // TrueBecause the class property 'prototype' is shared among all objects created by a constructor function we can add functionality after the class has been defined.
Person.prototype.isAwesome = true
brandon.isAwesome // True, thanks to __proto__When we call a function or property on and object, and that object does not specifically have it, it then checks the dunder proto to see if it lives there. If it doesn't find it there, it checks the next dunder proto, and so on, until it is found. This is because many objects are extended from other objects; they inherit all of those parent object functions and properties.
Curious about an Object and want to see its full functionality? Call the __proto__ property on it. If we use it on Array, we can see all of it's methods.
In older JavaScript "class-like" declarations we can leverage prototype to create new functions and properties available to every instance of that object.
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}Even if we are not using modern class syntax with 'extend' we can still pass methods and properties from one class to another.
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
function Student(firstName, lastName) {
return Person.apply(this, arguments);
}
Student.prototype = Person.prototype;
Student.prototype.status = function() {
return "I am a student!"
}
let p = new Person("Brandon");
p.status; // "I am a student!", but that's not right!However, this is not enough. If we create a new Person, we are still able to access Student's functions. This is because the prototypes are linked. Instead, we need to create a brand new object.
We can use Object.create to accomplish this. It creates a brand new function. The first paramater is what the prototype object should be for the newly created object.
We do not use 'new'. It does approximately the same thing but also adds additional, unnecessary properties on to the prototype object.
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
function Student(firstName, lastName) {
return Person.apply(this, arguments);
}
Student.prototype = new Person; // Incorrect
Student.prototype = Object.create(Person.prototype); // CorrectWe use destructuring to extract values from data stored in objects and arrays.
Previously, we would have to specifically retrieve a key from an object and assign it to a variable.
// ES5
const person = {
name: "Brandon",
email: "foo@bar.com"
}
let name = person.name;
name; // BrandonNow, we can directly unpack specific properties from objects. The first example requires that the variable name match the key name. However, in the second example, we can use colon syntax to create our own names.
// ES 2015
let {name} = person;
let {name:personsName, email:personsEmail} = person;
name; // Brandon
personsName; // Brandon
personsEmail; // foo@bar.comSimilar to objects, we can use destructuring to interact with arrays as well. Consider this value assignment:
// ES5
const arr = [1, 2, 3];
let a = arr[0];
let b = arr[1];
let c = arr[2];Now, we can do this in one line:
// ES 2015
const arr = [1, 2, 3];
let [a, b, c] = arr; // a = 1, b = 2, c = 3Consider these return value assignments:
// ES5
function returnNumbers(a, b) {
return [a, b];
}
let first = returnNumbers(5, 10)[0]; // 5
let second = returnNumbers(5, 10)[1]; // 10We can refactor and minimize the return value assignment footprint, too.
// ES 2015
[first, second] = returnNumbers(5, 10);We can also swap values:
const swap = (a, b) => {
return [a, b] = [b, a];
}
const swap = (arr, index1, index2) => {
return [arr[index1], arr[index2]] = [arr[index1], arr[index2]];
}Making copies of objects in JavaScript is not as easy as assigning one to another, because that is merely assigning a reference. We can use Object.assign to resolve this.
// Broken ES5 example
let o = {name: "Brandon"};
let o2 = o;
o2.name = "Jenny";
o.name; // JennyUsing Object.assign fixes this. It creates copies of objects without the same reference.
Note: If the first parameter is not an empty object, the new object will maintain a reference to the source object. Make sure to pass empty object {} first before the second parameter: The object you would like to copy.
// ES 2015
let o = {name: "Brandon"};
let o2 = Object.assign({}, o);
o2.name = "Jenny";
o.name // Brandon
o2.name // JennyThis does not create a deep clone. If there are objects inside of the objects that you are copying, they will still maintain references to the original object. You may need to use a different data structure or just instantiating a new instance of an object instead.
Maps are JavaScript's hash map. Maps may only have string keys, but their values may be anything. Prior to ES2015, objects were replacements for maps. They are similar to objects except that the keys can be any data type; not just strings.
Unlike Object.prototype, you cannot accidentally overwrite keys.
const m = new Map();- If you need to look up keys d ynamically
- If you need non-string keys
- If you are frequently adding/removing key-value pairs
- If you are operating on multiple keys simultaneously
WeakMap serves as a performant alternative to a Map. However, all of the keys must be objects and not primitives. WeakMaps cannot be iterated over. WeakMaps are less common.
Maps implement a Symbol.iterator. This means we can use a for of loop. See the Map keys and values methods for more.
Sets a key-value pair in the Map.
const m = new Map();
m.set("Key", "Value");
m.set(123, 456);
m.set("isNice", true);Deletes a key-value pair, by key name, from the Map.
const m = new Map();
// ...
m.delete(someKey);Gets a key-value pair in the Map. We can use this in tandem with Set to update a key-value pair.
const m = new Map();
// ...
m.get(key, value);Determines if the Map contains some key.
const m = new Map();
// ...
m.has(someKey);This is a MapIterator of all keys in the map. This allows us to use a for of loop.
const m = new Map();
// ...
m.keys() // returns all keysThis is a MapIterator of all values in the map. This allows us to use a for of loop.
const m = new Map();
// ...
m.values() // returns all valuesconst m = new Map();
// ...
m.size // n size by number of key-value pairsA data structure in which all values are unique. They exist in a few other languages but are new to JavaScript with ES 2015.
- Any type of value can exist in a set
- They are created using the new keyword
Here is a set implementation. Methods are described after the snippet:
const s1 = new Set;
const s2 = new Set([3, 1, 4, 5]); // Can be instantiated with an array
const s = new Set;
s.add(10);
s.add(20);
s.add(10); // Won't be added since it is already included
s.size; // 2Much like Maps and WeakMaps, there is a performant WeakSet option. WeakSet only accepts objects as values.
Sets implement a Symbol.iterator so we can iterate over them using a for of loop.
Adds a specific value to a set.
Deletes a specific value from a set.
Detects if a set has some value.
Returns the size of a set.
ES 2015 introduced the rest operator. Now, we can use rest to gather the remaining keys (the "rest" of the keys) and values in an object and create a new one out of them:
let person = {first: "Brandon", last: "K", job: "Software Engineer", pet: "Purple"};
let {first, last, ...data} = person;
first; // Brandon
last; // K
data; // {job: "Software Engineer", pet: "Purple"}ES 2015 introduced the spread operator. Now, can use spread on objects to spread out keys and values from one object to another. It can be used for creating objects starting with default values and is a more concise alternative to Object.assign.
Note: This is very common in React and Redux.
let person = {first: "Brandon", last: "K", job: "Software Engineer", pet: "Purple"};
let person2 = {...person, first: "Jenny", last: "X", job: "OMBUDS"};
person2.first; // Jenny
person2.last; // X
person2.job; // OMBUDS
person2.pet; // PurpleThis section outlines some of new additions to arrays, as well as recaps existing functionality for the sake of comprehension.
Allows us to convert an array-like object into an array.
// ES5
var divs = document.getelementsByTagName("div"); // returns an array-like object
divs.reduce; // undefined, since it is not an actual array
var converted = [].slice.call(divs); // Convert into an array
converted.reduce // function reduce() { ... }Instead of the array slice call syntax above, we can now use Array.from():
// ES 2015
let divs = document.getElementsByTagName("div");
let converted = Array.from(divs); // An array of divs with full array functionality!Adds a value to the end of the array.
arr.push(3)Removes the value at the end of the array.
arr.pop()Removes a value from the beginning of the array.
arr.shift()Adds a value to the beginning of the array.
arr.unshift(3)- Iterates through an array
- Runs a callback function each value in the array
- When the loop ends, forEach returns 'undefined'
forEach is called on the array and accepts an anonymous function with three potential arguments:
- value: The current value
- index: The current value's index in this array
- array: The entire array
[1, 2, 3].forEach( (value, index, array) => {
// ...
});Instead of a standard for loop, we can use a 'for in' loop to streamline writing it. It is traditionally used to loop over keys in an object.
for(let i = 0; i < someArray.length; i++) { let index = i }
// Instead, we can:
for(let i in someArray) { i = 0, 1, ... n }Taking the 'For In' a step further, 'for of' will refer directly to the value in the array. We no longer have to worry about keys or indices with a 'for of' loop.
for(let i = 0; i < someArray.length; i++) { let value = someArray[i] }
// Instead, we can:
for(let value of someArray) { i = 50, i = 73, ... i = nth value }forEach always returns undefined, but very commonly, we want to transform one array into another array with different values. We could use a forEach and push values into a new array or we could use map.
Once invoked on an array:
- It creates a new array
- It iterates over the array it was called on and adds the result of that callback function to the new array
- It returns a brand new array
Map returns a new array of the same length that it was invoked on.
const arr = [1, 2, 3];
const doubleArray = (arr) => {
return arr.map((value, index, array) => {
return value * 2
});
}
doubleArray(arr); // Returns [2, 4, 6]Just like map and forEach it accepts a callback function. However, the result of the callback function is a boolean.
- Creates a new array
- Iterates through an array
- runs a callback function on each value in the array
- If the result of the callback returns true that value will be added to the new array
- Otherwise, it will be ignored
const arr = [1, 2, 3];
const filterEven = (arr) => {
return arr.filter((value, index, array) => {
return value % 2 === 0
});
}
filterEven(arr); // Returns [2]- Iterates through an array
- Runs a callback on each value in the array
- If the callback returns true for at least one single value it returns true
- Otherwise, it returns false
const arr = [1, 2, 3];
const filterEven = (arr) => {
return arr.some((value, index, array) => {
return value % 2 === 0;
});
}
filterEven(arr); // Returns True because the array has an even number- Iterates through an array
- Runs a callback on each value in the array
- If the callback returns true if every item in the array meets the criteria
- Otherwise, it returns false
const arr = [1, 3, 5];
const filterEven = (arr) => {
return arr.every((value, index, array) => {
return value % 2 === 0;
});
}
filterEven(arr); // Returns True because all the numbers are oddReduce allows us to take an array and turn it into another data structure such as a single integer value, a string, or an object. It accepts a callback function and an optional second parameter.
The first parameter to the callback is either the first value in the array or the optional parameter. The first parameter to the callback is often called the accumulator.
- Iterates through an array
- Runs a callback on each value in the array
- The returned value from the callback becomes the new value of accumulator
Whatever is returned from the callback function becomes the new value of the accumulator.
[1, 2, 3].reduce(function(accumulator, nextValue, index, array) {
/* Whatever is returned in here will be the value
* of the accumulator in the next iteration
*/
}, optional second parameter)const arr = [1, 2, 3, 4, 5]
let result = arr.reduce((accumulator, nextValue) => {
return accumulator + nextValue
});
console.log(result); // The sum of the array: 15const arr = [1, 2, 3, 4, 5]
const result = arr.reduce((accumulator, nextValue) => {
return accumulator + nextValue
}, 10);
console.log(result); // The sum of the array,
// but the accumulator starts at 10,
// So the result is 25 (15 + the 10 starter)const arr = ["Brandon"]
const result = arr.reduce((accumulator, nextValue) => {
return accumulator + nextValue
}, "This guide was brought to you by ");
console.log(result); // "This guide was brought to you by Brandon"const arr = [5, 4, 1, 4, 5];
arr.reduce((accumulator, nextValue) => {
if(nextvalue in accumulator) {
accumulator[nextValue]++;
} else {
accumulator[nextValue] = 1
}
return accumulator;
}, {});
/* Returns
{
5: 2,
4: 2,
1: 1
}
*/It returns the value found or undefined if it is not found. Find accepts a callback function with value, index, and array; similar to forEach, map, filter, etc.
const people = [{name: "Brandon"}, {name: "Jenny"}];
people.find((value) => {
return value.name === "Brandon"; // "Brandon" || undefined
});Finds and returns the index, or, if no match, -1, of a matching item.
const people = [{name: "Brandon"}, {name: "Jenny"}];
people.findIndex((value) => {
return value.name === "Brandon"; // 0
});Returns a boolean value on whether or not an array contains some value.
const people = [{name: "Brandon"}, {name: "Jenny"}];
people.includes((value) => {
return value.name === "Brandon"; // True
});Features methods related to the Number object in JavaScript.
Checking to see if a number is not a number is difficult. Now, we can use Number.isFinite to check whether a number is a number and is not 'NaN':
function seeIfNumber(val) {
if(Number.isFinite(val)) {
return "It is a number!";
}
}This section is dedicated to newer string methods.
Using string templating we can embed variables in our text using double ticks, dollar sign $, and curly braces {}.
let name = "Brandon";
console.log(`My name is ${name}`;Allows us to pad the start of the string. padStart accepts two arguments:
- The total length of the new string
- What to pad with from the start. Defaults to empty space
This is useful when we want to make sure a set of strings are all the same length.
Similar to padStart, except that it allows us to pad the end of a string.
ES2015 introduced the class keyword and class syntax similar to other languages including, but not limited to:
- class declarations
- class constructors
- class helper method declarations
- Inheritence with Extends
- The 'super' keyword to access parent functionality
A class is a blueprint for creating objects with pre-defined properties and methods. While it is a staple of object oriented programming, JavaScript does not have built-in support for object oriented programming. The 'class' keyword is a new way to interface with JavaScript's object prototype.
- It is an abstraction of constructor functions and prototypes
- The 'class' keyword creates a constant
- The 'class' keyword does not hoist; make sure it is at the top
- We use 'new' to create instances of class objects; else you will receive a type error
The constructor keyword is used to create requirements for the creation of a new instance of this object. Without this information, an instance of this object cannot be created. Classes can have multiple constructors.
Constructor methods are used to instantiate an instance of this class. We may have multiple constructors to accommodate different input. In some cases, we may have less information and use default values to fill in the missing information. In others, we may have more complete information, and instantiate with the summation of that.
Standard methods are declared without any additional keywords. They are used to interact with 'this' instance of some object. Getters and Setters are examples of instance-specific methods.
The static keyword is used to create methods that do not require an instantiated object. This is useful for helper methods that have to do with the object but do not necessarily require specific object information. They allow us to interface with objects of this type rather than one specific object. For example, the Student class below may have a static method that serves as a Student factory. Or, in this case, emails all students.
Note: Static functions lead with an uppercase unlike their object specific counterparts.
In ES5 a class-like declaration would look like this:
// ES5
function Student(firstName, lastName) {
this.firstName = firstname;
this.lastName = lastName;
}
Student.prototype.getName = function() {
return this.firstName + " " + this.LastName;
}
var brandon = new Student("Brandon", "K");
brandon.getName() // Brandon KNow, we can use class syntax to accomplish similar functionality:
// ES 2015
class Student {
constructor(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
this.classesPassed = 0
}
getFullName() {
return "`Your full name is ${this.firstName} ${this.lastName}`"
}
passedClass() {
this.classesPassed++
return "`You have passed ${this.classesPassed} class(es)!`"
}
static EnrollStudents(...students) { // Does not pertain to one particular student
// Email students that were passed
// (Note: We use the spread operator to indicate that there may be an unknown number of students)
// (See the spread operator section for more)
}
}
let s = new Student("Brandon", "K")
s.firstName // "Brandon"
s.lastName // "K"
s.getFullName // "Your full name is Brandon K"
s.passedClass() // "You have passed 1 class(es)!"
// Imagine I instantiate a few new students here.
// We can call EnrollStudents, a static method, directly, without relying on other students.
Student.EnrollStudents(student1, student2, student3)We use inheritance to pass along methods and properties from one class to another.
For example, we may have class Pet, which has some generic properties such as name, owner, veteriarian, favorite toy, and favorite food. However, despite sharing some similarities, cats and dogs may have additional properties that make them more unique such as number of naps, number of times played fetch, and more.
We can abstract away some of the common property work using inheritance:
-> Cat
Pet -|
-> Dog
Here, Cat and Dog are derived from Pet; they inherit all of the properties of Pet and include some of their own.
They may both have a speak() method but Cats and Dogs certainly do not sound the same. We an create a speak() method in Pet, as a template, but not define it until Cat and Dog have a defined "sound" property. Pet, on its own, may have an undefined sound property, as a Pet is more abstract than our concrete Cat and Dog derived, child classes.
// ES5 Inheritance
function Pet(name, owner) {
this.name = name;
this.owner = owner;
this.sound = undefined;
}
Pet.prototype.speak() = function() {
return this.sound; // undefined
}
function Cat(sound) {
Person.apply(this, arguments); // Useful for many arguments
this.sound = sound;
}Now, we can use 'class' and 'extends' to accomplish inheritance:
// ES 2015
class Pet {
constructor(name, owner) {
this.name = name;
this.owner = owner;
this.sound = undefined;
}
speak() {
return this.sound; // undefined
}
}
class Cat extends Pet {
constructor(name, owner, sound) {
this.name = name;
this.owner = owner;
this.sound = sound; // Meow
}
// speak() is inherited
}
class Dog extends Pet {
constructor(name, owner, sound) {
this.name = name;
this.owner = owner;
this.sound = sound; // Woof
}
// speak() is inherited
}We can dry this up even more using the 'super' keyword.
We can use the 'super' keyword to access parent methods. It invokes a method by the same name in the parent.
class Pet {
constructor(name, owner) {
this.name = name;
this.owner = owner;
this.sound = undefined;
}
speak() {
return this.sound; // undefined
}
}
class Cat extends Pet {
constructor(name, owner, sound) {
super(name, owner);
this.sound = sound;
}
}We can now use default parameters in JavaScript. If an argument is missing, we can default to certain values:
const add = (a = 0, b = 0) => {
return a + b
}