In this lecture we cover closures, prototypal inheritance, and constructor functions.
A closure function
is a function that is returned from another function. The inner function that is returned will rely on data that is declared in the outer functions scope.
Here is an example.
function counter(){
// Local variable to the counter function
let count = 0;
// The closure function that is returned that will rely on data from the counter functions scope
function addOne(){
return count += 1;
};
// Return the inner 'addOne' function
// It's important to note that we DO NOT invoke the 'addOne' function because we want to return the function itself
return addOne;
};
We can now create what we like to call snapshots
of the closure function. When a function forms a closure, it has access to all of it's local variables and the lexical environment. The lexical scope that the function has access to is the snapshot
.
lexical scope
- This is where we determine a variables or functions scope based solely on it's position in our code.
We can create the snapshot like this.
const countOne = counter();
countOne
is now a function. It's easy to think that it is now just the addOne
inner function that gets returned from counter
. The countOne
function now retains a reference to the variables that were declared inside of the lexical scope of the counter
function.
So we can invoke this snapshot multiple times to increment the private
count variable.
countOne();
countOne();
countOne();
We can also create a brand new snapshot
.
const countTwo = counter();
Then we can use the new snapshot multple times. The count
variable in the countTwo
will not be the same variable we refer to in countOne
.
countTwo();
countTwo();
countTwo();
We can follow a module pattern
with our closure functions by creating private variables
and private functions
. This is a way that we can create data that will be shielded from our global scope
so the only function that has access to it, is the closure function
.
function modulePattern() {
// variables and functions here are private and are only accessed through the public functions in the returned object
let privateVariable = 'I am private';
let privateFunction = function() {
console.log(privateVariable)
}
return {
// everything returned is public
changeVar: function(str) {
privateVariable = str;
},
readVar: function() {
privateFunction();
}
}
}
// module1 is now a public object with public methods that access and change private variables.
// Notice how we can not call module1.privateFunction
// We can only call that function within the code of the function.
var module1 = modulePattern();
This time we are returning multiple functions, rather than just one function.
Now let's practice creating some closure functions with different use cases.
Let's first create a closure function to make a sandwhich.
function createSandwhich(){
// this will keep track
const sandwhichIngredients = [];
// thefunction that gets returned will add ingredients to the sandwhich
function addIngredient(ingredient){
sandwhichIngredients.push(ingredient);
return sandwhichIngredients;
}
// return the function
return addIngredient;
}
We now have a sandwhich maker closure function that we can easily re-use to create new sandwhiches. Let's go ahead an now make a few.
// make the sandwhich
let tayteSandwhich = createSandwhich();
// add ingredients
tayteSandwhich('Meatballs');
tayteSandwhich('Marinara Sauce');
tayteSandwhich('Parmasen Cheese');
// make the sandwhich
let mattSandwhich = createSandwhich();
// add ingredients
mattSandwhich('Ham')
mattSandwhich('Cheese')
Now let's revamp our closure function to add more functionality by following the module pattern.
function createSandwhich(){
// this will keep track
const sandwhichIngredients = [];
// add ingredients
function addIngredient(ingredient){
sandwhichIngredients.push(ingredient);
return sandwhichIngredients;
};
// remove ingredients
function removeIngredient(){
// check to make sure the ingredient exists
// this will return the index value or -1 if not found
let ingredientIndex = sandwhichIngredients.indexOf(ingredient);
// check to see if item is missing
if(ingredientIndex === -1){
// return an error message
return 'Sorry, that ingredient does not exist. Pleas try another!'
} else {
// if it is found, remove the item
sandwhichIngredients.splice(ingredientIndex, 1);
return sandwhichIngredients;
};
};
// read the ingredients
function readIngredients(){
return sandwhichIngredients;
};
// create the module or object thatv will be returned
return {
addIngredient,
removeIngredient,
readIngredients
};
}
Let's used our revamped sandwhich maker closure function.
// make the sandwhich
let tayteSandwhich = createSandwhich();
// add ingredients
tayteSandwhich.addIngredient('Meatballs');
tayteSandwhich.addIngredient('Marinara Sauce');
tayteSandwhich.addIngredient('Parmasen Cheese');
// remove ingredient
tayteSandwhich.removeIngredient('Meatballs');
// read the ingredients on the sandwhich
tayteSandwhich.reaadIngredients();
Now let's create another closure function that has a little more functionality. This time we will create a calculator that we can use to perform basic math operations for us. We will use the modular pattern
to house our methods.
function calculator(){
// create a starting value at zero
let value = 0;
// return a module that will contain methods for functionality
return {
add(num){
return value += num;
},
subtract(num){
return value -= num;
},
multiply(num){
return value *= num;
},
divide(num){
return value /= num;
}
};
};
Let's use the new calculator that we just created.
let ti84 = calculator();
// logging the ti84 should return the module or object that encases the different methods we can execute
console.log(ti84);
// lets use the new texas instrument YEEEHAW ðŸ¤
ti84.add(10);
ti84.subtract(5);
ti84.multiply(2);
ti84.divide(2);
The final closure method that we will now create will be a bank account. We want this closure function to have full functionality of a bank account. We want to be able to deposit, withdraw, and check our balance. Let's make sure we use the modular pattern
so we can return multiple functions.
function createAccount(){
// define a starting balance
let balance = 0;
// return a module with methods to deposit, withdraw, and check balance
return {
deposit(amount){
return balance += amount;
},
withdraw(amount){
return balance -= amount;
},
checkBalance(){
return 'Account has a balance of ' + balance;
}
}
};
A constructor function
is special function we can use in javascript to create a new object. These are what classes
are built around in javascript.
It's easy to think that a constructor function
is a blueprint to the object we want to build.
We will create a constructor function
just like any other function, except this time it will start will capital letter.
function Car(){
}
Inside of the constructor function, we use the this
keyword to refer to the object the function will build. The instance will automatically be returned for us so we do not need to delcare the return
statement.
function Car(){
// this ==> {} IMPLIED
this.make = 'Tesla';
this.model = 'Model X';
// return this IMPLIED
}
We can now create a new instance
or a new object from the constructor function using the new
keyword.
const myCar = new Car();
console.log(myCar) // => {make: 'Tesla', model: 'Model X'}
The new
keyword will set the context for our constructor. It is what creates the object that we refer to using the this
keyword.
We can also use parameters
in our constructor functions to accept user input and use dynamic data.
// same constructor as before, but this time we recieve arguments
function Car(make, model){
this.make = make;
this.model = model;
}
we can now call upon that constructor function to create a new instance and pass in data to that instance.
const myCar = Car('Tesla', 'Model X');
Now let's add some methods to our contructor function.
function Car(make, model){
this.make = make;
this.model = model;
// method to honk
this.honk = function(){
alert('Beep Beep');
}
}
Now let's create some instances from our new constructor that has a method on it.
var subaru = new Car('Subaru', 'WRX');
var civic = new Car('Honda', 'Civic');
var tesla = new Car('Tesla', 'Model S');
We can now call the honk
method on all of the car instances that we have created.
subaru.honk();
civic.honk();
tesla.honk();
If we take a closer look at this, by console logging the instance objects, you can see that we are delcaring a new honk
function everytime we create a new instance from the Car constructor. This could be very impactful and not memory efficiant for our applications when we create hundreds of instances from that constructor.
So how can we go about having this method avaiable to us for every instance, but only declare it once? This is where prototype functions
save the day!
Instead of putting the honk
method on every instance that gets created, we can add the method as a prototype method
to the Car
constructor. This will allow all car objects made from the Car constructor to have access to it, while only having to delcare it once.
We will directly target the prototype
property that exists on the Car
function (object) and add the method onto it.
Car.prototype.honk = function(){
alert(`Beep Beep, I am a ${this.make}!`);
}
// Now we can create our instances
var subaru = new Car('Subaru', 'WRX');
var civic = new Car('Honda', 'Civic');
var tesla = new Car('Tesla', 'Model S');
// We can still use the prototype method on every instance
subaru.honk();
civic.honk();
tesla.honk();
// notice how the `honk` method does not show up on the instance when we console log it now
console.log(subaru)
console.log(civic)
console.log(tesla)
After console logging the instances, we can see that the honk
method no longer exists on that object. So how does the object have access to that prototype method?
This is where prototypal inheritance
comes into play.
In javascript all functions are really just objects behind the scenes. This means that functions can have properties. Every function has a property called prototype
, which is also another object that will hold methods inside of it.
When we invoke a function that exists on an object, let's call that object car
. The browser will check the car
object for the function, if it's not in that object, the browser will look into the prototype
object that is a property of the car
object. It will go up the chain of objects and their prototype properties until it feither finds the function or it doesn't find it. If it doesn't find it, that means the function is undefined.
A constructor function can inherit the prototype from another constructor function. This can be complicated and can get confusing, but it's good to be introduced to it. This is what that extends
keyword is doing behind the scenes in classes
.
function Car(make, model) {
this.make = make;
this.model = model;
}
Car.prototype.honk = function() {
console.log(`The ${this.make} goes Beep Beep!`);
}
const myCar = new Car('Tesla', 'Model X');
// We can get the prototype object from an instance using the Object.getPrototypeOf method
console.log(Object.getPrototypeOf(myCar)); // Car {honk: [function]}
Now let's create a SuperCar
constructor that will inherit from the Car
constructor and adds a v12 engine to it.
There are three steps we will need to follow to make this happen.
-
We will pass the current object through the
Car
constructor function to gain it's functionality. Then we will add theengine
property that is only available to super cars. -
We will then set the constructor functions protoype object to a new object created from the prototype of the object we want to inherit from. In this case it will be the
Car
prototype. -
We will now set the constructor function appropriately for the clarity on which constructor function is responsible for making the object.
// Step One:
function SuperCar(make, model){
// pass the current object in with the arguments we recieved to gain the Car functionality
Car.call(this, make, model);
// then set the engine property to v12
this.engine = 'v12';
}
// Step Two:
SuperCar.prototype = Object.create(Car.prototype);
// The Object.create() method creates a new object, using an existing object as the prototype of the newly created object.
// Step Three:
SuperCar.prototype.constructor = SuperCar;
The Object.create() method creates a new object, using an existing object as the prototype of the newly created object.
Now let's use our SuperCar constructor that is inehriting the prototype properties from another constructor function.
// Create a new instance of the supercar
const mySuperCar = new SuperCar('Tesla', 'Model Super');
// We can now use the prototype methods that we have inherited from Car
mySuperCar.honk();
// We can add more protype methods to the supercar
SuperCar.prototype.rev = function(){
console.log(`You have revved the ${this.engine} engine`);
}
// Then we can use the prototype method
mySuperCar.rev();
// Now if we create a prototype method on Car, our SuperCar will also ineherit it
Car.prototype.oilChange = function(){
console.log(`You have changed the oil for your ${this.make} ${this.model}`);
}
// We can now use that method
mySuperCar.oilChange();
There is alot going on here and this is why classes
were introduced to javascript. They are what is known as syntactic sugar
because they make this functionality simple using the extends
keyword and super
function.
Let's take a look at what we just did, but in class syntax.
class Car {
constructor(make, model){
this.make = make;
this.model = model;
}
honk(){
console.log(`The ${this.make} goes Beep Beep!`);
}
}
// Now implement the inheritance
class SuperCar extends Car {
constructor(make, model){
// call super to invoke the Car's constructor
super(make, model);
// add engine prop to this instance
this.engine = 'v12';
}
}
See how much easier it was?