JavaScript is a language that’s friendlier than many other programming languages in the world. However, it’s still very easy to make mistakes when writing JavaScript code through misunderstanding or overlooking stuff that we already know. By avoiding some of the mistakes below, we can make our lives easier by preventing bugs and typos in our code that bog us down with unexpected results. In this article, we’ll look mistaking object keys as array indexes, not understanding asynchronous code, misusing event listeners.
Confusing Object Keys and Array Indexes
In JavaScript, the brackets can be used to accept an object which can take a string containing a property name and then get the value from it. For example, if we have the following object:
const obj = {
foo: 1,
bar: 2
}
Then we can access the value of the foo
property by writing either:
obj.foo
or we can use the bracket notation and write:
obj['foo']
The bracket notation is also used to access an array entry by its index. For example, if we have:
const arr = [1, 2, 3];
Then we write:
arr[0]
to get the first entry of the arr
array.
We have to be careful in that we don’t confuse normal objects and arrays since the values of them can be accessed with the bracket notation, and the JavaScript interpreter doesn’t distinguish between the 2. This means that code like:
arr['foo']
are still allowed in JavaScript. It doesn’t stop us from commit the mistake of accessing an array-like we do with a regular object. If we log the value of arr[‘foo’]
we just get undefined
.
If we don’t know whether a variable is a regular object, an array or something, else, then we can use the Array.isArray
method which checks if a value that we pass in if an array. It takes one argument which is any objects that we want to check to see whether if it’s an array.
By using the Array.isArray
method, we’ll be sure that we’re accessing an array if Array.isArray
returns true
. For example, we can use it as the following code:
const arr = [1, 2, 3]
if (Array.isArray(arr)) {
console.log(arr[0]);
}
This way, we’re sure that arr
is an array before trying to access an array entry.
Not Understanding Asynchronous Code
Because of the single thread nature of JavaScript, lots of code is going to be asynchronous since running code line by line all the time is going to hang the computer if it’s waiting for something that takes some time such as a response from an HTTP request.
With asynchronous code, we often have run code that’s in a callback function, which runs in an indeterminate amount of time. They can’t be treated like synchronous code that runs line by line.
For example, if we want to get some data from a GET request, we often have to use an HTTP client that makes the request asynchronously and the response is obtained in an indeterminate amount of time.
If we use the Fetch API built into most modern browsers, we’ll get the response data asynchronously. For example, we’ll have code that looks something like the following:
fetch('[https://jsonplaceholder.typicode.com/todos/1'](https://jsonplaceholder.typicode.com/todos/1%27))
.then((response) => {
return response.json()
})
.then((responseBody) => {
console.log(responseBody);
})
As we can see we have multiple callback functions that do various things. If we write something like:
const response = fetch('[https://jsonplaceholder.typicode.com/todos/1'](https://jsonplaceholder.typicode.com/todos/1%27))
const responseBody = response.json()
console.log(responseBody);
It’s not going to work since the correct code doesn’t run code line by line. In the correct code, the fetch
function is called, which returns a promise, then inside the callback function in the first then
method, we return the result of theresponse.json()
object, which returns another promise which resolves to the JSON body of the response. Then in the callback function of the second then
method, we log the responseBody
which is obtained by resolving the promise returned by response.json()
call.
Promises may be run sequentially by chaining them, but they don’t run instantaneously and so they don’t run like synchronous code.
A shorthand that looks more like synchonous code would be using the async
and await
keyword introduced with ES2017. We can write:
fetch('[https://jsonplaceholder.typicode.com/todos/1'](https://jsonplaceholder.typicode.com/todos/1%27))
.then((response) => {
return response.json()
})
.then((responseBody) => {
console.log(responseBody);
})
to:
(async () => {
const response = await fetch('[https://jsonplaceholder.typicode.com/todos/1'](https://jsonplaceholder.typicode.com/todos/1%27));
const responseBody = await response.json();
console.log(responseBody);
})();
An async
function looks like it’s running line by line, but it’s just a shorthand for the then
method calls with callbacks passed inside. We can’t return anything other than a promise inside an async
function.
Adding Too Many Event Listeners
If we have the following HTML code:
<input type="checkbox" />
<button>Click</button>
and the corresponding JavaScript code:
const checkbox = document.querySelector('input[type=checkbox]');
const button = document.querySelector('button');
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
button.addEventListener('click', () => {
alert('Alert');
});
}
});
Then we’ll see that if we toggle the checkbox on and off a few times and then click the button, we’ll see the ‘Alert’ pop up multiple times. This is because we used the addEventListener
method on the button
object, which represents the button element that we have in the HTML. The addEventListener
method attaches a new click event listener each time the checkbox is checked. This means that we have multiple event listeners listening to the click event of the button. When the click event is finally fired by clicking the button, all the click event listener functions that we attached to the button all run, causing the ‘Alert’ pop up to appear multiple times.
This means we attached too many event listeners to the button.
The right way to do this is to set the onclick
property of the button
with the same event listener function instead. Likewise, we can also do the same thing for the checkbox
object even though we don’t have the same problem for consistency. We can instead write:
const checkbox = document.querySelector('input[type=checkbox]');
const button = document.querySelector('button');
checkbox.onchange = () => {
if (checkbox.checked) {
button.onclick = () => {
alert('Alert');
};
}
};
This way, we keep overwriting the onclick
event listener assigned to the button.onclick
property instead of keep attach new click event listeners to it.
In JavaScript, the brackets can be used to accept an object which can take a string containing a property name and then get the value from it. With arrays, we can use the bracket notation to access an array element by its index. There’s no way to distinguish whether an object with an array just by looking at the code. Therefore, we should check whether an object is actually an array before trying to access items by its index.
Lots of code in JavaScript are asynchronous since JavaScript is single-threaded, so having all the code run line by line will stall the browser. This means that we have to use asynchronous code to avoid stalling the computer by letting code that doesn’t return a result immediately wait in the background. We have to be aware that code are in callback functions of asynchronous functions are there because they can’t live outside the callback function.
Finally, we shouldn’t use addEventListener
too often since it keeps adding new event listeners to a DOM object without discarding the old ones. Instead, we can use the set the event listener function to the property of the DOM object which listens to the event we want to listen to. For example, if we want to listen to the click event, then we can set the onclick
property of a DOM object with the event handler function that we defined.