7.1 Code Structure & Approaches
# Code Structure
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
.
# Callback Functions
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`
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
This video explains what callback functions are:
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.
# Higher Order Functions
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Recursive Functions
When a function calls itself, this is known as an recursive function.
function it() {
it(); //iteratively call the function `it`
}
it(); //start the infinite iterative looping
2
3
4
5
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 a stop 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
2
3
4
5
6
7
8
9
10
11
# Closures
Closures are not something that you 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.
# Currying
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.
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
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');
2
3
4
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');
2
3
4
5
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.
# This and Context
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)';
}
2
3
4
5
6
7
8
9
10
11
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)';
}
2
3
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:
# This - Beyond Events
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 (opens new window)
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.
- 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
- 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
- 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
- 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
2
3
4
5
6
7
8
9
10
11
12
13
14
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.
# Call, Apply, Bind
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`
2
3
4
5
6
7
8
9
10
11
12
13
14
# Prototypes
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 (opens new window), 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();
2
valueof reference (opens new window)
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.
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 bob
s.
//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'];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.
2
3
4
5
6
7
8
9
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.
# Projects
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.
# 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.
Best Practice
Try to minimize the number of variables and functions (named objects) that you have in your global namespace.
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
},
};
2
3
4
5
6
7
8
9
10
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
2
3
4
5
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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Remember when you use namespaces to include the namespace in front of the method or property name.
# MultiPage Scripts
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# ES Modules
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 export
ed 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';
2
3
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.
# Exporting
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 };
2
3
4
5
6
7
8
9
10
11
# Default Exports
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');
}
2
3
4
The import statement for the function bob
would look like this:
//app.js
import frank from './bob.js';
2
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 };
2
3
4
5
6
7
8
9
10
11
12
13
# Import variations
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.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Dynamic Imports
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
});
2
3
4
5
6
7
8
9
10
11
# What to do this week
TODO Things to do before next week.
- Read all the content from
Modules 7.1, 7.2, and 9.1
. - Continue working on the Hybrid Quizzes