This week, I found myself having difficulty understanding a seemingly simple object-oriented programming concept. So it all started when I was playing around with some JS and trying to count “lorem ipsum” paragraphs in my HTML document.
There were 8 lorem ipsum paragraphs in total in my document and some of them had the word ‘success’ in them and others had the word ‘error’ in them. I was practicing using the forEach()
method to iterate over the p tags and find the ones that were either success or errors. Whatever was left then had to be the “normal” lorem ipsums.
Here is my HTML document:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="icon" href="#">
<link rel="stylesheet" href="style.css">
</head>
<body>
<p>lorem error ipsum</p>
<p>lorem success ipsum</p>
<p>lorem ipsum lorem</p>
<p>lorem ipsum success</p>
<p>error lorem ipsum</p>
<p>lorem ipsum lorem</p>
<p>lorem ipsum error</p>
<p>success lorem ipsum</p>
<script src="sandbox.js"></script>
</body>
</html>
Here’s the actual code I wrote to count them:
const content = document.querySelectorAll('p');
let count = {
total: 0,
success: 0,
error: 0,
normal: count.total - (count.success + count.error);
};
console.log(count.total)
content.forEach(p => {
let str = p.textContent;
if (str.includes('success')) { count.success++;}
if (str.includes('error')) { count.error++;}
count.total++;
})
console.log(`There are ${count.total} lorem ipsums.`)
console.log(`${count.success} successes, ${count.error} errors.`)
console.log(`${count.normal} normals.`)
I wanted to store the results in an object, and have each of its properties keep a count of the amount of times a success or error was found. This worked flawlessly because the numbers just increment as the forEach()
iterates. However, for the ‘normal’ counter, I wanted to try something a little different. I wanted to use an expression to calculate how many were left after counting the success/error ones. Simple, right? Just take the total amount and subtract from the combined amount that were counted to be either success or false.
You can already tell that I’ve made a big mistake in the code above (if you’re familiar at all with JS). In the count
object, I’m trying to directly access the properties of the object inside of that same object. Now, why doesn’t this work? At the time, I had no clue.
In my head, the properties that I was trying to access had already been defined. They were given starting values of ‘0’ and I assumed they were already initialized since they came before my calculation even occurred. That assumption was wrong, of course.
As someone who will refuse to move on to the next thing until understanding the thing in front of him, I had to go searching for answers immediately. I took off to Google, Reddit, StackOverflow, and even ChatGPT for answers. All in an attempt to learn why my code wasn’t behaving the way I thought it would.
Now, after all that time spent researching and reading through different explanations, I think I have a better understanding of why I can’t directly access properties in an object literal, at least not the way I wanted to.
The Properties Don’t Exist Yet
We can’t access an object’s properties because they don’t exist yet – at least not in a form in which we can access them.
To understand this better, consider something simpler like:
let x = 1 + 2
We don’t know what x
will be until you evaluate 1, then evaluate 2, then evaluate the result of the + operator used on both of those values. After all of that is complete, x
gets assigned the final value of all that work, 3.
If we were to try to refer to x
in its own definition, we’d get an error.
let x = 1 + 2 + x // Error
Since x
hasn’t been defined yet, given everything that is needed to make up its value needs to be evaluated first, x
can’t use itself to define itself. To access x
, you need to know what x
is.
Something similar happens with object literals
let obj = {
x: 1,
y: obj.x, // Error
}
Here, obj
can’t be defined until the object defining it is completely evaluated. And since that literal contains a reference to obj
, which at that point in time hasn’t been defined, an error is thrown because there’s nothing in obj
that can be referenced to get a value from.
As one redditor commented,
Think of it like trying to bite your own teeth.
— shuckster
To put it simply, an object isn’t defined until the compiler reaches that closing }
tag. This isn’t just a JavaScript thing. Other languages also behave this way. So, some things are only available after the block is closed and the object is initialized.
Inside the object, the object itself hasn’t been created yet. So until it is created, the things inside of it are still in the process of being defined (they’re being defined at that point in time). If you were to console log obj
at that point in the code, you’ll see that it automatically gets set to undefined
by JavaScript. This is because at that point, it is still, in fact, not entirely defined. Not until JS finishes evaluating everything inside of it first (and reaches that closing bracket).
When I finally got this answer, I was so happy. Not only because I finally knew why my code didn’t work as intended, but because I could finally move on to the next thing.
Using a Function
When a function is added inside of an object literal and references that object’s properties, we might expect it to also have this issue. However, we’d be wrong. It actually works just fine.
let count = {
total: 0,
success: 0,
error: 0,
normal: () => {
return count.total - (count.success + count.error);
}
};
In the code above, the ‘normal’ counter has been turned into a function, and it can directly access the count
object’s properties for its calculation. It doesn’t matter that it is still inside the object being defined. It works.
It can then be called up later, such as this:
console.log(`${count.normal()} normals.`)
The reason that this works is due to something called deferred execution.
The function’s body is not executed until the function is invoked. This means that the expression inside the function is not evaluated during the object’s initialization. It remains as a piece of code to be executed later.
By using a function, you effectively postpone the evaluation of your code until it is actually needed. This ensures that the object has been fully initialized (created) before any expressions are evaluated, avoiding the issues we mentioned before relating to referencing properties during initialization.
There’s actually a specific type of function that can be used for this very reason. It is called a get function. You can read about it in the MDN docs here.
The main advantage of using a get function over a regular function is that it allows you to access the property as if it were a regular property of the object, using dot notation or square bracket notation.
let count = {
total: 0,
success: 0,
error: 0,
get normal() { // this is a getter function
return count.total - (count.success + count.error);
}
};
console.log(`${count.normal} normals.`) // object dot notation
This can make your code cleaner and more intuitive to read, especially when working with complex objects. Getter functions are automatically invoked when you access the property, so you don’t need to explicitly call them like you would with a regular function.
Conclusion
Calling properties directly inside of an object while it’s being created is very much akin to trying to build the roof of a house before the walls have even been constructed yet.
Hope this article has helped anyone who is stuck with the same problem or had the same questions I had.
Thank you for reading!
Last Updated on October 15, 2024
Leave a Reply