Key Takeaways
- Understanding the core workings of JavaScript is more valuable than familiarizing with the latest frameworks and environments, as these change rapidly. The basics of the language are the foundation for any complex application.
- Modern JavaScript introduced two new keywords, ‘let’ and ‘const’, which replace ‘var’ for declaring variables. However, they do not behave exactly the same way as ‘var’ and have their own idiosyncrasies, such as their scope being limited to the local block where they’re declared.
- The ‘const’ keyword in JavaScript is not completely constant. While it can’t be declared without a value or set to a new value after it’s been declared, an object or an array declared as a ‘const’ can have its content altered.
- Modern JavaScript has split the behavior of the traditional function into arrow functions and classes. Arrow functions allow for a more concise syntax and automatically return the result of the statement executed. Classes in JavaScript require forward declaration and need to be declared in the script before they are instantiated.
- The best practice in modern JavaScript is understanding what the language is actually doing. It’s crucial to consider what the code is trying to accomplish, where it needs to be deployed, and who will be modifying it next, before deciding on the best approach.
Something Solid to Cling To
I don’t know anybody who isn’t confused at the state of JavaScript these days, whether you’re new to JavaScript, or you’ve been coding with it for a while. So many new frameworks, so many changes to the language, and so many contexts to consider. It’s a wonder that anybody gets any work done, with all of the new things that we have to learn every month. I believe that the secret to success with any programming language, no matter how complex the application, is getting back to the basics. If you want to understand Rails, start by working on your Ruby skills, and if you want to use immutables and unidirectional data flow in isomorphic React with webpack (or whatever the cool nerds are doing these days) start by knowing your core JavaScript. Understanding how the language itself works is much more practical than familiarizing yourself with the latest frameworks and environments. Those change faster than the weather. And with JavaScript, we have a long history of thoughtful information online about how JavaScript was created and how to use it effectively. The problem is that some of the new techniques that have come around with the latest versions of JavaScript make some of the old rules obsolete. But not all of them! Sometimes a new syntax may replace a clunkier one to accomplish the same task. Other times the new approach may seem like a simpler drop-in replacement for the way we used to do things, but there are subtle differences, and it’s important to be aware of what those are.A Spoonful of Syntactic Sugar
A lot of the changes in JavaScript in recent years have been described as syntactic sugar for existing syntax. In many cases, the syntactic sugar can help the medicine go down for Java programmers learning how to work with JavaScript, or for the rest of us we just want a cleaner, simpler way to accomplish something we already knew how to do. Other changes seem to introduce magical new capabilities. But if you try to use modern syntax to recreate a familiar old technique, or stick it in without understanding how it actually behaves, you run the risk of:- having to debug code that worked perfectly before
- introducing subtle mistakes that may catch you at runtime
- creating code that fails silently when you least expect it.
When Your const
Isn’t Consistent
Modern JavaScript introduced two new keywords, let
and const
, which effectively replace the need for var
when declaring variables in most cases. But they don’t behave exactly the same way that var
does.
In traditional JavaScript, it was always a clean coding practice to declare your variables with the var
keyword before using them. Failure to do that meant that the variables you declared could be accessed in the global scope by any scripts that happened to run in the same context. And because traditional JavaScript was frequently run on webpages where multiple scripts might be loaded simultaneously, that meant that it was possible for variables declared in one script to leak into another.
The cleanest drop-in replacement for var
in modern JavaScript is let
. But let
has a few idiosyncrasies that distinguish it from var
. Variable declarations with var
were always hoisted to the top of their containing scope by default, regardless of where they were placed inside of that scope. That meant that even a deeply nested variable could be considered declared and available right from the beginning of its containing scope. The same is not true of let
or const
.
console.log(usingVar); // undefined
var usingVar = "defined";
console.log(usingVar); // "defined"
console.log(usingLet); // error
let usingLet = "defined"; // never gets executed
console.log(usingLet); // never gets executed
When you declare a variable using let
or const
, the scope for that variable is limited to the local block where it’s declared. A block in JavaScript is distinguished by a set of curly braces {}
, such as the body of a function or the executable code within a loop.
This is a great convenience for block-scoped uses of variables such as iterators and loops. Previously, variables declared within loops would be available to the containing scope, leading to potential confusion when multiple counters might use the same variable name. However let
can catch you by surprise if you expect your variable declared somewhere inside of one block of your script to be available elsewhere.
for (var count = 0; count < 5; count++) {
console.log(count);
} // outputs the numbers 0 - 4 to the console
console.log(count); // 5
for (let otherCount = 0; otherCount < 5; otherCount++) {
console.log(otherCount);
} // outputs the numbers 0 - 4 to the console
console.log(otherCount); // error, otherCount is undefined
The other alternative declaration is const
, which is supposed to represent a constant. But it’s not completely constant.
A const
can’t be declared without a value, unlike a var
or let
variable.
var x; // valid
let y; //valid
const z; // error
A const
will also throw an error if you try to set it to a new value after it’s been declared:
const z = 3; // valid
z = 4; // error
But if you expect your const
to be immutable in all cases, you may be in for a surprise when an object or an array declared as a const
lets you alter its content.
const z = []; // valid
z.push(1); // valid, and z is now [1]
z = [2] // error
For this reason, I remain skeptical when people recommend using const
constantly in place of var
for all variable declarations, even when you have every intention of never altering them after they’ve been declared.
While it’s a good practice to treat your variables as immutable, JavaScript won’t enforce that for the contents of a reference variables like arrays and objects declared with const
without some help from external scripts. So the const
keyword may make casual readers and newcomers to JavaScript expect more protection than it actually provides.
I’m inclined to use const
for simple number or string variables I want to initialize and never alter, or for named functions and classes that I expect to define once and then leave closed for modification. Otherwise, I use let
for most variable declarations — especially those I want to be bounded by the scope in which they were defined. I haven’t found the need to use var
lately, but if I wanted a declaration to break scope and get hoisted to the top of my script, that’s how I would do it.
Limiting the Scope of the Function
Traditional functions, defined using thefunction
keyword, can be called to execute a series of statements defined within a block on any parameters passed in, and optionally return a value:
function doSomething(param) {
return(`Did it: ${param}`);
}
console.log(doSomething("Hello")); // "Did it: Hello"
They can also be used with the new
keyword to construct objects with prototypal inheritance, and that definition can be placed anywhere in the scope where they might be called:
function Animal(name) {
this.name = name;
}
let cat = new Animal("Fluffy");
console.log(`My cat's name is ${cat.name}.`); // "My cat's name is Fluffy."
Functions used in either of these ways can be defined before or after they’re called. It doesn’t matter to JavaScript.
console.log(doSomething("Hello")); // "Did it: Hello"
let cat = new Animal("Fluffy");
console.log(`My cat's name is ${cat.name}.`); // "My cat's name is Fluffy."
function doSomething(param) {
return(`Did it: ${param}`);
}
function Animal(name) {
this.name = name;
}
A traditional function also creates its own context, defining a value for this
that exists only within the scope of the statement body. Any statements or sub-functions defined within it are executing, and optionally allowing us to bind a value for this
when calling the function.
That’s a lot for the keyword to do, and it’s usually more than a programmer needs in any one place. So modern JavaScript split out the behavior of the traditional function into arrow functions
and classes.
Getting to Class on Time
One part of the traditionalfunction
has been taken over by the class
keyword. This allows programmers to choose whether they would prefer to follow a more functional programming paradigm with callable arrow functions, or use a more object-oriented approach with classes to substitute for the prototypal inheritance of traditional functions.
Classes in JavaScript look and act a lot like simple classes in other object-oriented languages, and may be an easy stepping stone for Java and C++ developers looking to expand into JavaScript as JavaScript expands out to the server.
One difference between functions and classes when doing object-oriented programming in JavaScript is that classes in JavaScript require forward declaration, the way they do in C++ (although not in Java). That is, a class
needs to be declared in the script before it is instantiated with a new
keyword. Prototypal inheritance using the function
keyword works in JavaScript even if it’s defined later in the script, since a function
declaration is automatically hoisted to the top, unlike a class
.
// Using a function to declare and instantiate an object (hoisted)
let aProto = new Proto("Myra");
aProto.greet(); // "Hi Myra"
function Proto(name) {
this.name = name;
this.greet = function() {
console.log(`Hi ${this.name}`);
};
};
// Using a class to declare and instantiate an object (not hoisted)
class Classy {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hi ${this.name}`);
}
};
let aClassy = new Classy("Sonja");
aClassy.greet(); // "Hi Sonja"
Pointed Differences with Arrow Functions
The other aspect of traditional functions can now be accessed using arrow functions, a new syntax that allows you to write a callable function more concisely, to fit more neatly inside a callback. In fact, the simplest syntax for an arrow function is a single line that leaves off the curly braces entirely, and automatically returns the result of the statement executed:const traditional = function(data) {
return (`${data} from a traditional function`);
}
const arrow = data => `${data} from an arrow function`;
console.log(traditional("Hi")); // "Hi from a traditional function"
console.log(arrow("Hi")); // "Hi from an arrow function"
Arrow functions encapsulate several qualities that can make calling them more convenient, and leave out other behavior that isn’t as useful when calling a function. They are not drop-in replacements for the more versatile traditional function
keyword.
For example, an arrow function inherits both this
and arguments
from the contexts in which it’s called. That’s great for situations like event handling or setTimeout
when a programmer frequently wants the behavior being called to apply to the context in which is it was requested. Traditional functions have forced programmers to write convoluted code that binds a function to an existing this
by using .bind(this)
. None of that is necessary with arrow functions.
class GreeterTraditional {
constructor() {
this.name = "Joe";
}
greet() {
setTimeout(function () {
console.log(`Hello ${this.name}`);
}, 1000); // inner function has its own this with no name
}
}
let greeterTraditional = new GreeterTraditional();
greeterTraditional.greet(); // "Hello "
class GreeterBound {
constructor() {
this.name = "Steven";
}
greet() {
setTimeout(function () {
console.log(`Hello ${this.name}`);
}.bind(this), 1000); // passing this from the outside context
}
}
let greeterBound = new GreeterBound(); // "Hello Steven"
greeterBound.greet();
class GreeterArrow {
constructor() {
this.name = "Ravi";
}
greet() {
setTimeout(() => {
console.log(`Hello ${this.name}`);
}, 1000); // arrow function inherits this by default
}
}
let greeterArrow = new GreeterArrow();
greeterArrow.greet(); // "Hello Ravi"
Understand What You’re Getting
It’s not all just syntactic sugar. A lot of the new changes in JavaScript have been introduced because new functionality was needed. But that doesn’t mean that the old reasons for JavaScript’s traditional syntax have gone away. Often it makes sense to continue using the traditional JavaScript syntax, and sometimes using the new syntax can make your code much faster to write and easier to understand. Check out those online tutorials you’re following. If the writer is usingvar
to initialize all of the variables, ignoring classes in favor of prototypal inheritance, or relying on function
statements in callbacks, you can expect the rest of the syntax to be based on older, traditional JavaScript. And that’s fine. There’s still a lot that we can learn and apply today from the traditional ways the JavaScript has always been taught and used. But if you see let
and const
in the intializations, arrow functions in callbacks, and classes as the basis for object-oriented patterns, you’ll probably also see other modern JavaScript code in the examples.
The best practice in modern JavaScript is paying attention to what the language is actually doing. Depending on what you’re used to, it may not always be obvious. But think about what the code you’re writing is trying to accomplish, where you’re going to need to deploy it, and who will be modifying it next. Then decide for yourself what the best approach would be.
Frequently Asked Questions (FAQs) about Modern JavaScript Best Practices
What is the importance of using ‘let’ and ‘const’ in modern JavaScript?
In modern JavaScript, ‘let’ and ‘const’ are used to declare variables. ‘let’ allows you to declare variables that are limited to the scope of a block statement, or expression on which it is used, unlike ‘var’ which defines a variable globally or locally to an entire function regardless of block scope. On the other hand, ‘const’ is used to declare a constant, a value that once declared cannot be changed. Using ‘let’ and ‘const’ provides better scope management and reduces the chances of running into problems related to scope leakage and variable hoisting.
What is hoisting in JavaScript and how does it affect my code?
Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their containing scope during the compile phase. This means that you can use variables and functions before they have been declared. However, it’s important to note that only the declarations are hoisted, not the initializations. This can sometimes lead to unexpected results, especially when using ‘var’, as it can make it seem like the variable is accessible in a scope where it’s not actually available.
How can I avoid callback hell in JavaScript?
Callback hell, also known as Pyramid of Doom, refers to heavily nested callbacks that make code hard to read and debug. To avoid this, you can use Promises or async/await. Promises are objects that represent the eventual completion or failure of an asynchronous operation. They can be chained together to avoid nesting. Async/await is a syntactic sugar on top of Promises that makes asynchronous code look and behave a little more like synchronous code, making it easier to understand and manage.
What is the significance of arrow functions in JavaScript?
Arrow functions, introduced in ES6, offer a more concise syntax to write function expressions. They are anonymous and change the way ‘this’ binds in functions. Arrow functions are particularly useful when working with higher order functions, like ‘map’, ‘filter’, and ‘reduce’, that take other functions as arguments or return them as a result.
How does JavaScript handle asynchronous operations?
JavaScript handles asynchronous operations through its event loop, callbacks, Promises, and async/await. The event loop allows JavaScript to perform non-blocking operations by offloading operations to the system kernel whenever possible. Callbacks are functions that are passed as arguments to other functions and are invoked after some event has occurred. Promises and async/await are abstractions built on callbacks that make asynchronous code easier to write and to read.
What is the use of template literals in JavaScript?
Template literals are string literals that allow embedded expressions. They provide an easy way to interpolate variables and expressions into strings. You can create multi-line strings and perform string interpolation features with them. Template literals are enclosed by the back-tick (
) character instead of double or single quotes.
What is destructuring in JavaScript?
Destructuring is a JavaScript expression that allows us to unpack values from arrays, or properties from objects, into distinct variables. This can greatly simplify your code, making it more readable and maintainable.
What is the spread operator and how can I use it?
The spread operator is a new addition to the set of operators in JavaScript ES6. It allows an iterable to expand in places where zero or more arguments for function calls or elements for array literals are expected. It can be used in array literals, function calls, and object literals.
What is the purpose of the ‘use strict’ directive in JavaScript?
The ‘use strict’ directive was introduced in ECMAScript 5. It’s a way to voluntarily enforce stricter parsing and error handling on your JavaScript code. When used, it can prevent certain actions from being taken and throws more exceptions.
How can I use modules in JavaScript?
Modules in JavaScript are reusable pieces of code that can be exported from one program and imported for use in another program. Modules are particularly useful for maintaining large applications and when you need to call a module from another module.
I've worked as a Web Engineer, Writer, Communications Manager, and Marketing Director at companies such as Apple, Salon.com, StumbleUpon, and Moovweb. My research into the Social Science of Telecommunications at UC Berkeley, and while earning MBA in Organizational Behavior, showed me that the human instinct to network is vital enough to thrive in any medium that allows one person to connect to another.