7.2 Code Structure
# 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();
- 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);
- 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();
- Otherwise, default the
this
(default binding). If in strict mode, pick undefined, otherwise pick the global object.
let bar = foo();
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 (function or global) scope. 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.
# 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 tto 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.
# 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 Exercises
- The first four Hybrid Exercises must be submitted before the Reading Week.