JavaScript Patterns
// Visitor lets you define a new operation without changing
// the class of the elements on which it operates.
// Define 'Employee' Class
function Employee(name, salary, vacation) {
this.name = name;
this.salary = salary;
this.vacation = vacation;
}
Employee.prototype = {
getName: function() {
return this.name;
},
getSalary: function() {
return this.salary;
},
setSalary: function(newSalary) {
this.salary = newSalary;
},
getVacation: function() {
return this.vacation;
},
setVacation: function(vacation_days) {
this.vacation = vacation_days;
},
// Let visitor apply its functionallity
// on the instance, by passing the instance
// object to the 'visit' function.
accept: function(visitor) {
visitor.visit(this);
}
}
// Define Visitors
function RaiseSalary() {} // Salaray raise visitor
RaiseSalary.prototype.visit = function(employee) {
employee.setSalary(employee.getSalary() * 1.2);
}
function AddVacationDays() {} // Vacation days addition visitor
AddVacationDays.prototype.visit = function(employee) {
employee.setVacation(employee.getVacation() + 3);
}
/****************
Usage Example
***************/
// data logger
function logData(employees, msg) {
console.log(msg);
console.log('--------------------------');
employees.forEach(function(employee) {
console.log('Name: ' + employee.getName());
console.log('Salary: ' + employee.getSalary());
console.log('Vacation Days: ' + employee.getVacation());
console.log('---');
});
console.log('\n\n\n');
}
// set employees
var employees = [
new Employee('Guy', 17000, 10),
new Employee('Dor', 8000, 7),
new Employee('Josh', 11000, 12),
];
// initialize visitors
var salaryVisitor = new RaiseSalary();
var vacationVisitor = new AddVacationDays();
// log data before accepting visitors
logData(employees, 'Before accepting visitors:');
// apply visitors functionallity
employees.forEach(function(employee) {
employee.accept(salaryVisitor);
employee.accept(vacationVisitor);
});
// log data to see the changes after accepting visitors
logData(employees, 'After accepting visitors:');
//Structure
var mySingleton = (function() {
function init(options) {
//some private variables
var x = '1', y = 2, z = 'Abc', pi = Math.PI;
//return public methods(accessing private variables if needed.)
return {
X : x,
getPi : function() {
return pi;
}
}
}
//There must be exactly one instance of a class,
//and it must be accessible to clients from a well-known access point
var instanceOfSingleton;
return {
initialize: function(options) {
//initialize only if not initialized before
if(instanceOfSingleton === undefined) {
instanceOfSingleton = init(options);
}
return instanceOfSingleton;
}
};
})();
var singleton = mySingleton.initialize();
console.log(singleton.X); //'1'
console.log(singleton.getPi());//3.141592653589793
// setTimeout() inside a loop.
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i); // prints 4 4 4
}, 1000);
}
// Work all fine if we use `let` keyword in ES6
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i); // prints 1 2 3
}, 1000);
}
// Locking the looped values inside a IIFE (closure).
for (var i = 1; i <= 3; i++) {
(function (index) {
setTimeout(function () {
console.log(index); // prints 1 2 3
}, 1000);
})(i);
}
// Note: When the IIFE is inside the setTimeout, it prints the corrct values.
// However, the values are printed immediately and not after the timout value.
// Essentially rendering the setTimeout useless.
// setTimeout() needs a fn as it's 1st parameter.
for (var i = 1; i <= 3; i++) {
setTimeout((function (index) {
console.log(index); // prints 1 2 3
})(i), 1000);
}
// You can still use and IIFE inside setTimeout(), but you need to return a function as it's first parameter.
for (var i = 1; i <= 3; i++) {
setTimeout((function (index) {
return function () {
console.log(index); // prints 1 2 3
}; // IIFE needs to return a function that setTimeout can schedule.
})(i), 1000);
}
// Note: Both setTimeout and setInterval accept and additional params that can be passed to the callback fn.
// Thanks: https://twitter.com/WebReflection/status/701091345679708161
for (var i = 0; i < 10; i++) {
setTimeout(function (i) {
console.log(i);
// This will print 0 1 2 3 4 5 6 7 8 9
}, 1000, i)
}
// Another way is to just create a separate function.
for (var i = 0; i < 10; i++) {
registerTimeout(i);
}
function registerTimeout (i) {
setTimeout(function () {
console.log(i);
// This will print 0 1 2 3 4 5 6 7 8 9
}, 1000);
}
// You should use proxy pattern when you want to extend a
// class functionality without changing its implementation
/****************
Original Class
****************/
function Hotel(stars, isCityCenter, isNew, numberOfRooms, avgRoomSize) {
this.stars = stars;
this.isCityCenter = isCityCenter;
this.isNew = isNew;
this.numberOfRooms = numberOfRooms;
this.avgRoomSize = avgRoomSize;
}
Hotel.prototype = { // Define getters and setters
getStars: function() {
return this.stars;
},
setStars: function(starsRate) {
this.stars = starsRate;
},
isCityCenter: function() {
return this.isCityCenter;
},
toggleCenter: function() {
this.isCityCenter = !this.isCityCenter;
},
isNew: function() {
return this.isNew;
},
toggleNew: function() {
this.isNew = !this.isNew;
},
getNumberOfRooms: function() {
return this.numberOfRooms;
},
setNumberOfRooms: function(num) {
this.numberOfRooms = num;
},
getAvgRoomSize: function() {
return this.avgRoomSize;
},
setAvgRoomSize: function(newAvg) {
this.avgRoomSize = newAvg;
}
}
/****************
Proxy
****************/
var HotelProxy = function(stars, isCityCenter, isNew, numberOfRooms, avgRoomSize) {
// Create Hotel Instance
var hotel = new Hotel(stars, isCityCenter, isNew, numberOfRooms, avgRoomSize);
// Private function
function scoreByStars(stars) {
switch(stars) {
case(5):
return 6;
case(4):
return 5;
case(3):
return 3;
case(2):
return 1.5;
default:
return 0.5;
}
}
// Extend hotel instance
Object.assign(hotel, {
getScore: function() {
var score = scoreByStars(hotel.stars);
if(hotel.isCityCenter) {
score += 2;
}
if(hotel.isNew) {
score += 1.5;
}
if(hotel.numberOfRooms > 5000) {
score += 0.5;
}
return score;
},
getHotelRoomsVolume: function() {
return hotel.numberOfRooms * hotel.avgRoomSize;
}
});
// Return extended instance
return hotel;
}
// Usage example
var hp = HotelProxy(4, true, false, 2800, 150);
console.log(hp.getScore()); // 7
console.log(hp.getHotelRoomsVolume()); // 420000
hp.setAvgRoomSize(160);
console.log(hp.getHotelRoomsVolume()); // 448000
hp.setStars(5);
console.log(hp.getScore()); //8
//Structure
var obj = {
name: "My object's name.",
objFunc: function () {
console.log( "Yay! A function!" );
}
};
//set a new object/function's prototype as another existing one
//here newObj is created with prototype as obj.
var newObj = Object.create( obj );
newObj.gender = 'Female';
// Now we can see that one is a prototype of the other
console.log( newObj.name ); //My object's name.
console.log( newObj.gender ); //Female
// OLOO (objects linked to other objects) pattern explored
// CONSTRUCTOR SYNTAX VS OLOO
// Constructor form
function Foo() {
}
Foo.prototype.y = 11;
function Bar() {
}
// Object.create(proto[, propertiesObject]) method creates a new object with the specified prototype object and properties.
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.z = 31;
var x = new Bar();
console.log(x.y + x.z); // 42
// OLOO form
var FooObj = {y: 11};
var BarObj = Object.create(FooObj);
BarObj.z = 31;
var x = Object.create(BarObj);
console.log(x.y + x.z); // 42
/**
* CLASS SYNTAX VS OLOO
*/
// ES6 Class style
class Foo {
constructor(x, y, z) {
// Object.assign(target, ...sources) method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.
Object.assign(this, {x, y, z});
}
hello() {
console.log(this.x + this.y + this.z);
}
}
var instances = [];
for (var i = 0; i < 500; i++) {
instances.push(
new Foo(i, i * 2, i * 3)
);
}
instances[37].hello(); // 222
// OLOO Form
function Foo(x, y, z) {
return {
hello() {
console.log(this.x + this.y + this.z);
},
x,
y,
z
};
}
var instances = [];
for (var i = 0; i < 500; i++) {
instances.push(
Foo(i, i * 2, i * 3)
);
}
instances[37].hello(); // 222
function theSubject(){
this.handlerList = [];
}
theSubject.prototype = {
addObserver: function(obs) {
//add all the observers in an array.
this.handlerList.push(obs);
console.log('added observer', this.handlerList);
},
removeObserver: function(obs) {
//remove given observer from the array.
for ( var ii=0, length = this.handlerList.length; ii<length; ii++ ) {
if(this.handlerList[ii] === obs) {
this.handlerList.splice(ii,1);
console.log('removed observer', this.handlerList);
}
}
},
notify: function(obs, context) {
//for all functions in handler, notify
var bindingContext = context || window;
this.handlerList.forEach(function(fn){
fn.call(bindingContext, obs);
});
}
}
function init() {
var theEventHandler = function(item) {
console.log("fired: " + item);
};
var subject = new theSubject();
subject.addObserver(theEventHandler); //adds the given function in handler list
subject.notify('event #1'); //calls the function once.
subject.removeObserver(theEventHandler); //removes this function from the function list
subject.notify('event #2'); //notify doesn't call anything
subject.addObserver(theEventHandler); //adds the function again
subject.notify('event #3'); //calls it once with event 3
}
init();
// Perhaps the most used pattern in javascript web development.
// MVC(Model-View-Controller) seperates data model representation
// from data visuallization.
// The glue that connects the model changes and view updates
// is the contoller.
// Define Account Model
function AccountModel(owner, balance) {
this.owner = owner;
this.balance = balance;
}
AccountModel.prototype = {
getOwner: function() {
return this.owner;
},
getBalance: function() {
return this.balance;
}
}
// Define Account View
function AccountView() {}
AccountView.prototype.showAccountDetails = function(owner, balance) {
console.log('Account owner: ' + owner);
console.log('Account balance: ' + balance);
}
// Define Account Controller
function AccountController(model, view) {
this.accountView = function() {
view.showAccountDetails(model.getOwner(), model.getBalance());
}
this.deposit = function(amount) {
this.setBalance(model.getBalance() + amount);
this.accountView();
}
this.discount = function(amount) {
this.setBalance(model.getBalance() - amount);
this.accountView();
}
this.setOwner = function(newOwner) {
model.owner = newOwner;
this.accountView();
}
this.setBalance = function(newBalance) {
model.balance = newBalance;
this.accountView();
}
}
// Usage Example
var controller = new AccountController(new AccountModel('Ben', 15000), new AccountView());
controller.accountView();
// Account owner: Ben
// Account balance: 15000
controller.deposit(500);
// Account owner: Ben
// Account balance: 15500
controller.discount(120);
// Account owner: Ben
// Account balance: 15380
controller.setOwner('Bob');
// Account owner: Bob
// Account balance: 15380
controller.setBalance(0);
// Account owner: Bob
// Account balance: 0
controller.deposit(1000000);
// Account owner: Bob
// Account balance: 1000000
//Structure
var a = (function(){
//private variables & methods
var privateVar = 'X';
//public methods and variables
return {
getX: function(){
return privateVar;
}
}
})();
a.getX(); //X
//Structure and Example
var mixins = {
get: function(){
console.log( "get this item" );
},
set: function(){
console.log( "set this item" );
},
delete: function(){
console.log( "delete it right away!" );
}
};
// Another skeleton constructor
function aConstructor(){
this.thatIsAllFolks = function(){
console.log('Nope! I refuse to do anything anymore!!!')
};
}
// Extend both protoype with our Mixin
for(var key in mixins) aConstructor.prototype[key] = mixins[key];
// Create a new instance of aConstructor
var myMixinConstructor = new aConstructor();
myMixinConstructor.get(); //get this item
myMixinConstructor.thatIsAllFolks(); //Nope! I refuse to do anything anymore!!!
// Memento pattern is used to restore state of an object to a previous state.
function Memento(initialState) {
var state = initialState || null;
var stateList = state ? [state] : [];
return {
getState: function() {
return state;
},
getStateList: function() {
return stateList;
},
get: function(index) {
if(index > -1 && index < stateList.length) {
return stateList[index];
}
else {
throw new Error('No state indexed ' + index);
}
},
addState: function(newState) {
if(!newState)
throw new Error('Please provide a state object');
state = newState;
stateList.push(newState);
}
}
}
// Helper function used to deep copy an object using jQuery
function copy(obj) {
return jQuery.extend(true, {}, obj);
}
// Example Usage, please notice that using this pattern, you should
// not mutate objects or arrays, but clone them, since they are passed
// by reference in javascript.
// If your state is a string or a number however, you may mutate it.
var songs = {
Queen: ['I want to break free', 'Another on bites the dust', 'We will rock you'],
Scorpins: ['Still loving you', 'Love will keep us alive', 'Wind of change'],
Muse: ['Butterflies and hurricanes', 'Starlight', 'Unintended'],
BeeGees: ['How deep is your love', 'Staying alive']
}
var memento = Memento(copy(songs)); // Initialize Memento
songs.BeeGees.push('Too much heaven');
songs.Muse.push('Hysteria');
memento.addState(copy(songs)); // Add new state to memento
songs['Abba'] = ['Mama mia', 'Happy new year'];
songs['Eric Clapton'] = ['Tears in heaven', 'Bell bottom blues'];
memento.addState(copy(songs)); // Add new state to memento
console.log(memento.getStateList()); // log state list
console.log(memento.getState()); // log current state
console.log(memento.get(1)); // log second state
songs = memento.get(0); // set songs to initial state
memento.addState(copy(songs)); // Add new old state to memento
console.log(memento.getStateList()); // log state list
// Mediator pattern is used to reduce communication
// complexity between multiple objects or classes.
function Mediator() {
var users = [];
return {
addUser: function(user) {
users.push(user);
},
// Message sending business logic
publishMessage: function(msg, receiver) {
if(receiver) {
receiver.messages.push(msg);
}
else {
users.forEach(function(user) {
user.messages.push(msg);
});
}
}
}
}
// Usage Example
var mediator = Mediator(); // Initialize mediator
// Define user class
function User(name) {
this.name = name;
this.messages = [];
mediator.addUser(this);
}
User.prototype.sendMessage = function(msg, receiver) {
msg = '[' + this.name + ']: ' + msg;
mediator.publishMessage(msg,receiver);
}
// Initialize users
var u1 = new User('Donald');
var u2 = new User('Peter');
var u3 = new User('Anna');
// Message sending
u1.sendMessage('Hi, anybody here?');
u2.sendMessage('Hi Donald, nice to meet you.', u1);
u3.sendMessage('Hi Guys!');
// Access elements of a collection sequentially without
// needing to know the underlying representation.
/************
Iterator
************/
function Iterator(arr) {
var currentPosition = -1;
return {
hasNext:function() {
return currentPosition+1 < arr.length;
},
next: function() {
if(!this.hasNext())
return null;
currentPosition++;
return arr[currentPosition];
}
}
}
// Example Usage
var people = [{id:1,name:'John'}, {id:2,name:'George'}, {id:3,name:'Guy'}];
var peopleIterator = Iterator(people); // Create Iterator for 'people'
while(peopleIterator.hasNext()) {
var person = peopleIterator.next();
console.log(person.name + '\'s id is: ' + person.id + '!');
}
// John's id is: 1!
// George's id is: 2!
// Guy's id is: 3!
//only common data here is model and brand,
//and created a flyweight object, that saves memory
var Car = function(model, brand) {
this.model = model;
this.brand = brand;
}
//carFactory using the common car model/method
var carFactory = (function() {
var existingCars = {}, existingCar;
return {
createCar: function(model, brand) {
existingCar = existingCars[model];
if (!!existingCar) {
return existingCar;
}
var car = new Car(model, brand);
existingCars[model] = car;
return car;
}
}
})();
//carProductionManager using the common car model/method
var carProductionManager = (function() {
var carDb = {};
return {
addCar: function(carId, model, brand, color, carType){
var car = carFactory.createCar(model, brand);
carDb[carId] = {
color: color,
type: carType,
car: car
}
},
repaintCar: function(carId, newColor) {
var carData = carDb[carId];
carData.color = newColor
}
}
})();
var fromPrototype = function(prototype, object) {
var newObject = Object.create(prototype);
for (var prop in object) {
if (object.hasOwnProperty(prop)) {
newObject[prop] = object[prop];
}
}
return newObject;
};
// Define our `DeviceFactory` base object
var DeviceFactory = {
screen: function() {
return 'retina';
},
battery: function() {
return 'lithium ion battery';
},
keypad: function() {
return 'keyboard';
},
processor: function() {
return 'Intel Core-i5';
}
};
// Extend `DeviceFactory` with other implementations
DeviceFactory.makeLaptop = function() {
return fromPrototype(DeviceFactory, {
screen: function() {
return 'retina 13 inches';
},
battery: function() {
return 'lithium ion 9 hours battery';
},
keypad: function() {
return 'backlit keyboard';
},
processor: function() {
return 'Intel Core-i5'
}
});
};
DeviceFactory.makeSmartPhone = function() {
return fromPrototype(DeviceFactory, {
screen: function() {
return 'retina 5 inches';
},
battery: function() {
return 'lithium ion 15 hours';
},
keypad: function() {
return 'touchscreen keypad';
},
processor: function() {
return 'ARMv8'
}
});
};
DeviceFactory.makeTablet = function() {
return fromPrototype(DeviceFactory, {
screen: function() {
return 'retina 9 inches';
},
battery: function() {
return 'lithium ion 15 hours';
},
keypad: function() {
return 'touchscreen keypad';
},
processor: function() {
return 'ARMv8'
}
});
};
var appleMacbookPro = DeviceFactory.makeLaptop();
console.log(appleMacbookPro.screen()); // returns 'retina 13 inches';
var iPhoneSomeS = DeviceFactory.makeSmartPhone();
var iPadSomeSS = DeviceFactory.makeTablet();
//Structure and example
//Facading hides complexities from the user.
var mouse = (function() {
var privates = {
getActivity: function(act) {
var activity = act.toLowerCase();
if(activity === 'click') {
return "User is clicking";
} else if (activity === 'hover') {
return "User is hovering";
} else if (activity === 'rightclick') {
return "User right clicked";
} else if (activity === 'scroll') {
return "User scrolled"
} else {
return "Unrecognised activity";
}
}
}
return {
facade: function(activity) {
return privates.getActivity(activity);
}
}
})();
console.log(mouse.facade('hover')); //User is hovering
//Structure
function functionA () {
this.a = function() { return 'a'; }
}
function describeA ( anA ) {
var aa = anA.a();
anA.a = function () {
return 'An "' + aa + '" is the first alphabet in English and the most important one.'
}
}
var anA = new functionA();
describeA( anA );//here aa='a'
var output = anA.a();
console.log(output); //'An a is the first alphabet in English and the most important one.'
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
// Usage
var myEfficientFn = debounce(function() {
// All the taxing stuff you do
}, 250);
window.addEventListener('resize', myEfficientFn);
//Structure
function aFunction(a,b) {
this.a = a;
this.b = b;
}
aFunction.prototype.protoFunction = function(){
return this.a;
}
var aA = new aFunction('a','b');
console.log(aA.protoFunction());
//'a';
// in Javascript, is completely pointless.
// Structure and example
var commandPattern = (function(){
var commandSet = {
doSomething: function(arg1, arg2) {
return "This is argument 1 "+ arg1 + "and this is arg 2 "+ arg2;
},
doSomethingElse: function(arg3) {
return "This is arg 3 "+arg3;
},
executeCommands: function(name) {
return commandSet[name] && commandSet[name].apply( commandSet, [].slice.call(arguments, 1) );
//gives arguments list
}
};
return commandSet;
})();
commandPattern.executeCommands( "doSomethingElse", "Ferrari");
commandPattern.executeCommands( "doSomething", "Ford Mondeo", "54323" );