By Dipto Karmakar
JavaScript is a scripting language used in webpages to add functionality and interactivity. For a beginner coming from a different programming language, JavaScript is quite easy to understand. With a few tutorials, you should be able to get started with it right away.
However, there are a few common mistakes that many beginner programmers make. In this article, we’ll address nine common mistakes (or bad practices) and their solutions to help you become a better JS developer.
Confusing the assignment (=) and equality (==, ===) operators
Like its name implies, the assignment operator(=) is used to assign values to variables. Developers often confuse it with the equality operator.
Here's an example:
const name = "javascript";
if ((name = "nodejs")) {
console.log(name);
}
// output - nodejs
The name variable and ‘nodejs' string are not compared in this case. Instead, 'nodejs' is assigned to name and 'nodejs' is printed to the console.
In JavaScript, the double equal sign(==) and triple equal sign(===) are called comparison operators.
For the code above, this is the appropriate way of comparing values:
const name = "javascript";
if (name == "nodejs") {
console.log(name);
}
// no output
// OR
if (name === "nodejs") {
console.log(name);
}
// no output
The difference between these comparison operators is that the double equals performs a loose comparison while triple equals performs a strict comparison.
In a loose comparison, only the values are compared. But in a strict comparison, the values and datatype are compared.
The following code explains it better:
const number = "1";
console.log(number == 1);
// true
console.log(number === 1);
// false
The variable number was assigned a string value of 1. When compared with 1 (of number type) using double equals, it returns true because both values are 1.
But when compared using triple equals, it returns false because each value has a different data type.
Expecting callbacks to be synchronous
Callbacks are one way that JavaScript handles asynchronous operations. Promises and async/await, however, are preferable methods for handling asynchronous operations because multiple callbacks lead to callback hell.
Callbacks are not synchronous. They are used as a function to be called after an operation when a delayed execution completes.
An example is the global setTimeout
function which receives a callback function as its first argument and a duration (in ms) as a second argument like so:
function callback() {
console.log("I am the first");
}
setTimeout(callback, 300);
console.log("I am the last");
// output
// I am the last
// I am the first
After 300 milliseconds, the callback function is called. But before it completes, the rest of the code runs. This is the reason why the last console.log was run first.
A common mistake developers make is to misinterpret callbacks as synchronous. For example, a callback which returns a value that would be used for other operations.
Here's that mistake:
function addTwoNumbers() {
let firstNumber = 5;
let secondNumber;
setTimeout(function () {
secondNumber = 10;
}, 200);
console.log(firstNumber + secondNumber);
}
addTwoNumbers();
// NaN
NaN
is the output because secondNumber
is undefined. At the time of running firstNumber + secondNumber
, secondNumber
is still undefined because the setTimeout
function would execute the callback after 200ms
.
The best way to approach this is to execute the rest of the code in the callback function:
function addTwoNumbers() {
let firstNumber = 5;
let secondNumber;
setTimeout(function () {
secondNumber = 10;
console.log(firstNumber + secondNumber);
}, 200);
}
addTwoNumbers();
// 15
Wrong references to this
this
is a commonly misunderstood concept in JavaScript. To use this
in JavaScript, you really need to understand how it works because it operates a bit differently compared to other languages.
Here's an example of a common mistake when using this
:
const obj = {
name: "JavaScript",
printName: function () {
console.log(this.name);
},
printNameIn2Secs: function () {
setTimeout(function () {
console.log(this.name);
}, 2000);
},
};
obj.printName();
// JavaScript
obj.printNameIn2Secs();
// undefined
The first result is JavaScript
because this.name
correctly points to the object's name property. The second result is **undefined**
because this
has lost reference to the object's properties (including name).
This is because this
depends on the object calling the function which it lives in. There is a this
variable in every function but the object it points to is determined by the object calling it.
The this
in obj.printName()
points directly to obj
. The this
in obj.printNameIn2Secs
points directly to obj
. But the this
in the callback function of setTimeout
does not point to any object because no object called it.
For an object to have called setTimeout
, something like obj.setTimeout...
would be executed. Since there is no object calling that function, the default object (which is window
) is used.
name
does not exist on window, resulting in undefined
.
The best ways to go about retaining the reference to this
in setTimeout
is to use bind
, call
, apply
or arrow functions (introduced in ES6). Unlike normal functions, arrow functions do not create their own this
.
So, the following will retain its reference to this
:
const obj = {
name: "JavaScript",
printName: function () {
console.log(this.name);
},
printNameIn2Secs: function () {
setTimeout(() => {
console.log(this.name);
}, 2000);
},
};
obj.printName();
// JavaScript
obj.printNameIn2Secs();
// JavaScript
Disregarding object mutability
Unlike primitive data types like string, number and so on, in JavaScript objects are reference data types. For example, in key-value objects:
const obj1 = {
name: "JavaScript",
};
const obj2 = obj1;
obj2.name = "programming";
console.log(obj1.name);
// programming
obj1
and obj2
possess the same reference to the location in memory where the object is stored.
In arrays:
const arr1 = [2, 3, 4];
const arr2 = arr1;
arr2[0] = "javascript";
console.log(arr1);
// ['javascript', 3, 4]
A common mistake developers make is they disregard this nature of JavaScript and this results in unexpected errors. For instance, if 5 objects have the same reference to the same object, one of the object may interfere with the properties in a large-scale code base.
When this happens, any attempt to access the original properties would return undefined or possibly throw an error.
The best practice for this is to always create new references for new objects when you want to duplicate an object. To do this, the rest operator ( ...
introduced in ES6) is a perfect solution.
For example, in key-value objects:
const obj1 = {
name: "JavaScript",
};
const obj2 = { ...obj1 };
console.log(obj2);
// {name: 'JavaScript' }
obj2.name = "programming";
console.log(obj.name);
// 'JavaScript'
In arrays:
const arr1 = [2, 3, 4];
const arr2 = [...arr1];
console.log(arr2);
// [2,3,4]
arr2[0] = "javascript";
console.log(arr1);
// [2, 3, 4]
Saving arrays and objects to browser storage
Sometimes, while working with JavaScript, developers may want to take advantage of the localStorage
for saving values. But a common mistake is trying to save arrays and objects as-is in the localStorage
. localStorage
only accepts strings.
In an attempt to save objects, JavaScript converts the object to a string. The result is [Object Object]
for objects and a comma-separated string for array elements.
For example:
const obj = { name: "JavaScript" };
window.localStorage.setItem("test-object", obj);
console.log(window.localStorage.getItem("test-object"));
// [Object Object]
const arr = ["JavaScript", "programming", 45];
window.localStorage.setItem("test-array", arr);
console.log(window.localStorage.getItem("test-array"));
// JavaScript, programming, 45
When objects are saved like this, it becomes difficult to access them. For the object example, accessing the object like .name
would result in an error. This is because [Object Object]
is a string now, without a name
property.
A better way to save objects and arrays in local storage is by using JSON.stringify
(for converting objects to strings) and JSON.parse
(for converting strings to objects). This way, accessing the objects becomes easy.
The correct version of the code above would be:
const obj = { name: "JavaScript" };
window.localStorage.setItem("test-object", JSON.stringify(obj));
const objInStorage = window.localStorage.getItem("test-object");
console.log(JSON.parse(objInStorage));
// {name: 'JavaScript'}
const arr = ["JavaScript", "programming", 45];
window.localStorage.setItem("test-array", JSON.stringify(arr));
const arrInStorage = window.localStorage.getItem("test-array");
console.log(JSON.parse(arrInStorage));
// JavaScript, programming, 45
Not using default values
Setting default values in dynamic variables is a very good practice for preventing unexpected errors. Here's an example of a common mistake:
function addTwoNumbers(a, b) {
console.log(a + b);
}
addTwoNumbers();
// NaN
The result is NaN
because a
is undefined
and b
is undefined
. By using default values, errors like this can be prevented. For example:
function addTwoNumbers(a, b) {
if (!a) a = 0;
if (!b) b = 0;
console.log(a + b);
}
addTwoNumbers();
// 0
Alternatively, the default value feature introduced in ES6 can be used like so:
function addTwoNumbers(a = 0, b = 0) {
console.log(a + b);
}
addTwoNumbers();
// 0
This example, though minimal, emphasizes the importance of default values. Additionally, developers can provide errors or warning messages when expected values are not provided.
Improper naming of variables
Yes, developers still make this mistake. Naming is hard, but developers really have no choice. Comments are good practice in programming, and so is naming variables.
For example:
function total(discount, p) {
return p * discount
}
The variable discount
is okay, but what about p
or total
? Total of what? A better practice for above would be:
function totalPrice(discount, price) {
return discount * price
}
Properly naming variables is important because a developer may never be the only developer on a codebase at a particular time or in the future.
Naming variables properly will allow contributors easily understand how a project works.
Check-up for boolean values
const isRaining = false
if(isRaining) {
console.log('It is raining')
} else {
console.log('It is not raining')
}
// It is not raining
It is common practice to check boolean values as seen in the above code. While this is okay, errors set in when testing some values.
In JavaScript, a loose comparison of 0
and false
returns true
and 1
and true
returns true
. This means that if isRaining
was 1
, isRaining
would be true
.
This is also a mistake often made in objects. For example:
const obj = {
name: 'JavaScript',
number: 0
}
if(obj.number) {
console.log('number property exists')
} else {
console.log('number property does not exist')
}
// number property does not exist
Although the number
property exists, obj.number
returns 0
, which is a falsy
value, therefore the else
block is executed.
So unless you're sure of the range of values that would be used, boolean values and properties in objects should be tested like this:
if(a === false)...
if(object.hasOwnProperty(property))...
Confusing Addition and Concatenation
The plus sign (+)
has two functions in JavaScript: addition and concatenation. Addition is for numbers and Concatenation is for strings. Some developers often misuse this operator.
For example:
const num1 = 30;
const num2 = "20";
const num3 = 30;
const word1 = "Java"
const word2 = "Script"
console.log(num1 + num2);
// 3020
console.log(num1 + num3);
// 60
console.log(word1 + word2);
// JavaScript
When adding strings and numbers, JavaScript converts the numbers to strings, and concatenates all values. For addition of numbers, a mathematical operation is performed.
Conclusion
There are, of course, more mistakes (some trivial, some serious) than the ones listed above. So just make sure you stay up to date with developments in the language.
Studying and avoiding these mistakes will help you build better and more reliable web applications and tools.