
15 JavaScript Tips
That every web developer should master
Hey Devs! How's it going? If you're like me, you've probably spent hours staring at JavaScript code trying to figure out why on earth that function isn't working as expected. Or maybe you've screamed at your monitor "WHY ARE YOU UNDEFINED?!". Welcome to the club!
JavaScript is like a friend full of personality: sometimes it surprises us positively, other times it leaves us completely confused. But, with a few tips and tricks up your sleeve, you can become best friends. Let's get to it!
1. Understand (Truly) Variable Scope
Scope in JavaScript can be tricky. Many people still stumble over the difference between var
, let
, and const
.
The WRONG Way:
function badExample() {
// Using var which has function scope
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // What do you think it will print?
}, 100);
}
}
badExample(); // Prints: 5, 5, 5, 5, 5
Surprise! Expected 0, 1, 2, 3, 4? Well, var
has function scope, not block scope. When setTimeout executes, the loop has already finished, and i
is 5.
The RIGHT Way:
function goodExample() {
// Using let which has block scope
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // Now it works!
}, 100);
}
}
goodExample(); // Prints: 0, 1, 2, 3, 4
With let
, each loop iteration creates a new binding for i
. Wonderful!
2. Stop Abusing this
this
in JavaScript is like a GPS that changes its mind depending on the street you're on. Many people waste hours of their lives trying to figure out where it's pointing.
The WRONG Way:
const object = {
value: 42,
showLater: function() {
setTimeout(function() {
console.log(this.value); // What is "this" here?
}, 1000);
}
};
object.showLater(); // Prints: undefined
Huh, where's the 42? The problem is that inside the setTimeout callback function, this
is no longer the object, but rather the global object (or undefined in strict mode).
The RIGHT Way:
const object = {
value: 42,
showLater: function() {
// Option 1: Arrow function
setTimeout(() => {
console.log(this.value); // "this" is captured from the outer context
}, 1000);
// Option 2: Capture "this" in a variable
const self = this;
setTimeout(function() {
console.log(self.value);
}, 1000);
// Option 3: Use bind
setTimeout(function() {
console.log(this.value);
}.bind(this), 1000);
}
};
object.showLater(); // Prints: 42
Personally, I prefer arrow functions. They are more elegant and eliminate the need to constantly try to "hold onto" this
in variables.
3. Learn to Love Modern Array Methods
Traditional for
loops are like your high school friend: you have history together, but there are much more interesting people to meet.
The WRONG Way:
const numbers = [1, 2, 3, 4, 5];
const evens = [];
// Traditional loop to filter even numbers
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
evens.push(numbers[i]);
}
}
// Traditional loop to double values
const doubled = [];
for (let i = 0; i < evens.length; i++) {
doubled.push(evens[i] * 2);
}
console.log(doubled); // [4, 8]
Does it work? Yes. Is it elegant? Not really.
The RIGHT Way:
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers
.filter(num => num % 2 === 0) // Filter evens
.map(num => num * 2); // Double values
console.log(doubled); // [4, 8]
Much cleaner and more expressive! Modern methods like map
, filter
, reduce
, find
, and forEach
are like superpowers for anyone working with arrays. Plus, it's much easier to read and understand what the code is doing.
4. Destructuring: Your Best Friend for Objects and Arrays
If you're still accessing object properties and array elements the old-fashioned way, it's time to meet destructuring.
The WRONG Way:
function showUser(user) {
const name = user.name;
const email = user.email;
const age = user.age;
console.log('Name: ' + name);
console.log('Email: ' + email);
console.log('Age: ' + age);
}
const user = {
name: 'João',
email: 'joao@exemplo.com',
age: 30
};
showUser(user);
The RIGHT Way:
function showUser({ name, email, age }) {
console.log(`Name: ${name}`); // Template strings are also amazing!
console.log(`Email: ${email}`);
console.log(`Age: ${age}`);
}
const user = {
name: 'João',
email: 'joao@exemplo.com',
age: 30
};
showUser(user);
// Bonus: array destructuring
const rgb = [255, 128, 0];
const [red, green, blue] = rgb;
console.log(`R: ${red}, G: ${green}, B: ${blue}`);
Destructuring makes your code cleaner and less verbose. It's like that multi-tool you didn't know you needed until you started using it.
5. Promises and async/await: Goodbye Callback Hell!
If you're still nesting callbacks like you're building a Russian doll, it's time for a change!
The WRONG Way (Callback Hell):
function searchData() {
searchUser(function (user) {
searchOrders(user.id, function (orders) {
searchDetails(orders[0].id, function (details) {
showResult(user, orders, details, function () {
console.log('I finally finished!');
// And what if we need to add another layer?
// 😱
});
});
});
});
}
Ouch, that hurt my eyes! This pattern is so infamous it earned the nickname "callback hell" or "pyramid of doom".
The RIGHT Way with Promises:
function searchData() {
return fetchUser()
.then(user => {
return fetchOrders(user.id)
.then(orders => {
return fetchDetails(orders[0].id)
.then(details => {
return showResult(user, orders, details);
});
});
})
.then(() => console.log('I finally finished!'))
.catch(error => console.error('Oops, something went wrong:', error));
}
Better, but we still have nesting. Let's improve it further!
The EVEN BETTER Way with async/await:
async function fetchData() {
try {
const user = await searchUser();
const orders = await searchOrders(user.id);
const details = await searchDetails(orders[0].id);
await displayResult(user, orders, details);
console.log('I finally finished!');
} catch (error) {
console.error('Oops, something went wrong:', error);
}
}
Now that's more like it! The code becomes almost as linear as normal synchronous code, but retains all the advantages of asynchronicity.
6. Use Logical Operators Intelligently
Logical operators in JavaScript are much more powerful than they first appear.
The WRONG Way:
function greet(user) {
let message;
if (user && user.name) {
message = 'Hello, ' + user.name;
} else {
message = 'Hello, visitor';
}
return message;
}
The RIGHT Way:
function greet(user) {
return `Hello, ${user?.name || 'visitor'}`;
}
The optional chaining operator ?.
(introduced in ES2020) checks if user
exists before trying to access the name
property. Combined with the ||
operator to provide a default value, we have a quite elegant line!
7. Use Modules to Organize Your Code
Having all your code in a single giant file is like having your entire wardrobe piled into one drawer. It works, but it's chaotic!
The WRONG Way:
// giant-file.js with 1000+ lines of code
function validateEmail(email) { /* ... */ }
function validatePassword(password) { /* ... */ }
function formatDate(date) { /* ... */ }
function calculateTotal(items) { /* ... */ }
// ... 100 more functions ...
// Using the functions
const validEmail = validateEmail('user@example.com');
The RIGHT Way:
// validation.js
export function validateEmail(email) { /* ... */ }
export function validatePassword(password) { /* ... */ }
// formatting.js
export function formatDate(date) { /* ... */ }
// calculator.js
export function calculateTotal(items) { /* ... */ }
// main.js
import { validateEmail } from './validation.js';
import { formatDate } from './formatting.js';
const validEmail = validateEmail('user@example.com');
const formattedDate = formatDate(new Date());
Besides making the code more organized, modules offer encapsulation and avoid polluting the global scope.
8. Handle Numbers Correctly
JavaScript has some... peculiarities when it comes to numbers that can catch you off guard.
The WRONG Way:
const result = 0.1 + 0.2;
console.log(result === 0.3); // false (!)
console.log(result); // 0.30000000000000004
// Another common problem:
console.log(1000000000000000 === 1000000000000001); // true (!) - Precision issues with large numbers
The RIGHT Way:
// For comparisons
function approximatelyEqual(a, b, epsilon = 0.0001) {
return Math.abs(a - b) < epsilon;
}
console.log(approximatelyEqual(0.1 + 0.2, 0.3)); // true
// For financial calculations, use a library or specific techniques
function addMoney(a, b) {
return (a * 100 + b * 100) / 100;
}
console.log(addMoney(0.1, 0.2)); // 0.3
In situations where you need high precision, consider using libraries like decimal.js
or big.js
.
9. Closure: The Secret Technique of the Grand Masters
Closure is a powerful concept that allows encapsulating data and creating functions with "memory".
The WRONG Way:
let counter = 0;
function increment() {
counter++;
return counter;
}
// The problem? Any code can modify counter
counter = 100; // Oops, someone messed with our variable!
console.log(increment()); // 101
The RIGHT Way:
function createCounter() {
let counter = 0;
return function () {
counter++;
return counter;
};
}
const increment = createCounter();
const increment2 = createCounter(); // A separate counter
console.log(increment()); // 1
console.log(increment()); // 2
console.log(increment2()); // 1 (independent)
// No one can access or modify the counter variable directly!
Closures are like small data capsules that can only be accessed by specific functions. This allows creating private variables and maintaining state between function calls.
10. Know How to Handle Events and the DOM
Manipulating the DOM is one of the most common tasks for web developers, but also one of the most prone to problems.
The WRONG Way:
// Adding events directly in HTML elements (in HTML)
<button onclick="myFunction()">Click</button>
// Or adding with addEventListener repeatedly
document.querySelectorAll('.button').forEach(button => {
button.addEventListener('click', function () {
// Duplicate code for each button
alert('Button clicked!');
});
});
The RIGHT Way:
// Event delegation - More efficient!
document.getElementById('buttons-container').addEventListener('click', function (e) {
// Check if the click was on a button
if (e.target.matches('.button')) {
alert('Button clicked!');
}
});
// Even better, separate HTML from JavaScript
// HTML: <div id="myButton" class="button">Click</div>
// JavaScript:
document.getElementById('myButton').addEventListener('click', myFunction);
function myFunction(e) {
// Event handler code
}
Event delegation allows adding just one listener for many elements, which is more efficient and easier to maintain.
11. Use fetch
for HTTP Requests
If you're still using XMLHttpRequest
for your AJAX requests, it's time to meet fetch
.
The WRONG Way:
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
} else {
console.error('Error:', xhr.status);
}
}
};
xhr.send();
The RIGHT Way:
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Request Error:', error);
});
// Or with async/await
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Request Error:', error);
}
}
fetch
is simpler, more elegant, and already returns a Promise, integrating perfectly with async/await.
12. Understand "Hoisting"
Hoisting is a JavaScript behavior where declarations are "lifted" to the top of their scope.
The WRONG Way:
console.log(x); // undefined (instead of error)
var x = 5;
// Functions declared with the function keyword are fully hoisted
hello(); // This works!
function hello() {
console.log("Hello");
}
// But function expressions are not
bye(); // Error: bye is not a function
var bye = function () {
console.log("Bye");
};
The RIGHT Way:
// Always declare variables at the top of the scope
var x;
console.log(x); // undefined, but at least it's clear
x = 5;
// Even better, use let and const which don't have full hoisting
// console.log(y); // Error: Cannot access 'y' before initialization
let y = 10;
// For functions, maintain consistency
// Either all declared at the top, or all as function expressions
function hello() {
console.log("Hello");
}
const bye = function () {
console.log("Bye");
};
hello();
bye();
Understanding hoisting will prevent many mysterious bugs in your code.
13. Use DevTools for Debugging
Using console.log
for debugging is like trying to fix a watch with a hammer. It works, but it's not the ideal tool.
The WRONG Way:
function calculate() {
const a = getValueA();
console.log('a:', a);
const b = getValueB();
console.log('b:', b);
const result = a + b;
console.log('result:', result);
return result;
}
The RIGHT Way:
function calculate() {
// Place a breakpoint here in DevTools
const a = getValueA();
const b = getValueB();
const result = a + b;
// Or use the debugger statement
debugger; // The browser will stop here when the developer tools are open
return result;
}
Modern developer tools allow you to:
- Inspect values at any point of execution
- View the call stack
- Monitor expressions
- Set watchpoints to detect when a property changes
- Time-travel debugging (in some browsers)
14. Use Spread and Rest Operators
The spread (...
) and rest operators are powerful tools that can significantly simplify your code.
The WRONG Way:
// Copy arrays
const original = [1, 2, 3];
const copy = [];
for (let i = 0; i < original.length; i++) {
copy.push(original[i]);
}
// Combine objects
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const combined = {};
Object.keys(obj1).forEach(key => {
combined[key] = obj1[key];
});
Object.keys(obj2).forEach(key => {
combined[key] = obj2[key];
});
// Functions with variable number of arguments
function sum() {
let result = 0;
for (let i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
}
The RIGHT Way:
// Copy arrays
const original = [1, 2, 3];
const copy = [...original];
// Combine arrays
const array1 = [1, 2];
const array2 = [3, 4];
const combined = [...array1, ...array2]; // [1, 2, 3, 4]
// Combine objects
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const combined = { ...obj1, ...obj2 }; // { a: 1, b: 2, c: 3, d: 4 }
// Replace specific properties
const user = { name: 'João', age: 25 };
const updatedUser = { ...user, age: 26 }; // { name: 'João', age: 26 }
// Rest parameter for functions with variable number of arguments
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
These operators make the code much cleaner and more expressive, especially when working with arrays and objects.
15. Learn to Use Map
, Set
, WeakMap
, and WeakSet
Modern JavaScript collection types offer powerful features that simple objects and arrays lack.
The WRONG Way:
// Using objects as dictionaries
const users = {};
users['joao@email.com'] = { name: 'João' };
users['maria@email.com'] = { name: 'Maria' };
// The problem? Collisions with Object.prototype properties
users['toString']; // Oops, this is a function!
// Check for unique elements
function uniqueElements(array) {
const result = [];
for (let i = 0; i < array.length; i++) {
if (result.indexOf(array[i]) === -1) {
result.push(array[i]);
}
}
return result;
}
The RIGHT Way:
// Map for dictionaries
const users = new Map();
users.set('joao@email.com', { name: 'João' });
users.set('maria@email.com', { name: 'Maria' });
console.log(users.get('joao@email.com')); // { name: 'João' }
console.log(users.has('pedro@email.com')); // false
// Set for collections of unique values
function uniqueElements(array) {
return [...new Set(array)];
}
console.log(uniqueElements([1, 2, 2, 3, 1, 4])); // [1, 2, 3, 4]
// WeakMap and WeakSet are useful when you need to associate data with objects
// without preventing these objects from being garbage collected
const calculationsCache = new WeakMap();
function calculateExpensiveResult(obj) {
if (calculationsCache.has(obj)) {
return calculationsCache.get(obj);
}
// Simulating an expensive calculation
const result = obj.a + obj.b;
calculationsCache.set(obj, result);
return result;
}
These structures offer better performance for certain operations and avoid common problems associated with regular objects and arrays.
JavaScript is an incredible and powerful language, but like any tool, it needs to be used correctly to show its full potential. The tips above are just the tip of the iceberg, but mastering them will already put you ahead of many programmers.
Remember: clear and readable code is just as important as code that works. As the saying goes, code is written once, but read dozens of times. So, do yourself (and your colleagues) a favor and adopt good practices!
And if you ever find yourself screaming at the monitor about undefined variables, remember: you're not alone. We're all in this JavaScript journey together, stumbling over unresolved promises and undefined properties, but always learning something new with each mistake.
Happy coding!