- Objects
- Organizing code into appropriate objects
- Object factories
- Determining/setting function execution context (this)
- Implicit function execution context
- Explicit function execution context
- Dealing with context loss
- Lexical scope
- Scope and Closures
- Higher-order functions
- Creating and using private data
- Garbage collection
- IIFEs
- Partial Function Application
- Object creation patterns
- class syntax
- Constructor functions
- Pseudo-Classical pattern
- Prototype objects
- Behavior delegation
- Modules
- JavaScript Weekly: Making Sense of Closures
- JavaScript Weekly: Understanding Links on the Object Prototype Chain
- JavaScript Weekly: An Introduction to First-Class Functions
- JavaScript Weekly: What in the World is this?!
- Object Factories, also known as factory functions, are functions that return copies of an object with unique state, reducing overall redundancy in the codebase.
- Reduces redundancy in code, especially when many similar objects are used.
- Simple and easy to understand
- Can use private data via closures
- Cannot determine whether an object was built from an object factory
- All methods shared by the objects are copies of the original function, increasing memory usage.
- Inheritance is not straightforward
function makeCar(make, model) {
return {
make,
model,
honk() {
return 'Beep!';
},
}
}
const honda = makeCar('Honda', 'Civic');
const toyota = makeCar('Toyota', 'Camry');
- The reserved
this
keyword represents the execution context, or the object that is currently used for context, at the point of code execution.
const dog = {
name: 'Sparky',
bark() {
console.log(this.name + ' says hello!');
},
};
dog.bark(); // Sparky says hello!
When invoking the
bark
method ondog
on line 8, the execution context ofdog
is bound to thethis
keyword. Because thedog
object contains the propertyname
, the value ofSparky
is returned, concatenated with the string, and outputtingSparky says hello!
.
- The implicit execution context is the context (
this
) that is set by default when a function or method is invoked without explicit context. While method invocations will typically reference the object in which the method is located, functions utilize the global object (orundefined
in strict mode).
const cat = {
name: 'Milo',
meow() {
console.log(this.name + ' says hello!'); // `this` => cat object
},
};
function meow() {
console.log(this.name + ' says hello!'); // `this` => global object
}
cat.meow();
// Milo says hello!
meow();
// undefined says hello!
this.name; // `this` => global object
// undefined
- In strict mode, the execution context for functions is
undefined
, which will throw exceptions if a property value is requested.
const name = 'Bob';
function greet() {
console.log('Hello, ' + this.name);
}
greet(); // TypeError: Cannot read properties of undefined
- Explicit execution context can be provided to a function or method invocation by using either
call
,apply
, orbind
to invoke it with explicit context. Rather than using the default context, one is provided as an argument to these methods that will override the implicit context.
const person = {
name: 'Jimmy',
age: 25,
describe() {
return `${this.name} is ${this.age} years old.`;
},
count(start, end) {
return `${this.name} is counting from ${start}-${end}.`;
}
}
const bobby = {
name: 'Bobby',
age: 32,
}
person.describe(); // Jimmy is 25 years old.
person.describe.call(bobby); // Bobby is 32 years old.
call
uses execution context as first argument, then method arguments as the rest.apply
uses execution context as first argument, then an array of method arguments as the second argument.
person.count.call(bobby, 1, 5); // Bobby is counting from 1-5.
person.count.apply(bobby, [1, 5]) // Bobby is counting from 1-5.
bind
permenantly binds an execution context to a copy of the original function. This method object is then returned from thebind
invocation and can be assigned to a variable. Because it is permenantly bound, no explicit execution context methods can overwrite it.
const bobbyCounts = person.count.bind(bobby); // [Function: bound count]
bobbyCounts(1, 5); // Bobby is counting from 1-5.
bobbyCounts.call(person, 1, 5); // Bobby is counting from 1-5. (unchanged)
person.count(1, 5); // Jimmy is counting from 1-5. (unaffected)
- Scenarios
- Invoking a function that has been removed from its original object
- Invoking a nested function
- Invoking a function passed as an argument
- Invoking a constructor function with the
new
keyword
- Fixes
- Arrow Notation
- Assign
this
to a variable - Optional
thisArg
argument - Explicit Context Execution
const cookieMonster = {
name: 'Cookie Monster',
eat(cookies) {
cookies.forEach(function(cookie) {
console.log(`${this.name} eats the ${cookie} cookie!`);
});
},
};
cookieMonster.eat(['Peanut Butter', 'Chocolate Chip']);
// undefined eats the Peanut Butter cookie!
// undefined eats the Chocolate Chip cookie!
const cookieMonster = {
name: 'Cookie Monster',
eat(cookies) {
cookies.forEach(cookie => {
console.log(`${this.name} eats the ${cookie} cookie!`);
});
},
};
const cookieMonster = {
name: 'Cookie Monster',
eat(cookies) {
const self = this;
cookies.forEach(function(cookie) {
console.log(`${self.name} eats the ${cookie} cookie!`);
});
},
};
const cookieMonster = {
name: 'Cookie Monster',
eat(cookies) {
cookies.forEach(function(cookie) {
console.log(`${this.name} eats the ${cookie} cookie!`);
}, this);
},
};
const cookieMonster = {
name: 'Cookie Monster',
eat(cookies) {
const logCookies = function(cookie) {
console.log(`${this.name} eats the ${cookie} cookie!`);
}.bind(this);
cookies.forEach(logCookies);
},
};
cookieMonster.eat(['Peanut Butter', 'Chocolate Chip']);
- Closures retain access to relevant variables within the lexical scope of an object at the point of definition, allowing it to be carried alongside the object and accessed at a later time. Even after invocation of the enclosing function is complete, so long as a reference to the object with the closure remains, the closure and its references do as well.
function makeSandwich() {
const ingredients = ['Bread', 'Turkey', 'Cheese', 'Mayonaise'];
return {
includes(item) {
return ingredients.includes(item);
},
};
}
const sandwich = makeSandwich();
sandwich.includes('Bread'); // true
sandwich.includes('Turkey'); // true
sandwich.includes('Ham'); // false
Although invocation of
makeSandwich
is completed on line 11, the returned object literal from the function remains as a reference fromsandwich
. Because theincludes
method of this object relies on theingredients
array, it is included in the method's closure, allowing the array to be referenced at a later time.
- Arrow functions do not have their own
this
binding; instead, they inherit execution context from the enclosing function where they are defined.
const chad = {
name: 'Chad',
say() {
const hello = () => console.log(`Hello, I'm ${this.name}.`);
hello();
},
}
const chuck = {
name: 'Chuck',
}
chuck.say = chad.say;
chad.say(); // Hello, I'm Chad.
chuck.say(); // Hello, I'm Chuck.
- Higher-Order Functions are functions that either accept a function as an argument, return an argument after invocation, or both. This is made possible by JavaScript's handling of functions as first-class objects, enabling functions to be passed around and referenced by variables throughout the program. Programmers can take advantage of these higher-order functions by declaring variables within the function's scope, creating a closure that can retain desired references and hide data from the rest of the program.
function magicNumber(num) {
return function() {
return num;
};
}
const luckyNumberSeven = magicNumber(7);
const luckyNumberFive = magicNumber(5);
luckyNumberSeven(); // 7
luckyNumberFive(); // 5
Even after the invocation of
magicNumber
on line 9, the anonymous function returned and assigned from it still maintains a reference tonum
within the function body due to a closure. This enables its reference to remain accessible when invokingluckyNumberSeven
on line 10.
-
Closures provide the ability to hide data and functionality from a returned object.
-
Hide Functionality
function newQuiver() {
let arrows = 3;
return function() {
if (!arrows) return console.log('Empty');
arrows--;
console.log(`You have ${arrows} left`);
}
}
const fireBow1 = newQuiver();
const fireBow2 = newQuiver();
fireBow1(); // You have 2 left
fireBow1(); // You have 1 left
fireBow2(); // You have 2 left
fireBow1(); // You have 0 left
fireBow1(); // Empty
-
Data is eligable for garbage-collection, a built-in JavaScript mechanism that frees up memory space by removing obsolete information in memory, once all references to the data within the program have been severed.
-
Variables that are referenced within a closure cannot be eligable for garbage-collection.
function makeCounter() {
let count = 1; // Not garbage collected until `counter` is reassigned
return function() {
console.log(count++);
}
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
Although the invocation of
makeCounter
is concluded on line 9, the anonymous function returned from the invocation is assigned tocounter
. Because the anonymous function relies oncount
in its scope, the variable is added to its closure and is therefore not eligable for garbage collection.
- Immediately Invoked Function Expressions (IIFEs) are functions that are immediately invoked upon defintion, allowing an isolated scope containing variables and functions to be made and executed without polluting the global scope.
- IIFEs can only be invoked using a function expression.
const generateTicket = (function() {
let ticket = 1;
return function() {
return ticket++;
};
})();
console.log(generateTicket()); // 1
console.log(generateTicket()); // 2
console.log(generateTicket()); // 3
https://launchschool.com/lessons/0b371359/assignments/f27fd52c
- IIFEs are useful in creating a private scope that allows for code execution that will not be affected by previous or further operations, such as object mutation/reassignment or naming collisions.
// Lots of code
(function() {
const derek = {
name: 'Derek',
age: 30,
}
console.log(`My name is ${derek.name} and I'm ${derek.age} years old.`);
})();
// More code
Because an anonymous function is immediately invoked within the codebase, a new scope is created that contains its own version of
derek
, allowing for an isolated code execution that will not pollute nor cause any potential errors from variables with the same name.
https://launchschool.com/lessons/0b371359/assignments/470d67c3
- Because IIFEs immediately create and execute a new function scope, a closure can be created that forms private data, restricting access to desired information from outside of the anonymous function.
const overdueBooks = (() => {
const books = ['Moby Dick', 'Hunger Games', 'Mistborn'];
return {
count() {
return books.length;
},
add(bookName) {
books.push(bookName);
},
}
})();
overdueBooks.count(); // 3
overdueBooks.add('Project: Hail Mary');
overdueBooks.count(); // 4
Because
overdueBooks
references an IIFE, thebooks
array is made private as only the returned object contains references to it via thecount
andadd
methods. The user outside of the object cannot directly referencebooks
, however both methods can access the array as thebooks
pointer is included in their closures.
-
Partial Function Application is the process in which a function is defined that calls a second function, accepting less arguments than that function expects. This is achieved through the use of pre-determined values to use as the remaining arguments upon invocation of the partial function.
-
"Partial function application refers to the creation of a function that can call a second function with fewer arguments than the second function expects."
-
"Partial function application requires a reduction in the number of arguments you have to provide when you call a function."
function greet(greeting, person) { // Primary
console.log(greeting + ', ' + person);
}
function greetingGenerator(greeting) { // Generator
return function(person) {
greet(greeting, person);
}
}
let sayHello = greetingGenerator('Hello'); // Applicator
let sayGoodbye = greetingGenerator('Goodbye');
sayHello('Derek'); // Hello, Derek
sayGoodbye('Josh'); // Goodbye, Josh
- Constructor Functions are functions that are used to instantiate objects that inherit similar attributes and behaviors.
function Musician(instrument) {
this.instrument = instrument;
}
const derek = new Musician('clarinet');
The constructor function
Musician
is used to instantiate thederek
object.
-
While the
new
keyword used with constructor functions is not manditory, it is highly recommended as its absence may cause unexpected results. -
The
new
keyword instructs JavaScript to do the following:
- Creates a new object that inherits from the constructor function's prototype property.
- Binds the new object to the
this
keyword - Return the object after invocation
function Musician(instrument) {
this.instrument = instrument;
}
// Invoked with the `new` keyword
function Musician(instrument) {
const that = Object.create(Musician.prototype);
that.instrument = instrument;
return that;
}
- The Pseudo-Classical Pattern in JavaScript is a combination of the Constructor Pattern and the Prototype Pattern by instantiating objects via constructor functions and developing attributes and behaviors through inheritance.
function Musician(name, instrument) {
this.name = name;
this.instrument = instrument;
}
Musician.prototype.play = function() {
console.log(`${this.name} is playing their ${this.instrument}!`);
}
function Clarinetist(name, model) {
Musician.call(this, name, 'clarinet');
this.model = model;
}
Object.setPrototypeOf(Clarinetist.prototype, Musician.prototype);
const derek = new Clarinetist('Derek', 'Buffet R13');
derek.play(); // Derek is playing their clarinet!
-
The prototype property is a built-in property default with any JavaScript function. This property houses all shared behaviors for the constructor function, a
[[Prototype]]
property that points to its own prototype, and aconstructor
property that points back to the constructor function itself. -
Arrow function do not have a prototype property.
-
Object literals do not have a prototype property
function Musician(name, instrument) {
this.name = name;
this.instrument = instrument;
}
Musician.prototype.play = function() {
console.log(`${this.name} is playing their ${this.instrument}!`);
};
/*
Musician (Constructor) = {
prototype: {
play: [Function: play],
[[Prototype]]: Object.prototype,
constructor: [Function: Musician],
},
}
Musician (instance) = {
name: (value),
instrument: (value),
[[Prototype]]: Musician.prototype
}
*/
-
[[Prototype]]
is an internal slot built-in with every object in JavaScript that references the object's prototype, allowing for prototype chaining. -
Use
Object.getPrototypeOf
to find immediate relative on prototype chain. -
Use
Object.prototype.isPrototypeOf
to see if an object is anywhere on the prototype chain. -
Use
Object.prototype.hasOwnProperty
to determine if the origin of a property belongs to the calling object. -
Prototype references:
- Constructor Function => Function.prototype
- Constructor.prototype => Parent.prototype
- Constructor Instance => Constructor.prototype
function Musician(name, instrument) {
this.name = name;
this.instrument = instrument;
}
Object.getPrototypeOf(Musician); // Function.prototype;
Object.getPrototypeOf(Musician.prototype); // Object.prototype;
Object.getPrototypeOf(new Musician('Derek', 'clarinet')); // Musician.prototype;
-
The
constructor
property is used to determine the function that builds the object. -
Constructor references:
- Constructor Function =>
[Function: Function]
- Constructor.prototype => Constructor Function
- Constructor Instance => Constructor Function
function Musician(name, instrument) {
this.name = name;
this.instrument = instrument;
}
Musician.constructor; // [Function: Function]
Musician.prototype.constructor; // [Function: Musician]
new Musician('Derek', 'clarinet').constructor; // [Function: Musician]
- To inherit the attributes of a parent class, we can use the
call
function and includethis
as its context. This will invoke the desired constructor function and assign all properties ofthis
.
function Musician(name, instrument) {
this.name = name;
this.instrument = instrument;
}
function Clarinetist(name, model) {
Musician.call(this, name, 'clarinet');
// this.name = name;
// this.instrument = 'clarinet'
this.model = model;
}
- To inherit the behaviors of a parent class, we must assign the prototype property of the given constructor function to the desired constructor function's prototype property.
Object.setPrototypeOf(Clarinetist.prototype, Musician.prototype);
// Clarinetist {
// prototype: {
// Musician.prototype: {play}
// }
// }
Clarinetist.prototype => Musician.prototype => play()
- Because JavaScript Object Oriented Programming utilizes prototypal inheritance rather than class inheritance, the
class
keyword actually acts as syntactic sugar to make it more approachable to programmers more familiar with classical syntax from other languages.
class Musician {
constructor(name, instrument) {
this.name = name;
this.instrument = instrument;
}
play() {
console.log(`${this.name} is playing their ${this.instrument}!`);
}
}
class Clarinetist extends Musician {
constructor(name, model) {
super(name, 'clarinet');
this.model = model;
}
}
const derek = new Clarinetist('Derek', 'Buffet R13');
derek.play(); // Derek is playing their clarinet!
- The
constructor
method is used as syntactic sugar for calling the constructor function when instantiating a new object.
class Musician {
constructor(name, instrument) {
this.name = name;
this.instrument = instrument;
}
}
// Same as
function Musician(name, instrument) {
this.name = name;
this.instrument = instrument;
}
-
Rather than directly assigning methods to the constructor function's prototype property, classical syntax allows programmers to add instance methods, or methods of the prototype, directly within the class structure. Under the hood, JavaScript will automatically place these methods within the prototype property, allowing for easier read code.
-
Instance methods are not separated by comas.
class Musician {
play() {
console.log('Playing');
}
breathe() {
console.log('Breathing');
}
}
// Same as
Musician.prototype.play = function() {
console.log('Playing');
};
Musician.prototype.breathe = function() {
console.log('Breathing');
};
- JavaScript employs static properties by adding attributes or behaviors directly to the constructor function in pseudo-classical notation or by using the keyword
static
in classical notation.
// Pseudo-Classical
function Musician() {
}
Musician.tuningNote = 'A';
Musician.transpose = function(music, startKey, endKey) {
return 'Transposing';
}
// Classical
class Musician {
static tuningNote = 'A';
static transpose(music, startKey, endKey) {
return 'Transposing';
}
}
- JavaScript classical pattern allows getters to be made to more carefully access data within a class. While the getter is defined as a method inside the class structure, it can be accessed via standard property dot notation outside of it.
class Person {
#firstName;
#lastName;
constructor(firstName, lastName) {
this.#firstName = firstName;
this.#lastName = lastName;
}
get name() {
return `${this.#firstName} ${this.#lastName}`;
}
}
const george = new Person('George', 'Washington');
george.name; // George Washington
- JavaScript classical pattern allows for setters to be made to more carefully reassign data within a class. While the setter is defined as a method inside of the class structure, it can be employed using standard property reassignment notation.
class Person {
#age;
constructor(age) {
this.#age = age;
}
set age(newAge) {
if (newAge > 0) {
return this.#age = newAge;
} else {
throw new RangeError('Age must be greater than 0');
}
}
}
const bob = new Person(28);
bob.age = 29; // 29
bob.age = -2; // RangeError: Age must be greater than 0
-
Encapsulation allows us to hide the data and functionality of an object from outside its respective class, exposing only the attributes and behaviors required by the users. In JavaScript, this can be achieved through the use of
#
alongside variable names in classical notation and with closures in pseudo-classical notation. -
All private properties must be dictated at the top of the class.
-
The exception for attempting to access a private property will be thrown at compile time.
-
Using pseudo-classical notation is less memory efficient, as a copy of the getter method is created for every child of the constructor function.
// Classical Notation
class Person {
#ssn;
constructor(name, age, fullSSN) {
this.name = name;
this.age = age;
this.#ssn = fullSSN;
}
get ssn() {
return 'XXX-XX-' + String(this.#ssn).slice(5);
}
}
const derek = new Person('Derek', 30, 123456789);
derek.name; // Derek
derek.age; // 30
derek.ssn; // XXX-XX-6789
derek.#ssn; // SyntaxError: Private field '#ssn' must be declared in an enclosing class
// Pseudo-Classical Notation
function Person(name, age, ssn) {
this.name = name;
this.age = age;
this.ssn = function() {
return 'XXX-XX-' + String(ssn).slice(5);
}
}
const derek = new Person('Derek', 30, 123456789);
derek.name; // Derek
derek.age; // 30
derek.ssn(); // XXX-XX-6789
derek.ssn; // [Function: (anonymous)]
https://launchschool.com/lessons/24a4613a/assignments/7143264c
- The prototype chain is a mechanism of inheritance that JavaScript uses to determine which attributes and behaviors belong to an object. Every object has a
[[Prototype]]
property that points to a different object, providing additional functionality. - See Prototype for more info and relevant methods.
- When a child's method shares a name with one of the parent's method, the invocation will override the behavior of the parent. This is due to how JavaScript handles property lookup - it finds the first occurrence within the prototypal chain.
function Animal() {
}
Animal.prototype.speak = function() {
console.log('Animal noises');
};
function Dog() {
}
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
const sparky = new Dog();
sparky.speak(); // Animal noises
Dog.prototype.speak = function() {
console.log('Woof!');
};
sparky.speak(); // Woof!
https://launchschool.com/books/oo_javascript/read/classes
- The
super
keyword is used to invoke the same instance method from a class's superclass. - When used in the constructor method, only
super
is required. - When used in any other method,
super
must be appended with the respective method.
class Person {
constructor(name) {
this.name = name;
}
}
class Musician extends Person {
constructor(name, instrument) {
super(name);
this.instrument = instrument;
}
play() {
return 'Playing';
}
}
class Student extends Musician {
constructor(name, instrument, grade) {
super(name, instrument);
this.grade = grade;
}
play() {
return super.play() + ' and Learning';
}
}
const derek = new Student('Derek', 'clarinet', 11);
console.log(derek.play());
https://launchschool.com/gists/e7d0531f
-
The benefits of using modules.
-
How to use and create CommonJS modules.
-
How CommonJS modules pass exported items to the importing module.
-
Modules allow programmers to divide functionality of a project into multiple files within a system, allowing for better abstraction of code and a more efficient building process. Because parts of the code are isolated, developers are able to maintain multiple aspects of the codebase without any fear of conflicts.
- The CommonJS Module Syntax is one of the oldest implementations of module support within JavaScript, utilizing the
require
keyword to import functionality from one file to another.
// greet.js
function greet() {
console.log('Hello, world!');
}
module.exports = greet;
// main.js
const greet = require('./greet');
greet(); // Hello, world!
module
=> Object that represents the current moduleexports
=> Names exported by modulerequire
=> Loads a module__dirname
=> Absolute pathname of directory that contains module__filename
=> Absolute pathname of file that contains module
- Do not need to know for assessment.
const walkMixin = {
walk() {
return "Let's go for a walk!";
}
}
class Cat {
constructor(name) {
this.name = name;
}
greet() {
return `Hello! My name is ${this.name}!`;
}
}
// Assigning the mixin to the Cat prototype object.
Object.assign(Cat.prototype, walkMixin);
let kitty = new Cat("Sophie");
console.log(kitty.greet());
console.log(kitty.walk()); // Cat objects can now use walk() from the mixin