CSCI 4513
Week 4, Lecture 8
Huginn (thought/behavior) and Muninn (memory/data) work together, just as methods and properties combine in OOP.
The Tale:
Every morning, Odin sends two ravens across the Nine Realms. Huginn ("thought") observes patterns and relationships. Muninn ("memory") records specific events and details. At evening, both return to whisper their findings.
Huginn shares patterns, behaviors, and logical connections. Muninn recounts specific details, exact words, precise times. Together they make Odin the most informed beingβneither alone would suffice. Huginn without Muninn has understanding but no facts; Muninn without Huginn has data but no comprehension.
// Muninn (Memory) - Data
const ravenReport = {
"date": "2025-01-15",
"realm": "Midgard",
"events": [
{ "time": "09:00", "location": "Village",
"event": "Market day" }
]
};
// Huginn (Thought) - Methods + Behavior
class Raven {
constructor(name) {
this.name = name;
this.observations = []; // Muninn
}
analyzePatterns() { // Huginn
return this.observations.map(
obs => this.interpret(obs));
}
predictOutcome(situation) { // Huginn
return this.applyLogic(situation);
}
}
Two halves of OOP - Data (properties) and behavior (methods) belong together in one object.
ravenReport - Pure data (JSON-like). No behavior β just stores facts.
class Raven - Combines data (observations) with behavior (analyzePatterns) in one structure.
constructor(name) - Runs when you create a new Raven. Sets up initial properties.
Like Odin's ravens: data alone (Muninn) or logic alone (Huginn) isn't enough β you need both!
Now that you've used Webpack, let's learn some tools to make setup easier and workflow more efficient!
// package.json
{
"scripts": {
"build": "webpack",
"dev": "webpack serve",
"deploy": "git subtree push --prefix dist origin gh-pages"
}
}
# Now use these shortcuts:
npm run build # Instead of: npx webpack
npm run dev # Instead of: npx webpack serve
npm run deploy # Instead of: git subtree push...
"scripts" - Key-value pairs: name β shell command to execute
npm run <name> - Runs the command mapped to that script name
Why? - Shorter to type, self-documenting, and standardized across projects.
Scripts can run any shell command β not just npm/webpack!
npm run build for production, npm run dev for development server
Try changing mode: "development" to mode: "production" and see the difference in dist!
// webpack.dev.js
module.exports = {
mode: "development",
devtool: "eval-source-map",
// ... dev config
};
// webpack.prod.js
module.exports = {
mode: "production",
// ... production config
};
// package.json
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack serve --config webpack.dev.js"
}
webpack.dev.js - Readable output, source maps, fast rebuilds
webpack.prod.js - Minified, optimized, smaller output for deployment
--config - Tells webpack which config file to use instead of the default
Now npm run dev and npm run build automatically pick the right config!
Create a repository with all your standard Webpack setup, mark it as a template, then use it as a starting point for new projects!
JSON is a standardized, text-based format for representing structured data based on JavaScript object syntax.
{
"name": "John Doe",
"age": 30,
"isStudent": false,
"courses": ["HTML", "CSS", "JavaScript"],
"address": {
"city": "New York",
"zip": "10001"
}
}
Allowed types: strings, numbers, booleans, arrays, objects, null
β
Keys must be in double quotes
β
Strings in double quotes only
β
No trailing commas
β No functions or undefined
JSON can nest objects inside objects and arrays inside arrays β any depth!
const obj = {
name: 'John', // No quotes on key!
greet() { // Functions OK
return 'Hi';
}
};
{
"name": "John" // Must quote!
// NO functions!
}
JS objects: unquoted keys, single quotes OK, functions allowed, lives in memory
JSON: double-quoted keys, double-quoted strings, data only, it's a string
β οΈ Key distinction: JSON is a string format for transmitting data. A JS object is a live data structure in memory.
// JSON string (maybe from an API)
const jsonString = '{"name":"John","age":30}';
// Parse it into a JavaScript object
const user = JSON.parse(jsonString);
console.log(user.name); // "John"
console.log(user.age); // 30
// Now we can use it like a normal object!
user.age = 31;
console.log(user.age); // 31
JSON.parse() - Converts a JSON string into a usable JavaScript object
Before parse: It's just a string β you can't access .name on it. After parse: It's a real object with properties.
Mutable - Once parsed, you can modify properties just like any object.
API responses arrive as JSON strings β always parse before using!
// JavaScript object
const user = {
name: "John",
age: 30,
courses: ["HTML", "CSS", "JS"]
};
// Convert to JSON string
const jsonString = JSON.stringify(user);
console.log(jsonString);
// '{"name":"John","age":30,
// "courses":["HTML","CSS","JS"]}'
// Ready to send to an API or
// save to localStorage!
JSON.stringify() - The reverse of parse: converts an object into a JSON string
Why stringify? - APIs and localStorage only accept strings. You must convert objects before sending or saving.
Functions are dropped! - stringify skips methods and undefined values.
const user = { name: "John", age: 30 };
// Compact (default)
console.log(JSON.stringify(user));
// {"name":"John","age":30}
// Pretty print with 2 spaces
console.log(JSON.stringify(user, null, 2));
// {
// "name": "John",
// "age": 30
// }
JSON.stringify(obj, replacer, spaces) takes three arguments:
1st: The object to convert
2nd: Replacer function (usually null)
3rd: Indentation spaces (2 or 4)
Pretty printing is great for debugging β much easier to read than a single line!
// β Trailing comma
{ "name": "John", }
// β Single quotes
{ 'name': 'John' }
// β Unquoted keys
{ name: "John" }
// β Functions
{ "greet": function() {} }
// β
Correct!
{ "name": "John" }
JSON is stricter than JS:
β No trailing commas after last item
β Must use "double quotes" only
β All keys must be quoted
β No functions, undefined, or comments
Common cause: Writing JSON like JavaScript. Remember β JSON is a data format, not code!
Use jsonlint.com to validate β it pinpoints the exact error location!
You've already used JSON in package.json!
We know HOW to create objects. Now let's learn WHEN and WHY to organize them certain ways.
A class/object/module should have one reason to change - it should only have one responsibility.
Bad Example: A function that checks game over AND manipulates the DOM
// β BAD: Does TOO MUCH
function isGameOver() {
// Check game over logic
if (gameOver) {
// ALSO manipulates DOM!
const div = document.createElement('div');
div.classList.add('game-over');
div.textContent = `${this.winner} won!`;
document.body.appendChild(div);
}
}
Two jobs in one function: checking game state AND building DOM elements.
Problems:
- Can't change UI without touching game logic
- Can't reuse game logic without the DOM code
- Harder to test either part independently
A function named "isGameOver" should only answer yes or no β not build HTML!
// β
GOOD: Separate concerns
// Game logic module - checks game state
function isGameOver() {
// Just return true/false
return gameOver;
}
// DOM module - handles UI
const DOMStuff = {
gameOver(winner) {
const div = document.createElement('div');
div.classList.add('game-over');
div.textContent = `${winner} won!`;
document.body.appendChild(div);
}
};
// Controller decides what to do
if (isGameOver()) {
DOMStuff.gameOver(winner);
}
isGameOver() - One job: return true/false. Knows nothing about the DOM.
DOMStuff - One job: render UI. Knows nothing about game rules.
Controller - Connects the pieces: checks state, then tells the DOM what to show.
Now you can swap the UI (console β DOM β React) without changing game logic!
Tightly coupled: Objects that depend so heavily on each other that changing one requires changing the other.
β οΈ Problem: Can't change the UI without rewriting game logic!
// Game logic - knows nothing about UI
class Game {
checkWinner() {
return this.winner;
}
}
// UI module - knows nothing about game internals
class UI {
displayWinner(winner) {
console.log(`${winner} wins!`);
}
// Could easily switch to DOM instead:
// displayWinner(winner) {
// const div = document.createElement('div');
// div.textContent = `${winner} wins!`;
// document.body.appendChild(div);
// }
}
// Controller connects them
const game = new Game();
const ui = new UI();
const winner = game.checkWinner();
ui.displayWinner(winner);
ββββββββ ββββββ
β Game βββ?βββΆβ UI β
ββββββββ β² ββββββ
β
ββββββββββββββ
β Controller β
ββββββββββββββ
Loosely coupled - Game and UI don't know about each other. The controller is the only connection point.
Swappable - Switch from console.log to DOM rendering by only changing the UI class β Game stays untouched.
class Animal {
move() {}
}
class Dog extends Animal {
// Dog IS-A Animal
bark() {}
}
const movement = {
move() {}
};
const dog = {
// Dog HAS-A movement
...movement,
bark() {}
};
extends - Inheritance: Dog gets everything from Animal's prototype chain
...movement - Composition: spread copies behaviors into the object directly
Composition wins - Mix multiple behaviors freely. No rigid hierarchy to maintain.
A flying fish can have both swimming and flying behaviors β no awkward class tree needed!
Example: A flying fish doesn't fit cleanly in a FishβBird hierarchy, but it can have swimming AND flying behaviors!
// Use factory function or class
class Todo {
constructor(title, description, dueDate, priority) {
this.title = title;
this.description = description;
this.dueDate = dueDate;
this.priority = priority; // 'low', 'medium', 'high'
this.notes = '';
this.checklist = [];
this.completed = false;
}
toggleComplete() {
this.completed = !this.completed;
}
}
constructor - Parameters set up each todo's initial state. Some properties have default values.
completed = false - Defaults: new todos start incomplete, with empty notes and checklist.
toggleComplete() - Flips boolean: !false β true, !true β false
Data (properties) + behavior (methods) together β that's OOP in action!
class Project {
constructor(name) {
this.name = name;
this.todos = [];
}
addTodo(todo) {
this.todos.push(todo);
}
removeTodo(todoId) {
this.todos = this.todos.filter(
t => t.id !== todoId);
}
}
// Default project on app start
const defaultProject = new Project("My Todos");
this.todos = [] - Each project holds its own array of Todo objects
addTodo / removeTodo - Methods to manage the collection. Logic stays inside the class.
filter() - Returns a new array with only items that pass the test (keeps non-matching IDs)
Create a default project on startup so users have somewhere to add todos immediately.
// Save todos to localStorage
function saveTodos(projects) {
const json = JSON.stringify(projects);
localStorage.setItem('todos', json);
}
// Load todos from localStorage
function loadTodos() {
const json = localStorage.getItem('todos');
if (!json) return []; // No data?
const projects = JSON.parse(json);
// Re-add methods to objects!
return projects.map(p =>
Object.assign(new Project(), p));
}
setItem(key, value) - Stores a string in the browser. Persists across page reloads.
getItem(key) - Retrieves the string. Returns null if nothing was stored.
Object.assign(new Project(), p) - Creates a real Project instance and copies parsed data into it, restoring methods!
β οΈ JSON loses methods! You must re-create class instances after parsing.
# Install date-fns
npm install date-fns
// Use date-fns for formatting
import { format, isToday, isPast } from 'date-fns';
const todo = {
dueDate: new Date(2025, 0, 15)
};
// Format date nicely
console.log(format(todo.dueDate, 'MMM dd, yyyy'));
// "Jan 15, 2025"
// Check if overdue
if (isPast(todo.dueDate)) {
console.log('Overdue!');
}
date-fns - A regular dependency (not --save-dev) because it runs in production
format(date, pattern) - 'MMM dd, yyyy' β "Jan 15, 2025". Tokens control the output.
isPast() / isToday() - Utility functions that return true/false for date comparisons
Import only what you need β Webpack tree-shakes the rest away!
Look at screenshots and videos for UI ideas - don't copy exactly, but get inspired!
π Next Class: JS in the Real World & Catch-up Lab
π Homework: To-do List