Almost everything is an object in JavaScript. There are only six things that are not objects. They are null
, undefined
, string
, number
, boolean
, and symbol
.
These are called primitive types or primitive values. They can hold one piece of information and are simple in nature. Anything other than these primitive is an Object. This includes your functions, arrays, constructors, and classes (which are functions in disguise).
An object is a data type and can be viewed as a reference box that represents information. An object represents information because it doesn’t actually hold the information directly but a reference to where that piece of data is stored in memory.
With primitives, you have direct access to the storage. With objects, you just get the equivalent of an address of where that particular piece of data is stored.
ECMA specification defines an object as “a collection of properties”. A property is a key-value pair where the key is the meta description of the value assigned.
Here is an example of what a key-value pair can look like:
{
firstName: "Aphinya",
lastName: "Dechalert"
}
When almost everything declared becomes an object, creating one is not hard.
However, nothing is ever straight-forward.
Code is a process of capturing human ideas and expectations. It’s the bridge between humans and machines. This means that on one end, the humans reading it need to understand what’s going on. Patterns let us do this by defining the purpose of an object, with optimized ways of writing it within the parameters of the language. Creational patterns are one segment that deals specifically with the process of object creation.
This means that you’re not going around creating functions and classes in an ad-hoc manner. It’s planned. It’s organized. It has a clear space to exist and reason for existing.
The purpose of creational patterns is to create a standardized pattern that other developers can read and understand. In a way, design patterns in code can be seen as a ‘second language’ in code. It’s the meta, the grammatical style, and the composition of code that makes it easy for the brain to process. Sure, you can write the correct syntax and it’ll work. Anyone can code — but not everyone is a code architect. That mystical ‘code elegance’, robustness and structurally sound JavaScript only kicks in when you can simplify the process and make it easier for others to understand. This is where creational patterns sit in the grand scheme of code design.
There are several creational patterns out there in the wild but for this piece, I’m going to focus on the top five in relation to JavaScript. They are factory method, abstract factories, builders, prototypes, and singletons.
Let’s begin with the first on the list: the factory method.
factory method
When it comes to JavaScript — anything can happen. This contributes to how easy it is to pick up JavaScript. There’s no real structure that you need to follow to get things going. The it just works mentality can also lead to apps that are created on the fly. The lack of architecture leads to blocks of code that are prone to unwanted mutations and side effects. A mutation in JavaScript is when the shape of your data changes. A side effect is when a state change occurs outside of your function.
Change itself is not evil. Unwanted and unexpected change is where the root of many bugs lie. It’s also one way an app can collapse into itself.
Creational patterns create a clear separation between the act of creating objects and how they are used. This is important because when almost everything is an object in JavaScript, coupled with prototypal inheritance — you want to keep the line between creation and usage clear.
This is where a factory method comes in.
Factories fall under the creational pattern category because they are simply functions that exists solely to create object and return them. When it’s sole purpose in life is to only create and return the item, it reduces the opportunity for side effects and mutations.
Under a normal and unstructured approach, we can create a new object blueprint like this:
const Cat = function(name) {
this.name = name;
}
Cat.prototype.talk = function(){
console.log(this.name + " said meeeow");
}
And then we can do something like this to create a new instance of an object based on the prototype blueprint:
tibbers = new Cat("tibbers");
annie = new Cat("annie");
tibbers.talk();
annie.talk();
However, it can become an issue when we forget to use the new
keyword, resulting in silent failures where the object doesn't get created. It's a silent failure because the console doesn't complain to let us know that something is wrong. Everything appears to be working fine until you try and invoke a method. It's the same issue when using class
.
Another issue with using class
as your object definition is that this
is treated differently.
It’s easy to just call objects and methods by themselves. But what happens when you attach it to something else? Well, this is where things get tricky.
Take a look at this class below:
class Cat{
constructor(name){
this.name = name;
}
talk(){
console.log(this.name + " said meeeow");
}
}
And then we instantiate it like this:
const tibbers = new Cat();
When you invoke tibbers.talk()
, it will work as expected. However, if you move it into a different level, the this
keyword inside the original created Cat
type object doesn't shift with it, causing the referencing to be incorrect.
The html
:
<input type="submit" id="submit" onclick="clickMe()">
The javascript
:
function clickMe(){
tibbers.talk();
}
tibbers
is not going to talk. Instead, undefined
will talk. It's because the this
keyword is scoped inside a closure.
The quick low-down on this
keyword
The concept of this
in JavaScript is intricately linked to the idea of scopes. A scope refers to the context of code accessibility. There are two types of scopes in JavaScript - global scope and local scope.
A global scope is applied when variables are declared outside of a block. globally scoped variables often sit at the topmost level and therefore can be access by everything in the application.
A global scope are variables that are declared inside a block. It’s accessibility is limited by its current context and any children it may have.
For example:
var cat = "Tibbers";
function anotherCat(){
var cat = "Annie";
console.log(cat);
}
console.log(cat);
anotherCat();
The first cat
sits at the topmost level and has no surrounding curly braces to define its scope. As a result, it attaches itself to the default window
scope. It's considered a global scope because it can be accessed from anywhere inside the code.
The variable cat
inside anotherCat()
function is limited to to the scope of the function. Think of the curly braces { }
as an informational and accessibility ring fence.
It’s the reason why we can have cat
as a variable twice. They might have the same name but their location is considered different and unique.
When it comes to JavaScript, inheritance and accessibility flows inwards, with the first set of curly brace { }
it encounters being a logical stop. Variables and functions outside of the scope doesn't have access to something that sits inside another a pair of curly braces { }
. However, the reverse is allowed. Things sitting inside a scope has access to things sitting outside via the keyword this
.
this
is a keyword in JavaScript that allows you to target and differentiate variables based on where it sits in your code. How you declare that variable also matters because the this
keyword impacts on accessibility differently.
Here’s a table to summarize it below:
While a function scope is limited to the function it’s declared in, a block scope is limited to the first set of curly braces { }
it encounters. Why does this make a difference?
Imagine having a for loop
with the index as a the variable scope. That for loop
might run inside of a function. Issues can crop up if you use var
because the scope becomes accessible to everything within the function and not just isolated to the scope of the for loop
.
function countMe(){
let array = [1, 2, 3, 4, 5];
for(let i=0; i<5; i++){ }
for(var ii=0; ii<5; ii++){ }
console.log(i);
//will return ReferenceError: i is not defined. It doesn't exist because i is scoped to the nearest block.
console.log(ii);
//will return 5. This is undesirable since we don't want something that should be limited to the for loop scope to be accessible outside it. This is function scope in action.
}
countMe();
What about hoisting in JavaScript?
Unlike other languages where variables need to be declared before it can be used, JavaScript isn’t so strict with this rule. Why? Because hoisting lets us do this.
So what is hoisting?
It’s a process of moving variable declaration to the top, regardless of its physical location in the code. It will bubble up to the closest valid scope.
Why does does this happen?
When JavaScript gets loaded, the compiler puts everything on a hypothetical table and rearranges it based on scope and execution order. This means that if you’re loading in several JavaScript files, it gets treated and sorted like a single file rather than independent and unrelated entities (unless explicitly declared otherwise).
Let’s take a look at the example below:
cat = "Tibbers";
function print(){
console.log(cat);
console.log(dog);
}
dog = "Woofie";
print();
var cat;
var dog;
The variables cat
and dog
is treated as if it's declared up the top, making console.log()
possible.
It’s good to note that hoisting only applies to var
. let
and const
are treated in a linear fashion and doesn't get the same hoisting effect. This means that the following code will break and return a ReferenceError: Cannot access 'variable name'
message.
cat = "Tibbers";
function print(){
console.log(cat);
console.log(dog);
}
dog = "Woofie";
print();
let cat;
let dog;
If you used a const
before declaring it, you'll get a SyntaxError: Missing initializer const declaration
error.
Where do closures come in?
A closure is often explained as a function within a function. But what exactly does that mean? and what are the implications of placing a function inside another function?
Let’s take a look at the code below:
function run(){
var name = "Tibbers";
function printName(){
console.log(name);
}
printName();
}
run();
The function run
creates a scope for all variables declared inside. This means that name
is only accessible to the function. Rather than abstracting out printName()
as a separate function, declaring and invoking it inside run()
makes the scope name
accessible.
Why is this important?
It keeps your global scope from pollution and the variable accessibility is limited to the lexical scoping that it exists in.
What’s a lexical scope in JavaScript?
Lexical scoping is a term where you set a variable so that it can be referenced from within the code block that it’s defined in. It is also an environment where data is unique to that instance of function invocation.
In the above example, name
is lexically scoped to run()
. The alternate to this is called dynamic scoping, which is a term that refers to variables that are called from outside of the code block.
How is all this related to closures?
Let’s take a look at the function factory example below:
function applyDiscount(discount){
return function(cartTotal){
return cartTotal * (1-discount);
}
}
var discount25 = applyDiscount(0.25);
var discount15 = applyDiscount(0.15);
console.log(discount25(100));
console.log(discount15(100));
A closure is a when functions are enclosed within one another to keep the accessibility of the surrounding states and variables limited. Each instance is unique and the function becomes the object blueprint without the explicit need for the keyword new
. discount25
and discount15
both use the same function body definition but have different lexical environments.
Splitting out the functions into their separate code blocks would result in unnecessary duplications, dependency injection and chaining required to create the same effect. When there is more steps involved, it’s easy to miss a step and break the required composition flow.
Here is an example of what an inefficient pattern looks like trying to achieve the same thing:
function applyDiscount(discount, cartTotal){
return cartTotal * (1-discount);
}
var discount15 = applyDiscount(0.15, 100);
var discount15Again = applyDiscount(0.15, 200);
var discount15ThirdTime = applyDiscount(0.15, 300);
The issue here is that there is two potential points of change. If you wanted to change the discount
argument from 0.15
to 15
because you've decided to change the equation inside your function, you'd need to do it for all of them.
However, with the function factory using closures and lexical environments, you only need to do it once.
How is all this related to the class
keyword?
The class
keyword is not evil, nor is it so perfect that everything else should be disregarded. There is always a time and a place for everything.
The class
keyword captures traditional thinking and associations when it comes to object creation. It runs off the ideology that everything is an object in the literal sense, and objects have properties and methods. Properties describes the objects while methods describes what the object can do.
While on a technicality, a class
is a function in the form of syntax sugaring, it is not have the exact same effect as your regular function.
Classes always require new
to declare an instance and automatically operates under strict mode
regardless of settings. This means that classes don't get hoisted like normal functions and requires declaration before they can be used. The behavior of this
also changes.
In JavaScript, you have block scope and function scope. How this
behaves depends on the variable type you used. If you used let
and const
, it's scoped to the closest closing block { }
. If you used var
, it's scoped to the closest closing function block function(){}
. Things get funky when you use class because you have constructors which creates a function within a function. This sounds oddly like the common perception of closure - but is it?
Let’s take a look at a simple class below:
class Cart {
constructor(items){
this.items = items;
}
sumCart(){
//some loop to add up all the items
}
}
In the Cart
class, you have a constructor, which is a function that deals with creating lexical scopes for items
. this
is a reference to the Cart
lexical scopes, making it available to other methods inside the class.
While the constructor
is technically a function within a function (aka, the class
), it doesn't create a closure because variables are declared inside a function and forced out into the parent scope through assignment. In a closure, a child scope has access to the parent scope. In a class
, a child scope makes information available to the parent through assignment. Without this assignment, the scope won't exist. The act of assignment turns into a dependency while in closures, this isn't necessary.
However, class
still has its perks - especially in the areas of defining objects for humans to read. At the end of the day, code is made to translate and communicate ideas. Writing in pure functions can be hard to pick up for the JavaScript uninitiated Class syntax gives familiarity and transferrable ideas for a developer that's coming in from another object-oriented language.
How the factory method work
The purpose of class
is to capture the idea of the object. This object can be any size, with extendable functionality and attributes. While everything is an object in JavaScript, not everything needs to be a class of an object.
Sometimes, you just need to define actions in a way that’s highly accessible, but separated from all the other instances and invocations. Using a factory method is one way to do this.
By definition, any function that returns an object without the new
keyword is a factory function. This means that it can't be a class or constructor that returns an object.
Let’s take a look at the code below:
var user = {
user: 'dottedsquirrel',
avatar: 'squirrel.png'
}
function newName(name){
this.user = name;
return this;
}
newName('squirrel');
While this code looks innocent enough, it’s limited in reusability. You have an object literal user
that sets up the user details, then gets modified completely when the function newName
swoops in with a side effect and messes up the object shape. Then there's another issue: what if you want to have an unspecified number of users? Or what if you want to add methods to the user? You can't cleanly do that with an object literal declaration.
Using a factory method solves this issue. It lets you create the initial object shape with details filled in when its invoked.
function createUser(user, avatar){
return {
user,
avatar,
newName(newName){
this.user = newName;
}
};
}
Syntax note: if you have a key and value that’s exactly the same, you can just remove the value portion and write it as above. It’s just a shorthand version of writing out the long version {user:user, avatar:avatar}
Here we have the initial object shape in the form of a function called createUser
. To set up a new instance, you just need to fill in the blanks without the need to invoke the new
keyword.
With this structure, you can create as many instances of a user as you’d like, but the definition for the method is defined in only one place.
So where does lexical scoping and closures come in?
Using the example above, when you create an object with a function factory method, the object results in something that looks like this:
const user01 = {
user: "dottedsquirrel",
avatar: "squirrel.png",
newName(newName){
this.user = newName;
}
}
The variables are scoped specifically to user01
- or lexically scoped to the the declared const
variable. The method function inside the object has access to the lexical scopes and doesn't require complex chaining or injections to make things work.
The process of creating an object that is identical in shape, expectation, and output is streamlined. The process of creating the object doesn’t require duplication of logic for object to exist. It just needs to be initialized.
That’s the factory method in a nutshell. Now for the other types of factories…
Abstract Factories
Abstract factories are one of twenty three object-oriented based design patterns. It’s an idea that’s language agnostic and deals with the grouping of related or dependent objects.
The thing with code snippets is that they only show a small portion of a bigger application. In reality, an application often consists of multiple and interrelated parts. These parts capture the granular ideas that makes the purpose of the app possible.
Let’s take a look at a shopping app as an example.
When you go shopping online, there’s going to be a cart. You can do several things with this cart — add items, apply a discount code, apply taxes, apply restrictions, apply variations, set stock levels at purchase, bundle deals, upsells, and cross-sells are just some of the many possible things.
Each can be created as a stand-alone set of factory methods. But if you look beyond the code, you’ll see that all these factory methods are part of the same idea. They all belong in the cart purchasing process. It’s just broken down into different parts.
When it comes to code, there’s more to it than just creating functions and objects on the screen. There’s actually two layers to it — the act of coding and the organization of code. Abstract factories sits in the organizational realm whilst following a factory method implementation. It coordinates your code in a way that makes it easier to logically group families of function methods together.
Let’s run through a cart
abstract factory. It has the following requirements:
- cart can contain many items
- items can be added/removed from cart
- can try out different discount codes against the cart total
//individual factory methods
function NewCartItem(sku, name, price){
return {sku: sku, name:name, price:price}
}
function ApplyDiscount(discount){
return function(cartTotal){
return cartTotal * (1-discount);
}
}
//putting it all together as an abstract cart factory
const CartFactory = function(){
//storage for cart items
cartItems = [];
return{
addCartItem: function(item){
cartItems.push(item);
},
removeCartItem: function(item){
for(let i = 0; i < cartItems.length; i++){
if(cartItems[i].sku === item.sku){
cartItems.splice(i, 1);
}
}
},
tryDiscount: function(discount){
function cartTotal(){
var cartTotal;
for(let i = 0; i < cartItems.length; i++){
cartTotal =+ cartItems[i].price;
}
return cartTotal;
}
return discount(cartTotal());
}
}
}
//setting up the objects with factory methods
const vintageClock = NewCartItem(1239, "Vintage Clock", 19.99);
const motivateCards = NewCartItem(3897, "Motivational Cards", 10.99);
const discount15 = ApplyDiscount(0.15);
const discount25 = ApplyDiscount(0.25);
//using the abstract factory
var cart = CartFactory();
cart.addCartItem(vintageClock);
cart.addCartItem(motivateCards);
cart.removeCartItem(vintageClock);
//user might have multiple discount codes to try out on the cart
console.log(cart.tryDiscount(discount15));
console.log(cart.tryDiscount(discount25));
There isn’t much to abstract factories, except how you think and organize your factory methods to form another object type. The abstract part in abstract factory pattern refers to the idea of the thing. In our case, our thing is the cart
. The cart itself isn't a concrete idea, but it's a collection of ideas put together to give the cart
the shape that it currently has.
Builders
Building a real app isn’t always as cookie cutter and clean as how things often go with online tutorials. The complexity is higher with more requirements than just creating a simple object.
The builder pattern is often described as having two parts — the base the Director. The base deals with the underlying shape of the object (the properties), while the Director deals with the execution (the methods).
In JavaScript, properties and methods are enforced and created by the developer. There’s no hard and fast methodology that forces you to follow the pattern. JavaScript is multi-paradigm by design, meaning that there’s no enforced rule that will ensure that you separate your properties from your methods.
To apply a builder pattern to your JavaScript code, you’ll need to separate the business logic from the object properties while maintaining a direct relationship between the two.
Why is this a big deal?
An object often has two parts to it — the properties that make up the object and the methods that capture the business rules. Sometimes that object needs a series of methods to be executed without the need to pre-configure the actual order. The beauty of the builder pattern is that it lets you decide your sequence order whilst maintaining the object’s properties. All the methods don’t have to be called, at the same time, or in a particular sequence. The code lets you build your output based on your required situation.
What does this look like?
Imagine you have to build a calculator object. This object has two parts — the output and the potential operations it can do. Here’s one way to infuse a builder pattern into the object blueprint.
//the object blueprint aka factory interface
const CalculatorBuilder = function(){
//starting the calculator off at 0
//initializing the lexical scope
var result = 0;
return {
result:result,
add: function(number){
this.result += number;
return this;
},
subtract: function(number){
this.result -= number;
return this;
}
}
}
var calculator = CalculatorBuilder();
calculator.add(5).subtract(2).add(3).subtract(1);
console.log(calculator.result);
We have the CalculatorBuilder
which defines the original object and its associated methods. The returned object inside the function implicitly declares the object as new
without using the new
keyword. The returned object is lexically scoped, making it accessible to the function methods add
and subtract
.
The major difference here and examples so far is that the function methods is returning this
instead of the result. Why is this important? Because it means that you're returning the object itself rather than just a single result after the function has executed.
Returning an object instead of a single value lets you chain your methods together in any order you want. Using the example code above, let’s take a closer look at how things work under the hood.
//here is your new calculator object
var calc = CalculatorBuilder();
If you console.log(cal)
, you'd get the object constructed and returned by CalculatorBuilder()
. It looks something like this:
{
add: function(number){
this.result += number;
return this;
},
result: 0,
subtract: function(number){
this.result -= number;
return this;
}
}
Now if you run the add()
method on calc
, it returns itself through this
after the process has been completed, thus allowing the method after it to access to the entire object rather than just a single returned result.
But if your function returned a value, then it would throw a TypeError
saying that it's not a function - which is correct and logical in the eyes of the interpreter. When you return a value instead of the entire object, there is no reference to the original calc
object.
Let’s take a look at another example. In fact, let’s rewrite something we’ve previously written. Let’s make our abstract cartFactory
better by upgrading with a builder pattern.
Don’t worry, it’s only a slight tweak. The abstract pattern has already done most of the heavy lifting. For the builder pattern, rather than just returning a value, it returns the object itself and creates a clear separation between attributes and methods.
For reference, here’s what the original code looks like:
//individual factory methods
function NewCartItem(sku, name, price){
return {sku: sku, name:name, price:price}
}
function ApplyDiscount(discount){
return function(cartTotal){
return cartTotal * (1-discount);
}
}
//putting it all together as an abstract cart factory
const CartFactory = function(){
//storage for cart items
cartItems = [];
return{
addCartItem: function(item){
cartItems.push(item);
},
removeCartItem: function(item){
for(let i = 0; i < cartItems.length; i++){
if(cartItems[i].sku === item.sku){
cartItems.splice(i, 1);
}
}
},
tryDiscount: function(discount){
function cartTotal(){
var cartTotal;
for(let i = 0; i < cartItems.length; i++){
cartTotal =+ cartItems[i].price;
}
return cartTotal;
}
return discount(cartTotal());
}
}
}
//setting up the objects with factory methods
const vintageClock = NewCartItem(1239, "Vintage Clock", 19.99);
const motivateCards = NewCartItem(3897, "Motivational Cards", 10.99);
const discount15 = ApplyDiscount(0.15);
const discount25 = ApplyDiscount(0.25);
//using the abstract factory
var cart = CartFactory();
cart.addCartItem(vintageClock);
cart.addCartItem(motivateCards);
cart.removeCartItem(vintageClock);
//user might have multiple discount codes to try out on the cart
console.log(cart.tryDiscount(discount15));
console.log(cart.tryDiscount(discount25));
Here’s what the refactored code looks like with the builder pattern in mind:
//individual factory methods
function NewCartItem(sku, name, price){
return {sku: sku, name:name, price:price}
}
function ApplyDiscount(discount){
return function(cartTotal){
return cartTotal * (1-discount);
}
}
//refactored abstract factory into builder factory
const CartFactory = function(){
//builder object definition
cartItems = [];
cartTotal = 0;
return{
cartItems: cartItems,
cartTotal: cartTotal,
addCartItem: function(item){
this.cartItems.push(item);
return this;
},
removeCartItem: function(item){
for(let i = 0; i < this.cartItems.length; i++){
if(this.cartItems[i].sku === item.sku){
this.cartItems.splice(i, 1);
}
}
return this;
},
applyDiscount: function(discount){
for(let i = 0; i < this.cartItems.length; i++){
this.cartTotal =+ this.cartItems[i].price;
}
this.cartTotal = discount(this.cartTotal);
return this;
}
}
}
//setting up the objects with factory methods
const vintageClock = NewCartItem(1239, "Vintage Clock", 19.99);
const motivateCards = NewCartItem(3897, "Motivational Cards", 10.99);
const discount15 = ApplyDiscount(0.15);
const discount25 = ApplyDiscount(0.25);
//using the abstract factory
var cart = CartFactory();
//chaning is now possible
cart
.addCartItem(vintageClock)
.addCartItem(motivateCards);
console.log(cart.cartItems);
//users can now stack discount codes rather than limited to only one
console.log(
cart
.applyDiscount(discount15)
.applyDiscount(discount25)
.cartTotal
);
The act of returning the entire object through this
rather than just a single value means that you can now chain the methods together. This makes adding to the cart more efficient and flexible.
So why don’t we just build our factories with the builder pattern all the time? That’s because with increased flexibility comes increased complexity. Previously, you had invoke the methods individually.
For example:
var cart = CartFactory();
cart.addCartItem(vintageClock);
cart.addCartItem(motivateCards);
cart.removeCartItem(vintageClock);
It’s much easier and shorter to write something like this:
var cart = CartFactory();
cart
.addCartItem(vintageClock)
.addCartItem(motivateCards)
.removeCartItem(vintageClock);
For this specific example, chaining makes sense.
In JavaScript, the builder pattern can be seen as a subset of the abstract pattern. Abstract factories group ideas together. Builder factories separate the object attributes from the methods. The abstract pattern itself is a composition idea, while the builder pattern takes it one step further and groups the contents inside the factory based on type — that is, attributes and methods. Abstract and builder patterns are not exclusive from each other. Instead, they can be seen as building on top of each other for maximum efficiency. What the code ends up looking like really depends on your requirements and how fine grain you want to go with it.
Prototypes
You can do a lot with the three creational patterns introduced so far. Factory methods lets you create succinct object blueprints. Abstract and builder patterns give you an organizational method to help arrange the contents inside your factory, object shape, and logic.
You can say that patterns are just syntax sugar and in a way it is. Sugar makes things nicer and tastier. When used in the right quantities in suitable situations, it can make your code easier to understand and mentally digest. There’s no point in writing code that no one can read. Just because it’s “complex” doesn’t mean that it accurately captures the complexity of your requirements, nor does it set yourself up to impress strangers, teammates, or your future self.
In JavaScript, the prototype pattern is another way to create object blueprints without using the class
keyword. But unlike the factory method, abstract, and builder patterns, the prototype pattern does require the new
keyword to work properly.
To keep your prototype pattern organized, it’s often good practice to separate your properties from your methods. This is something that’s required by the builder pattern but not necessary in the prototype pattern. In a way, separating out your properties and methods is like creating a constructor function inside a class
without it being explicitly called one. Your code will still work without the ideological abstraction. However, it can be hard to track down information if your properties and methods are mixed together.
Let’s take the cart factory we’ve previously built and refactor it with a prototype flavor.
function NewCartItem(sku, name, price){
return {sku: sku, name:name, price:price}
}
function ApplyDiscount(discount){
return function(cartTotal){
return cartTotal * (1-discount);
}
}
//refactored Cart into a prototype pattern
//this is the equivalent of your constructor
const Cart = function(){
this.cartItems = [];
cartTotal = 0;
}
//this is the equivalent of your methods
Cart.prototype.addCartItem = function(item){
this.cartItems.push(item);
return this;
}
Cart.prototype.removeCartItem = function(item){
for(let i = 0; i < this.cartItems.length; i++){
if(this.cartItems[i].sku === item.sku){
this.cartItems.splice(i, 1);
}
}
return this;
}
Cart.prototype.applyDiscount = function(discount){
for(let i = 0; i < this.cartItems.length; i++){
this.cartTotal =+ this.cartItems[i].price;
}
this.cartTotal = discount(this.cartTotal);
return this;
}
//setting up the objects with factory methods
const vintageClock = NewCartItem(1239, "Vintage Clock", 19.99);
const motivateCards = NewCartItem(3897, "Motivational Cards", 10.99);
const discount15 = ApplyDiscount(0.15);
const discount25 = ApplyDiscount(0.25);
//initializing a new cart object
var cart = new Cart();
cart
.addCartItem(vintageClock)
.addCartItem(motivateCards)
.removeCartItem(vintageClock)
.applyDiscount(discount15);
prototype
is a special method that is accessible to every object in JavaScript. The code above is almost identical to the previous iterations. The only difference is that rather than having everything in one big function, they are broken down into extensions through prototype
. When you return this
, it makes the object chainable and infuses it with a little bit of a builder spirit.
Why? Because one of the ideas behind the builder pattern is to give you the ability to call your object methods in any order you want. When you return this
, it gives you the entire object and gives you access to everything you need rather than just a snippet of it. This makes it chainable without it having to write complicated JavaScript.
The major perk of a prototype pattern is the ability to break up your object blueprint. In theory, you could write something like this for the builder pattern:
//refactored builder pattern
const CartFactory = function(){
//builder object definition
cartItems = [];
cartTotal = 0;
addCartItem = function(item){
this.cartItems.push(item);
return this;
}
removeCartItem = function(item){
for(let i = 0; i < this.cartItems.length; i++){
if(this.cartItems[i].sku === item.sku){
this.cartItems.splice(i, 1);
}
}
return this;
}
applyDiscount = function(discount){
for(let i = 0; i < this.cartItems.length; i++){
this.cartTotal =+ this.cartItems[i].price;
}
this.cartTotal = discount(this.cartTotal);
return this;
}
//composing the object
return{
cartItems,
cartTotal,
addCartItem,
removeCartItem,
applyDiscount
}
}
The functions are abstracted out and the final return
object is composed at the end. While it works well for small objects, big objects with an unknown amount of extensions can fall apart due to size over time.
Using the prototype pattern lets you create additional methods while maintaining your overall file size. However, the downside is that you don’t get to see all the methods in one place. They can be scattered across multiple files rather than just sitting inside one function. There is no one pattern fits all. What you end up using really depends on the requirements and situation of your application.
Singletons
How many times can a Cart
object be re-written? Perhaps one more time.
A singleton pattern is probably one of the most misunderstood patterns when it comes to creating objects. Why? Because you can only have a single instance of that instance at any one time.
It’s misunderstood because it’s often associated with code duplication because you can only have one instance. If you want the same object again, then you will have to create another object. Another issue that many developers have with singletons is that it’s often presented as a global state. If you have too many, it can pollute the global scopes or create conflicts within large applications.
But not every scenario requires you to write the same code twice — or have the need for multiple objects of the same thing. For example, in most circumstances, our Cart
object only needs to be instantiated once for any particular session. You don't want a user to have multiple carts with different items in the virtual trolley. Singletons help ensure this in the way the code is structured.
So how do you write a singleton?
The easiest way in JavaScript is to write an object literal — that is, declare it as a normal object with properties and functions to represent your methods.
Here’s what the Cart
object looks like as a singleton:
//cart as a singleton (object literal)
const cart = {
cartItems: [],
cartTotal: 0,
addCartItem: function(item){
this.cartItems.push(item);
return this;
},
removeCartItem: function(item){
for(let i = 0; i < this.cartItems.length; i++){
if(this.cartItems[i].sku === item.sku){
this.cartItems.splice(i, 1);
}
}
return this;
},
applyDiscount: function(discount){
for(let i = 0; i < this.cartItems.length; i++){
this.cartTotal =+ this.cartItems[i].price;
}
this.cartTotal = discount(this.cartTotal);
return this;
}
}
When you write an object literal, you don’t need to instantiate anything. The object is already there. You can access and use it in its current state. There is only one instance of the cart
object and no other objects can be created from it. Using const
will protect your object from a certain level of unwanted mutations.
In JavaScript, const
protects an object with weak immutability. This means that the shape of the object - the properties and methods originally declared - remains untouchable. The values assigned to them, however, can change.
For example, when using const
, you can't do this to your declared cart
object literal:
cart = "whoops";
This will result in a TypeError: Assignment to constant variable
. But you can do this:
cart.cartTotal = 5;
And it doesn’t prevent you from changing the other assignments either. So you can do something like this:
cart.applyDiscount = "double whoops";
This is an issue faced by all objects, regardless of how they’ve been declared or instantiated. It means that your object created by the factory method, abstract, and builder pattern will also have the same issue. So how can you prevent this from happening?
JavaScript has a method called freeze()
which allows you to set the object's state with immutability. freeze()
will 'freeze' your object in its current state and any attempts to change it result in the offending code being silently ignored.
Here’s what it looks like in action:
Object.freeze(cart);
So when you try to do something like this:
cart.applyDiscount = 5;
Nothing happens. Your original assignment remains safe and the code will continue on as if the new assignment never existed. Freezing an object will only protect it from external changes. Changes within the object caused by an internal method is not covered by freeze()
. This means you can still run your methods as expected.
For example, you can still do the following to your cart
object:
function NewCartItem(sku, name, price){
return {sku: sku, name:name, price:price}
}
const vintageClock = NewCartItem(1239, "Vintage Clock", 19.99);
const motivateCards = NewCartItem(3897, "Motivational Cards", 10.99);
cart
.addCartItem(vintageClock)
.addCartItem(motivateCards);
Everything about our cart
object is almost the same, except for it is only one and enforced instance in a singleton.
Final Notes
The pattern example used here are created using pure functions instead of classes. In part, this is done on purpose. When you create your blueprints with normal functions, there’s no need for the new
keyword. Instead, convention is used to differentiate the difference between an object instance (camelCasing
) and the object blueprint (UpperCamelCasing
).
When you use normal functions, this
refers to the object itself. Closures and lexical scoping makes it possible. However, this
can be a confusing one when it comes to classes and you can accidentally reference the window
object rather than something inside your object.
Ultimately, all the creational patterns presented here produces almost the same results. The major difference between them is how information is arranged. There is no right or wrong way to write a piece of JavaScript code — there're only degrees of suitability.
Suitability can also change over time as your requirements change, along with team preferences and how you think about the problem you’re trying to solve. So always look at your problems first. Spend most of your time figuring out and understanding what you want to achieve, lay down the logical pathways, come to a general understanding with your team on how to structure things, and then start coding.