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 thecounterValue
constant.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
, alet
variable calledcount
is 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,"count
is decreased.return count;
: Finally, the updatedcount
value 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();
: InvokescreateCounter
and stores the returned function incounter
.incrementBtn.addEventListener("click", () => {
: Adds a click event listener to the increment button.counterValue.textContent = counter("increment");
: Invokescounter
with "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");
: Invokescounter
with "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:
getElementById
is generally faster. - Flexibility:
querySelector
is more flexible.
Key Takeaways
- Closure: The
createCounter
function leverages closures. It encapsulates thecount
variable, providing controlled access to it. - Event Listeners:
addEventListener
attaches behavior (increment
anddecrement
) to buttons. - DOM Manipulation:
document.getElementById
is 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 twofor
loops 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 incounter1
andcounter2
.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:
counter1
andcounter2
store 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
if
statements 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
, andBird
are subclasses ofAnimal
. - Polymorphism: Each subclass overrides the
makeSound
method. - 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.