CSCI 4513
Week 3, Lecture 5
Dwarven workshops created legendary treasures using different crafting patterns, like constructors and factory functions create objects.
The Tale:
When Loki cut off Sif's golden hair, he sought the sons of Ivaldi to create magical replacements. These master craftsmen created not only golden hair but also Gungnir (Odin's spear) and Skidbladnir (Freyr's ship that folds into a pocket).
Loki then challenged the brothers Brokk and Eitri, who forged Gullinbursti (a glowing boar), Draupnir (a ring that creates new gold rings), and Mjolnir (Thor's mighty hammer). Each treasure was unique with its own properties and behaviors, yet all followed similar crafting patternsโspecialized workshops creating magical items through different implementations.
// Constructor - blueprint for treasures
function DivineWeapon(name, wielder, power) {
this.name = name;
this.wielder = wielder;
this.power = power;
}
// Factory Function - flexible creation
function createTreasure(type, owner) {
return {
type,
owner,
enchant() { return `${this.type} glows with magic`; }
};
}
// Module Pattern - workshop secrets
const DwarvenWorkshop = (function() {
const secretTechnique = "Ancient magic";
return {
forgeTreasure(specs) { /* uses secret */ }
};
})();
After pasting the code above into your console, test each pattern:
const mjolnir = new DivineWeapon("Mjolnir", "Thor", 100);console.log(mjolnir.name);const ring = createTreasure("Ring", "Odin");ring.enchant();DwarvenWorkshop.forgeTreasure({name: "Gungnir"});console.log(DwarvenWorkshop.secretTechnique); โ This returns undefined (it's private!)Click each card to reveal thoughts!
Two workshops created different treasures using their own methods. When would you use a factory function vs a constructor?
Consider: Use factories when you need private variables, want to return different object types, or don't want to use new. Use constructors when you need inheritance via prototypes or want clear object types. Like different workshops, each approach has its specialization.
Each treasure had unique properties. How does this parallel object creation patterns?
Consider: Every object instance can have unique property values while sharing method definitions. Like Mjolnir and Gungnir were both legendary but different, objects from the same pattern can be customized while maintaining common functionality.
The workshops kept some techniques secret. How does the module pattern provide encapsulation?
Consider: Closures let you keep implementation details private while exposing only a public interface. Like dwarven forging secrets, internal variables and helper functions remain hidden. Only the methods you explicitly return become accessible to the outside world.
Scoping asks: "Where is a certain variable available to me?" - it indicates the current context of a variable.
// var - Function scoped (pre-ES6)
function oldWay() {
var x = 1;
if (true) {
var x = 2; // Same variable!
console.log(x); // 2
}
console.log(x); // 2 - var leaked out
}
// let/const - Block scoped (ES6+)
function newWay() {
let x = 1;
if (true) {
let x = 2; // Different variable!
console.log(x); // 2
}
console.log(x); // 1 - let stayed in block
}
var - Function-scoped. Redeclaring inside a block overwrites the outer variable.
let - Block-scoped. Creates a new variable confined to its block { }.
The Bug - With var, both x declarations refer to the same variable, so the second overwrites the first.
This is why var was replacedโaccidental overwrites caused hard-to-find bugs.
const by default, let when you need to reassign, avoid var.
let globalAge = 23; // Global variable
function printAge(age) {
var varAge = 34; // Function scoped
if (age > 0) {
// Block-scoped variable
const constAge = age * 2;
console.log(constAge); // Works!
}
// ERROR! constAge only exists in if block
console.log(constAge);
}
printAge(globalAge);
// ERROR! varAge only exists in function
console.log(varAge);
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ GLOBAL SCOPE โ โ โโ globalAge โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ FUNCTION SCOPE โโ โ โ โโ varAge โ โโ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ BLOCK SCOPE โโโ โ โ โ โโ constAge โ โโโ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโ โ โ constAge โ (not here) โโ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ varAge โ (not here) โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Scope Levels - Variables are only accessible within their scope and nested inner scopes.
function makeAdding(firstNumber) {
// "first" is scoped within makeAdding
const first = firstNumber;
return function resulting(secondNumber) {
// "second" is scoped within resulting
const second = secondNumber;
return first + second; // Still access first!
}
}
const add5 = makeAdding(5);
console.log(add5(2)); // 7
console.log(add5(10)); // 15
Outer function - Creates first and returns an inner function.
Inner function - "Closes over" first, keeping it alive even after outer function returns.
The Magic - add5 remembers first = 5 and can use it later!
This is a closure: a function bundled with its lexical environment.
A closure is the combination of a function and the surrounding state (lexical environment) in which it was declared. This includes any local variables that were in scope at the time.
add5 is a reference to the resulting functionfirst variablefirst remains available for use!new keyword: Forgetting it causes silent failuresinstanceof issues: Not reliable in JavaScriptโ ๏ธ Result: Constructors have become unpopular in favor of Factory Functions!
const User = function(name) {
this.name = name;
this.discord = "@" + name;
}
// Need 'new' keyword!
const user = new User("josh");
Issues: Requires new, uses this binding, forgetting new causes bugs.
function createUser(name) {
const discord = "@" + name;
return { name, discord };
}
// Just call it!
const user = createUser("josh");
Benefits: No new, no this, just returns a plain object.
new needed!
const name = "Bob";
const age = 28;
const color = "red";
// The old way (redundant)
const oldObject = { name: name, age: age, color: color };
// The new way (clean!)
const newObject = { name, age, color };
// Bonus: Great for console.log!
console.log(name, age, color);
// Bob 28 red (messy)
console.log({ name, age, color });
// { name: "Bob", age: 28, color: "red" }
{ name: name } โ { name }
When property name matches variable name, just use the name once.
Why It Matters - Factory functions return objects constantly. Shorthand makes them readable.
Debug Tip - Wrapping values in {} shows labeled output in console.
// Object destructuring
const obj = { a: 1, b: 2 };
const { a, b } = obj;
// Creates variables a and b
// Array destructuring
const array = [1, 2, 3, 4, 5];
const [zerothEle, firstEle] = array;
// Creates variables for first two elements
// Destructuring in function parameters
function createPlayer({ name, level }) {
return { name, level };
}
createPlayer({ name: "Link", level: 50 });
{ a, b } = obj - Extract properties into variables with matching names.
[x, y] = arr - Extract array elements by position.
Parameter Destructuring - Unpack object properties directly in function signature. Very common in factories!
Destructuring is the opposite of shorthand: extract values instead of creating objects.
function createUser(name) {
const discordName = "@" + name;
// Private variable - not returned!
let reputation = 0;
// Methods using closure
const getReputation = () => reputation;
const giveReputation = () => reputation++;
return { name, discordName,
getReputation, giveReputation };
}
const josh = createUser("josh");
josh.giveReputation();
josh.giveReputation();
console.log(josh.reputation); // undefined!
console.log(josh.getReputation()); // 2
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ createUser closure โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ reputation (private) โ โ โ โ getReputation โโโโโโโโโโผโโโผโโบ returned โ โ giveReputation โโโโโโโโโผโโโผโโบ returned โ โโโโโโโโโโโโโโโโโโโโโโโโโโ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Trick - reputation exists in the closure but is NOT in the returned object.
Access Control - Only the returned methods can read/modify reputation.
reputation to -18000// Base factory
function createUser(name) {
const getReputation = () => reputation;
const giveReputation = () => reputation++;
let reputation = 0;
return { name, getReputation, giveReputation };
}
// Extended factory
function createPlayer(name, level) {
const { getReputation, giveReputation } = createUser(name);
const increaseLevel = () => level++;
return { name, getReputation, giveReputation,
increaseLevel };
}
โโโโโโโโโโโโโโโโโโโ
โ createUser โ
โ โโ name โ
โ โโ getReputation
โ โโ giveReputation
โโโโโโโโโโฌโโโโโโโโโ
โ destructure
โผ
โโโโโโโโโโโโโโโโโโโ
โ createPlayer โ
โ โโ name โ
โ โโ getReputation (from User)
โ โโ giveReputation (from User)
โ โโ increaseLevel (new)
โโโโโโโโโโโโโโโโโโโ
Composition - Call the base factory, destructure its methods, then add new functionality.
function createUser(name) {
const getReputation = () => reputation;
const giveReputation = () => reputation++;
let reputation = 0;
return { name, getReputation, giveReputation };
}
function createPlayer(name, level) {
const user = createUser(name);
const increaseLevel = () => level++;
// Merge user properties with new ones
return Object.assign({}, user, { increaseLevel });
}
const player = createPlayer("Link", 50);
player.giveReputation();
player.increaseLevel();
Object.assign(target, ...sources)
Copies all properties from source objects into target.
Object.assign(
{}, // empty target
user, // copies user props
{ increaseLevel} // adds new props
)
// โ merged object with all properties
Alternative Syntax - Same result as destructuring, but copies ALL properties without listing them.
// Regular factory - can call multiple times
function createCalculator() {
const add = (a, b) => a + b;
const sub = (a, b) => a - b;
return { add, sub };
}
// IIFE - runs once immediately
const calculator = (function() {
const add = (a, b) => a + b;
const sub = (a, b) => a - b;
return { add, sub };
})(); // <-- Note the () at the end!
calculator.add(3, 5); // 8
(function() { ... }) - Wrap function in parentheses to make it an expression.
() at the end - Immediately invoke (call) that function.
Result - calculator holds the returned object, NOT the function. There's only ever one instance.
IIFE = Immediately Invoked Function Expression
Pattern: Wrap factory in parentheses, immediately invoke it with ()
const calculator = (function() {
// Private helper function
const validate = (a, b) => {
if (typeof a !== 'number' ||
typeof b !== 'number') {
throw new Error('Must be numbers');
}
};
// Public API
const add = (a, b) => {
validate(a, b);
return a + b;
};
const sub = (a, b) => {
validate(a, b);
return a - b;
};
return { add, sub }; // Only these exposed
})();
calculator.add(3, 5); // 8
calculator.validate(1, 2); // undefined!
โโโโโโโโโโโโโโโโโโโโโโโโโโโ โ calculator module โ โ โโโโโโโโโโโโโโโโโโโโโ โ โ โ validate (PRIVATE)โ โ โ โโโโโโโโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโโโโโโโ โ โ โ add โโโโโโโโโโโโโโโผโโโผโโบ PUBLIC โ โ sub โโโโโโโโโโโโโโโผโโโผโโบ PUBLIC โ โโโโโโโโโโโโโโโโโโโโโ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโ
Private Helper - validate is used internally but never exposed to outside code.
Public API - Only add and sub are returned, so only they can be called.
Encapsulation: Bundling data and code into a single unit with selective access to the internals.
calculator.add()// Without namespacing - DISASTER!
function add(a, b) { return a + b; }
function add(a, b) { return String(a) + String(b); }
// Which add() are we calling?
// With namespacing - CLEAR!
const mathOps = (function() {
const add = (a, b) => a + b;
return { add };
})();
const stringOps = (function() {
const add = (a, b) => String(a) + String(b);
return { add };
})();
mathOps.add(2, 3); // 5
stringOps.add(2, 3); // "23"
The Problem - Two functions with the same name? The second one overwrites the first!
Global Scope: โโ mathOps.add() โ numeric โโ stringOps.add() โ string concat Both "add" functions exist safely!
Namespacing - Group related functions under a module name. Each module has its own isolated scope.
// Module pattern for gameboard (only need one)
const Gameboard = (function() {
const board = ['', '', '', '', '', '', '', '', ''];
const getBoard = () => board;
const setMark = (index, mark) => {
board[index] = mark;
};
const reset = () => board.fill('');
return { getBoard, setMark, reset };
})();
// Factory for players (need multiple)
const createPlayer = (name, mark) => {
return { name, mark };
};
// Module pattern for game controller
const GameController = (function() {
// Game logic here
})();
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Gameboard (Module/IIFE) โ
โ โโ Single instance โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ GameController (Module) โ
โ โโ Single instance โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ createPlayer (Factory) โ
โ โโโบ Player 1 { name, mark }โ
โ โโโบ Player 2 { name, mark }โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
IIFE - One gameboard, one controller. Created immediately.
Factory - Need two players with different names/marks.
Gameboard module with array storagecreatePlayer factory functionGameController module for game flowWhy? Separating logic from presentation makes debugging much easier!
"Building a House from the Inside Out"
Great article that shows how to approach this project and organize your code structure. Highly applicable to Tic Tac Toe!
๐ Next Class: Classes
๐ Homework: Tic Tac Toe