Lesson: Frequency Counters, Closure, Classes, and OOP in JavaScript
Part 1: Closure with Counter Function
Code Review
Let's start by reviewing this block of code:
const counterValue = document.getElementById("counterValue");
const incrementBtn = document.getElementById("incrementBtn");
const decrementBtn = document.getElementById("decrementBtn");
function createCounter() {
let count = 0;
return function (action) {
if (action === "increment") {
count++;
} else if (action === "decrement") {
count--;
}
return count;
};
}
const counter = createCounter();
incrementBtn.addEventListener("click", () => {
counterValue.textContent = counter("increment");
});
decrementBtn.addEventListener("click", () => {
counterValue.textContent = counter("decrement");
});Section 1: Variable Declarations
const counterValue = document.getElementById("counterValue");
const incrementBtn = document.getElementById("incrementBtn");
const decrementBtn = document.getElementById("decrementBtn");const counterValue = document.getElementById("counterValue");: This line accesses the HTML element with the IDcounterValue. It stores this reference in thecounterValueconstant.const incrementBtn = document.getElementById("incrementBtn");: Similarly, this line captures the button element with the IDincrementBtn.const decrementBtn = document.getElementById("decrementBtn");: This line captures the button element designated to decrement the counter.
Section 2: Creating the Counter Function
function createCounter() {
let count = 0;
return function (action) {
if (action === "increment") {
count++;
} else if (action === "decrement") {
count--;
}
return count;
};
}function createCounter() {: This function declaration begins the logic for a counter function.let count = 0;: InsidecreateCounter, aletvariable calledcountis initialized to 0.return function (action) {: A function is returned that takes an argumentaction.if (action === "increment") { count++; }: This checks if the action is "increment" and, if so, incrementscount.else if (action === "decrement") { count--; }: Alternatively, if the action is "decrement,"countis decreased.return count;: Finally, the updatedcountvalue is returned.
Section 3: Function Invocation and Event Listeners
const counter = createCounter();
incrementBtn.addEventListener("click", () => {
counterValue.textContent = counter("increment");
});
decrementBtn.addEventListener("click", () => {
counterValue.textContent = counter("decrement");
});const counter = createCounter();: InvokescreateCounterand stores the returned function incounter.incrementBtn.addEventListener("click", () => {: Adds a click event listener to the increment button.counterValue.textContent = counter("increment");: Invokescounterwith "increment" and updates the DOM element to show the new count.decrementBtn.addEventListener("click", () => {: Adds a click event listener to the decrement button.counterValue.textContent = counter("decrement");: Invokescounterwith "decrement" and updates the DOM to reflect this.
Section 4: getElementById vs querySelector
Explanation
document.getElementById: Direct and fast but only works with IDs.document.querySelector: More flexible, can use any CSS selector, but slightly slower.- Speed:
getElementByIdis generally faster. - Flexibility:
querySelectoris more flexible.
Key Takeaways
- Closure: The
createCounterfunction leverages closures. It encapsulates thecountvariable, providing controlled access to it. - Event Listeners:
addEventListenerattaches behavior (incrementanddecrement) to buttons. - DOM Manipulation:
document.getElementByIdis used for selecting elements to manipulate.
Part 2: Frequency Counter with "Is Anagrams" Challenge
Code Snippet
function isAnagram(str1, str2) {
if (str1.length !== str2.length) {
return false;
}
const counter1 = {};
const counter2 = {};
for (let char of str1) {
counter1[char] = (counter1[char] || 0) + 1;
}
for (let char of str2) {
counter2[char] = (counter2[char] || 0) + 1;
}
for (let key in counter1) {
if (counter1[key] !== counter2[key]) {
return false;
}
}
return true;
}Section 1: Function Declaration and Input Length Check
function isAnagram(str1, str2) {
if (str1.length !== str2.length) {
return false;
}function isAnagram(str1, str2) {: This function takes two strings as arguments to check if they are anagrams.if (str1.length !== str2.length) { return false; }: An immediate length check, if they're different lengths, they can't be anagrams.
Section 2: Counting the Frequency
const counter1 = {};
const counter2 = {};
for (let char of str1) {
counter1[char] = (counter1[char] || 0) + 1;
}
for (let char of str2) {
counter2[char] = (counter2[char] || 0) + 1;
}const counter1 = {};: Initializes an empty object to count characters forstr1.const counter2 = {};: Similarly, forstr2. 19-20. The twoforloops iterate through each string, counting occurrences of each character.
Section 3: Frequency Comparison
for (let key in counter1) {
if (counter1[key] !== counter2[key]) {
return false;
}
}
return true;
}for (let key in counter1) {: Iterates through the keys incounter1.if (counter1[key] !== counter2[key]) { return false; }: Compares each key's value incounter1andcounter2.return true;: If the function hasn't returned false by now, the strings are anagrams.
Refactored Version Using More Readable if Statements
In the refactored version, I've replaced the shorthand syntax with explicit if statements to make the logic more apparent for those getting tripped up with the shorter syntax using the || aka the or operator
const counter1 = {};
const counter2 = {};
for (let char of str1) {
if (counter1[char]) {
counter1[char] += 1;
} else {
counter1[char] = 1;
}
}
for (let char of str2) {
if (counter2[char]) {
counter2[char] += 1;
} else {
counter2[char] = 1;
}
}Comparison with Original Code
Original Code
counter1[char] = (counter1[char] || 0) + 1;Refactored Code
if (counter1[char]) {
counter1[char] += 1;
} else {
counter1[char] = 1;
}- Clarity: The refactored code explicitly checks whether a character already exists in the counter object. If so, it increments by one. Otherwise, it sets it to one. This makes the logic easier to follow, especially for those who are new to programming or not familiar with JavaScript shorthand.
- Length: The refactored code is longer, which might be seen as a downside if you're aiming for brevity. However, the increase in lines is justified by the enhanced readability.
- Performance: Both versions are equally efficient, but the original one-liner could be considered more "elegant" by those who favor concise code.
Key Takeaways
- Frequency Counter:
counter1andcounter2store the frequency of each character. - Comparison: After counting, it compares the frequency tables.
- DOM elements can be manipulated through JavaScript.
- Closures enable data encapsulation and local state.
- Frequency counters are a powerful tool for comparing data sets.
- Conditional logic and loops are core parts of these mechanisms.
- While shorthand expressions can be concise, they may sacrifice readability.
- Explicit
ifstatements make the code easier to understand at a glance. - The best approach often depends on the audience; if your audience values clarity, the refactored version is better suited.
Part 3: Object-Oriented Programming with Animals & Pokemon
Code Snippet
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
return 'Some generic sound';
}
}
class Dog extends Animal {
makeSound() {
return 'Woof!';
}
}
class Cat extends Animal {
makeSound() {
return 'Meow!';
}
}
class Bird extends Animal {
makeSound() {
return 'Tweet!';
}
}Pokémon Lab Precursor
Introduction
This advanced version of the Pokémon Lab dives into constructors, multiple methods, and the concept of prototypical inheritance in JavaScript. By the end, you should have a well-rounded understanding of these critical OOP components.
Constructors and Methods
Base Pokémon Class
We start with a Pokemon base class that has a constructor and three methods: attack, defend, and speak.
class Pokemon {
constructor(name, type) {
this.name = name;
this.type = type;
this.health = 100;
}
attack() {
return `${this.name} used a generic attack!`;
}
defend() {
this.health -= 10;
return `${this.name} now has ${this.health} health left.`;
}
speak() {
return `${this.name} says hello!`;
}
}Pokémon Subclasses
Now, we create subclasses for different Pokémon types: Water, Fire, and Grass. Each subclass has its own constructor and overrides the attack method.
class WaterPokemon extends Pokemon {
constructor(name) {
super(name, 'Water');
}
attack() {
return `${this.name} used Water Gun!`;
}
}
class FirePokemon extends Pokemon {
constructor(name) {
super(name, 'Fire');
}
attack() {
return `${this.name} used Ember!`;
}
}
class GrassPokemon extends Pokemon {
constructor(name) {
super(name, 'Grass');
}
attack() {
return `${this.name} used Vine Whip!`;
}
}Protypical Inheritance
In JavaScript, inheritance works through prototypes. When we use extends to create a subclass, JavaScript sets up a prototype chain. The subclass prototype points to the base class prototype, allowing the subclass to inherit properties and methods from the base class.
For instance, a WaterPokemon object will have access to both its own methods and those defined in the Pokemon class. This is because WaterPokemon.prototype.__proto__ will point to Pokemon.prototype.
Example
Let's create some instances and call their methods:
const squirtle = new WaterPokemon('Squirtle');
const charmander = new FirePokemon('Charmander');
console.log(squirtle.attack()); // Output: "Squirtle used Water Gun!"
console.log(charmander.defend()); // Output: "Charmander now has 90 health left."
console.log(charmander.speak()); // Output: "Charmander says hello!"Here, squirtle and charmander are instances of WaterPokemon and FirePokemon, respectively. They inherit properties and methods from the base Pokemon class due to prototypical inheritance, and can also use methods that are overridden in their own subclasses.
Key Takeaways
- Inheritance:
Dog,Cat, andBirdare subclasses ofAnimal. - Polymorphism: Each subclass overrides the
makeSoundmethod. - Constructors initialize the properties of an object.
- Subclasses can override base class methods.
- Prototypical inheritance allows objects to inherit properties and methods from their prototype chain.
- Methods can perform actions and update an object's internal state.
Remember, understanding these concepts isn't just about writing code; it's about writing efficient, maintainable, and scalable code. In the lab you will look deeper into OOP by practicing creating classes.