Skip to content

Week 7 - Beyond Basic JS

When you build a web page that contains multiple HTML files, it means that each time the user navigates to a new page, the JavaScript is being reloaded.

When the JavaScript file reloads, it means that it loses all the values that it had in its variables. The DOMContentLoaded event fires again. Your init function will be running again. Event listeners are re-added.

If you have code that runs differently depending on which page you are on, you can check for things like an id attribute in the <body> tag to see which page you are on. A switch statement is usually a good way to check for and write the page specific code.

let id = document.body.id;
switch (id) {
case 'home':
//on the home page
break;
case 'contact':
//on the contact page
break;
case 'profile':
//on the profile page
break;
default:
//on any other page
}

In addition to organizing your code across your project, it is also important that you understand some of the terminology and practices that describe how you can structure your code within you modules and namespaces.

As already discussed, when you attach multiple script files to the same webpage, they are all able to see each other’s variables within the same global scope.

This may not be an issue when you are a single developer or a small team. You can coordinate and watch for name conflicts with your variables and objects.

It becomes much more difficult when you start to create libraries to reuse across projects, or include libraries written by others. The chance of naming conflicts starts to rise.

A common approach to solving this problem is to create namespaces. You can wrap all your project code or even parts of your project code inside of a single object. Use const to declare the object and be careful that your object name is unique. Your Object is your namespace.

A namespace would look like this:

const MYAPP = {
apikey: 'SomeUniqueAPIKey',
today: new Date(),
init: function () {
//an initial function that we will call in our namespace
},
someMethod: function () {
//some method belonging to our namespace
},
};

The namespace has a couple property values and a couple methods.

If you wanted to access one of the properties or one of the methods inside the namespace then you just have to put the name of the const in front of the property or method.

MYAPP.apikey; // gets the value of the apikey property inside of MYAPP
MYAPP.someMethod(); // call the someMethod function
document.addEventListener('DOMContentLoaded', MYAPP.init);
//call the init function WHEN the DOMContentLoaded event happens

A common practice is to wrap all your project code inside one or two namespace objects and then have one listener for the DOMContentLoaded event that will call some initial method in one of your namespace objects.

//APP namespace
const APP = {
//this is the primary namespace to control user interaction, events, data loading
init: function () {
APP.addListeners();
},
addListeners: function () {
//function to add event listeners to the app each time the page loads
window.addEventListener('pageshow', NAV.pageSpecific);
document.querySelector('form#searchForm').addEventListener('submit', APP.doSearch);
},
doSearch: function (ev) {
//check for keyword and do a search
},
};
//NAV namespace
const NAV = {
//this object deals with history and navigation and form submission
pageSpecific: function (ev) {
//check for id on body element and do page specific things
},
};
//start everything
document.addEventListener('DOMContentLoaded', APP.init);

Remember when you use namespaces to include the namespace in front of the method or property name.

In recent years, a new ability was added to browsers that let us scope each of our namespaces or imported files separately.

With ES Modules we define a first file to load and then start to import other scripts into the first file. Each of the files that we import gets its own scope and gets to define exactly what is allowed to be exported from itself. The first file needs to be loaded by a <script> tag with the type="module" attribute added. All the other scripts will be loaded via import statements inside the first JS file.

<script src="js/app.js" type="module"></script>

After we added the type attribute we can start adding import statements to our app.js. Alternatively, you can use the .mjs file extension for files that you want to use as modules.

app.js
import { someFunc } from './utils.js';
import { otherFunc, someConst } from 'https://example.com/scripts.js';

We now have the ability to make variables, objects, and functions private. They can be part of our code but we control who is allowed to access them.

As mentioned, to be able to import things into our main JS file, we need to explicitly export them from the other files. With no export there will be nothing to import.

We use the export keyword to export an object. Inside that object we list the items that will be available through import. If a function or variable is not listed inside the export object then you can consider it to be private to

const PI = 3.14;
const name = 'Steve';
function f1() {
console.log('Hello', name);
//we can use the name variable here without exporting it
}
//we are exporting PI and f1
//name is private to this file
export { PI, f1 };

If there is only going to be one thing exported from a file, then we can add the keyword default to an export statement. In this way, the default export becomes an exported object instead of being wrapped in one. In the import for a default export we can also use any name we want.

bob.js
export default function bob() {
console.log('this function can be called anything in the import');
}

The import statement for the function bob would look like this:

app.js
import frank from './bob.js';

Notice how you can use a different name because bob was the default export.

It is also possible to have a default export as well as other exports.

export default function bob() {
//this is the default export
}
function f1() {
//allowed to be exported but is not the default
}
function f2() {
//allowed to be exported but is not the default
}
export { f1, f2 };

There are a number of ways we can import scripts.

import * as thing from './script.js';
//all the things exported from script.js will be wrapped in thing
import { a, b, c } from './script.js';
//importing a, b, and c from script.js
//there could be other things exported that we didn't need.
import { a as x, b as w, c } from './script.js';
//importing a, b, and c from script.js
//a is renamed as x
//b is renamed as w
//c is left with its original name
import bob from './script.js';
//this means that bob was a default export
import bob, { a, b } from './script.js';
//bob would be the default export.
//a and b are non-default exports from the same file.

ES Modules

Another thing that you can do with imports is make the import conditional. We can wait for something to happen - like the user visiting a certain page, or spending a minimum amount of time on the site, or filling out a form, or logging in. Then once that goal is achieved then we can dynamically import another script.

//using async/await with dynamic import
async function getScript() {
const { default: myDefault, foo, bar } = await import('./js/someScript.js');
//now we know that the script is loaded...
//we use destructuring to get the items from the imported script
}
//alternatively
import('./js/someScript.js').then(({ default: defaultObj, obj1, obj2 }) => {
//now we can use defaultObj, obj1, and obj2
});

Dynamic Imports

A callback function is just a regular function, like any function that you have written so far. What makes a function into a callback function is simply how you use it.

When you pass a function reference to a second function so that it can be called, from inside the second function, after the rest of the second function code is complete, then the function reference being passed in is called a callback function.

function myCallback() {
//this function will be called by another function as a callback
console.log('This is the callback');
}
function countToTen(cb) {
//cb is a variable that will hold the callback function reference
for (let i = 1; i <= 10; i++) {
console.log(i);
}
cb(); //make the callback function run now
}
countToTen(myCallback);
//call the countToTen function
//pass in the reference to the myCallback function
//note there are no parentheses after `myCallback`

The following video explains what callback functions are.

Callback Functions

This video is a sample interview question, which will let you check if you understand what a callback is and how it works. It also provides the solution.

Callback Function Test

Functions in JavaScript are called first-class objects because they can be passed around just like any other variable. We can pass a function reference to another function, like we do for callback functions. We can also return a function from a function.

function f1() {
console.log('this is function f1');
return function () {
console.log('this is an anonymous function');
//this function will be returned from f1
return 42;
};
}
const f2 = f1();
//call the function f1 and put it's return value into f2.
//f2 is now a reference to the anonymous function returned by f1
let num = f2(); // runs the anonymous function returned by f1
console.log(num); // outputs 42, which is the return value of the anonymous function in f2

When a function calls itself, this is known as an recursive function. Functions can recursively call themselves multiple times. This does mean that we can create an infinite loop with a recursive function. So, we need to be always have an escape condition to stop the recursive calls.

function it() {
it(); //iteratively call the function `it`
}
it(); //start the infinite iterative looping

While, technically this is an iterative function call, it does have one huge problem - it never stops calling itself.

When you iteratively call a function you need to have an exit condition.

Let’s do the same thing but say that we only want to call the function 10 times.

function it(count = 0) {
//if the function is called without a `count` argument then `count` will be set to zero.
count++;
//increment the value of `count`
if (count <= 10) {
it(count); //iteratively call the function `it` with the current value of `count`
//but only if count is less than or equal to ten
}
}
it(1); //call `it` with an initial value of 1

Closures are not something that you necessarily learn how to write in JavaScript, they are a feature of the language. Closures are something that you need to be aware of and understand how they impact the scope of variables.

When you call a function, an execution context gets created with its own scope. Inside this context you can declare variables or create other functions. When creating the other functions they will understand what their execution context is (where they were created). If you declared variables in the same execution context as where the function is being created, then the function will be aware of those variables too.

function f1() {
let name = 'Jon';
return function () {
console.log(name);
};
}
const f2 = f1();
//call `f1`, which will create the variable `name` and the anonymous function
//the anonymous function is put into `f2`.
f2();
//call `f2`.
//`f2` needs a variable called `name`.
//There is no variable called `name` declared or assigned a value inside of `f2`
//JavaScript will look inside the execution context where the anonymous function was created
//inside that execution context there WAS a variable called `name`. So, it gets used

So, when function A runs and it returns function B, the fact that B has access to anything that was declared inside the execution context of A is called a closure. We are creating a bubble around all the potential variables inside that execution context to prevent them being deleted by the garbage collection process.

Normally, when you run a function and that function finishes running, all the locally declared variables are no longer needed. JavaScript is allowed to delete them and free up the memory.

A closure will prevent the garbage collection as long as the returned function exists / is referenced.

When you intentionally write a function which returns a second function, and the second function references variables that are inside its own scope plus variables that came from its execution context, this is known as currying.

Currying is a great way to dynamically create a series of functions that are slight variations of the same functionality, without having to actually write all the function variations.

Let’s say that we want to create a series of message functions. We have messages that are informative, ones that indicate success, and ones that indicate failure. Each of those three will have a different css style, need to include the name of the current user, and will not know the message text until later on when the user is interacting with the web app.

const message = function (username, type) {
let div = document.createElement('div');
let h2 = document.createElement('h2');
h2.textContent = `Attention: ${username}`;
let p = document.createElement('p');
p.className = `message ${type}`;
div.append(h2, p);
//we have created a message box that can be used later by the returned function below
return function (msg) {
p.textContent = msg; //add the message to the div > p
document.body.append(div); //add the div to the body
//this part of the function runs later on.
};
};

So, message is a function that we can use to create a message box. The user’s name will be contained in the heading for the message box. The actual message gets added later when the message box will be added to the webpage.

With the code from above we can now do the currying and create the function variations.

let user = 'Sam';
const info = message(user, 'infoMsg');
const success = message(user, 'successMsg');
const fail = message(user, 'errorMsg');

We now have three functions info, success, and fail, which can all be called at any point later when we need one of them.

success('Congrats! It worked');
info('You are currently logged in');
fail('Your credit card was declined');

All three will use the div that we created originally that contains the CSS classname and the username to display a message to the user. The message to the user was only created when one of those three functions was actually called.

The keyword this can be a confusing one in JavaScript. It is typically a reference to the object that made a function run.

For the purposes of this discussion we will limit the use of this to functions triggered by event listeners.

let btn1 = document.getElementById('myButton');
let btn2 = document.querySelector('.btn');
let btn3 = document.querySelector('#otherButton');
btn1.addEventListener('click', makeNoisy);
btn2.addEventListener('click', makeNoisy);
btn3.addEventListener('click', makeNoisy);
function makeNoisy(ev) {
ev.currentTarget.style.backgroundImage = 'url(./img/noisy-pattern.png)';
}

In this example we have three different buttons being referenced in the variables btn1, btn2, and btn3.

All three buttons have a click listener.

All three click listeners will call the function makeNoisy( ).

Inside the function makeNoisy, we are able to tell which button needs to have the background image added because ev.currentTarget gives us that information. The target property points to the button that was clicked.

The keyword this works in the same way.

function makeNoisy(ev) {
this.style.backgroundImage = 'url(.img/noisy-pattern.png)';
}

We can replace ev.currentTarget with this.

This points to the object written in front of addEventListener( ).

To learn a lot more about the keyword this watch the following video:

Basic keyword this

This excerpt from the You Don't Know JS book by Kyle Simpson gives a good description of the 4 rules for determining the value of this. Here is the link to the page in You Don’t Know JS


We can summarize the rules for determining this from a function call’s call-site,in their order of precedence. Ask these questions in this order, and stop when the first rule applies.

  1. Is the function called with new (new binding)? If so, this is the newly constructed object.
let bar = new foo(); //inside foo, this === the object being created
  1. Is the function called with call or apply (explicit binding), even hidden inside a bind hard binding? If so, this is the explicitly specified object.
let bar = foo.call(obj2); // inside foo, this === obj2
  1. Is the function called with a context (implicit binding), otherwise known as an owning or containing object? If so, this is that context object.
let bar = obj1.foo(); // inside foo, this === obj1
  1. Otherwise, default the this (default binding). If in strict mode, pick undefined, otherwise pick the global object.
let bar = foo(); // inside foo, this is most likely the global object

That’s it. That’s all it takes to understand the rules of this binding for normal function calls. Well… almost.


There are a few exceptions. Arrow functions are one of those exceptions.

Arrow functions use lexical scoping for determining the value of this. Instead of using the four standard this rules, arrow-functions adopt the this binding from the enclosing scope (function, block, or global).

Simply put, an Arrow function will use whatever value you would get for this if you wrote it on the line above where the function is declared.

const bob = {
hello: function () {
//standard function
console.log(this); // `this` is `bob`
},
goodbye: () => {
//arrow function
console.log(this); // `this` is `window`(global object)
},
};
bob.hello(); //bob
bob.goodbye(); //window
bob.goodbye.call(bob); //STILL the window object

The value of this in an arrow function will always refer to the context of where the code was written. It doesn’t care about run time changes or where it is being called from. Arrow functions ignore binding with the call, apply, or bind methods.

Calling a function can be accomplished by writing the name of the function followed by one of these three methods.

The difference between them is that bind creates a copy of the function with the new context for you to use later. call and apply will both call the function immediately with the new context.

function f1(a, b) {
console.log(this);
return a + b;
}
let answer1 = f1.call(window, 10, 5);
// returns 15 (parameters passed separately)
// will console.log the Window Object
let answer2 = f1.apply(document, [10, 5]);
// returns 15 (parameters passed in an array)
// will console.log the document Object
let answer3 = f1.bind(document.body);
//answer3 is now a copy of the function f1
// when we run answer3, document.body will be the value for `this`

call, apply, and bind Methods

Every type of Object has a prototype. A prototype is a special kind of an object that contains all the methods that will be shared by all Objects of that type.

You will hear a lot about prototype and class over the next few semesters. They are two different approaches to designing and architect software. The problem is that in your early days of programming they can seem like almost the same thing.

We will try to help you understand the differences here in simple practical terms that will let you write better JavaScript with fewer unexpected errors.

A Class is a blueprint for building objects. It is not an object itself, just the plans for building a certain kind of object. Classes inherit properties and methods from parent classes. When you create (instantiate) an object from a class, the object will be given copies all the properties and methods from it’s class blueprint as well as copies of all the properties and methods from all the ancestor parent classes. So, when you call an Object’s method, the method actually exists inside the Object.

A prototype is an example Object. It is an Object. Think of it as the first one built. In JavaScript, when we create an Object a constructor function is used to build the object. That function has a prototype object. We can put any methods that we want to share with all the objects built with that constructor into that prototype object. We can still link our objects to parent ones but we don’t copy the methods, instead, we just link to the parent’s prototype. There is a chain of prototype type objects. When we create (instantiate) our Object, it doesn’t need copies of all the methods and parent methods. If we call an Object’s method and the method does not exist inside our Object, then JavaScript will look up the prototype chain for the method and delegate (borrow) the method to run.

JavaScript has something called the prototype chain, which is how inheritance works in JavaScript. Each one of the Object prototypes will have a connection to the prototype object belonging to it’s parent object. At the top of the chain is the prototype of the Object object.

As an example, look at the toString() method. When you create an Array, there is no method in Array called valueof. However, you can write the following and no error occurs.

let letters = new Array('a', 'e', 'i', 'o', 'u');
letters.valueof();

valueof reference

This works because of the prototype chain.

We are calling the method Array(). The Array function has a prototype object. All the methods that you would call on your array, like map or sort or filter are inside the Array.prototype object. The prototype object of Array.prototype is the Object.prototype object. (this is the prototype chain)

When the line letters.valueof() is run, the JavaScript engine looks inside of letters for a method called valueof. If it is not found then JS looks inside Array.prototype for a method called valueof. If the method is not found there, then JS looks inside Object.prototype for the method. Since Object.prototype.valueof does exist it can be run.

The prototype of Object.prototype is null. Once null is reached in the search through the prototype chain, then JS knows that it can safely say that an error has occured.

Prototype Chain Visually

Intro to prototype

Understanding the prototype chain

Practical uses of prototype with Arrays

As this last video explains, a practical use of the prototype chain is to add new functionality to existing objects. Let’s make a completely useless example of a method that we are going to add to all Array objects.

We will create a new method called bob and the purpose of this method is to change all the values of every element in an Array to bob. Got an Array filled with important numbers? Not any more. Now it is filled with bobs.

//adding the method bob to all Arrays
Array.prototype.bob = function () {
//bob is now a function inside of Array.prototype object
//You can think of Array.prototype as a name space where we are saving our function
//The keyword `this` refers to the Array that is calling this method
this.forEach(function (item, index) {
//can't use arrow functions because they have a different `this` value
//looping through all the values in the array and replacing them one at a time
this[index] = 'bob';
});
};
let robert = ['Robert', 'Robert', 'Robert', 'Robert'];
robert.bob();
//now the contents of robert are ['bob', 'bob', 'bob', 'bob'];

So, that was just a fun example of how to add something into a prototype object.

The most important reason why we use the prototype is to be able to reuse functions and save memory.

Imagine if each time you created an Array, JavaScript had to copy all the methods into that Array.

let a1 = ['a', 'b'];
let a2 = ['c', 'd'];
let a3 = ['e', 'f'];
let a4 = ['g', 'h'];
let a5 = ['i', 'j'];
let a6 = ['k', 'l'];
//we have created 6 arrays
//imagine if we had to keep a copy of every array method inside each of those variables,
//along with the length property value and the actual values from the Array.

Array’s have access to about 30 methods. With six arrays that would be ~180 functions that need to be held in memory. Now, add on all the methods from Object.prototype, for each of those arrays.

It would be a huge waste of memory.

Instead we get this prototype object where we can store and share all those methods. Plus we get the prototype chain and we can jump up through the list of prototype objects looking through all the shared methods.

When creating web apps, it means that you will be writing many lines of code and often using multiple JavaScript files. So, we need to think about how to organize our code for efficient reuse of the code as well as effectively combining our code with the code from libraries and other developers.