Believe it or not, many of the Javascript syntax features that developers take for granted today did not exist in the language just a few years ago.
Yes, even promises are new. Javascript use to be a verbose language with lots of quirks and it still is. For example, are you sure you understand how this
works? Do you know why you sometimes need to use bind
method? What about scoping and hoisting?
I started programming in Javascript professionally when jQuery was cool, Node.js was just announced on HackerNews and all the cool devs were working with Backbone.js.
It was not all roses and sunshine. Trust me. Callback Hell and Pyramid of Doom were real. Sometimes I dreamed about double-callbacks at night. It was very hard following the code at times.
Thankfully, things have greatly improved since, but we as developers always take things for granted without giving them any thought or appreciation.
That's why I decided to show what Javascript used to look like back in the days, (old school) and how much it has evolved in just a few years (new school).
Without further ado, here are nine of the ES features I am glad exist.
- Arrow functions
- Default parameters
- Destructuring
- Object literals
- Default returns
- Spread operator
- Async/await
- Optional chaining
- Nullish Coalescing Operator
There features save my fingers from hurting and make my code more concise and easier to understand.
Read on to learn them all!
1. Arrow functions
This is probably the ES feature I use and love the most. It makes code so much easier to read in my opinion.
// old school
function fullname(user) {
return `${user.firstName} + ${user.lastName}`;
}
// or
const fullname = function () {
return `${user.firstName} + ${user.lastName}`;
}
document.addEventListener('click', function (event) {
alert('old school');
});
// new school
const fullname = user => `${user.firstName} + ${user.lastName}`;
document.addEventListener('click', event => {
alert('new school');
});
// or just this
document.addEventListener('click', () => alert('new school'));
Note: Something to keep in the back of the head, when using arrow functions, is that this
works just as you expect it to inside them. Or maybe better to say: arrow functions don't have their own this
. It's taken from the outer scope instead. Sometimes it's actually not what you want.
Pretty neat and much easier to read. Don't you agree?
2. Default Parameters
Default parameters is a real life-saver sometimes. A simple code example is the best explanation.
// old school
const createUser = function (firstName, lastName, role) {
return {
firstName: firstName,
lastName: lastName,
role: role || 'user'
};
}
// new school
const createUser = (firstName, lastName, role = 'user') => ({
firstName,
lastName,
role,
});
Less code, less logic and reads much better, doesn't it?
3. Destructuring
This is a really beautiful ES feature. It works with objects, arrays and function parameters.
Desctructuring lets you extract one or more properties from arrays and objects into own variables.
const user = { firstName: 'Jane', lastName: 'Doe', role: 'admin' };
// extract role property to a variable
const { role } = user;
console.log(role);
// admin
You have probably used it many times in your import
statements without giving it much thought.
You can use destructuring with arrays too. If you worked with React hooks, then you have definitely used it.
// Naïve state hook implementation
// Don't use in production!
const state = {};
const useState = (name, initial) => {
state[name] = initial;
// return an array
return [
state[name],
value => (state[name] = value)
];
};
// returns an array
const loggedInState = useState('loggedIn', false);
// old school
const isLoggedIn = loggedInState[0];
const setLoginStatus = loggedInState[1];
// new school
// assign array[0] and array[1] to variables
const [isLoggedIn, setLoginStatus] = useState('loggedIn', false);
if (isLoggedIn) {
setLoginStatus(false);
}
It works with function parameters too. Very nice!
// instead of this
let isAdmin = user => user.role === 'admin';
// you can do this
isAdmin = ({ role }) => role === 'admin';
// and also combine it with default parameters
isAdmin = ({ role = 'user' }) => role === 'admin';
const user = { uid: 'jdoe', role: 'admin' };
console.log(isAdmin(user));
// true
There are other useful things you can do with desctructuring, such as rest parameters. I encourage you to explore it further on your own. You will find a list of resources at the end of the article.
4. Object Literals
The name "object literal" says nada. That's why I like to call it object property initializer shorthand. Maybe that's even its official name. Not sure.
Anyhow, it's one of the features that can save you a lot of repetitive typing. If the variable name and property name are the same you only have to specify variable name when creating an object's property.
// old school
function createUser(firstName, lastName, dob) {
const id = generateId();
return {
id: id,
firstName: firstName,
lastName, lastName,
dob: dob
};
}
// new school
const createUser = (firstName, lastName, dob) =>
({ id: generateId(), firstName, lastName, dob });
Just look at how many potential typos and untyped characters we just saved!
You can also use the shorthand operator with methods on the object.
// old school
function createUser(firstName, lastName, dob) {
return {
firstName: firstName,
lastName, lastName,
dob: dob,
fullName: function () {
return firstName + ' ' + lastName;
}
};
}
// new school
const createUser = (firstName, lastName, dob) => ({
firstName,
lastName,
dob,
fullName() {
return `${this.firstName} ${this.lastName}`;
},
});
I don't know about you, but I find this feature very useful.
5. Default Returns
I've already sneaked in default returns in some of the examples above, but it's not something that existed in Javascript before. Believe it or not.
const arr = [1, 2, 3, 4, 5];
// old school
const doubled = arr.map(function (val) {
return val * 2;
});
// new school with default implicit return
const tripled = arr.map(val => val * 3);
Default returns also lets you return objects directly.
// old school
function createUser(firstName, lastName) {
return {
firstName: firstName,
lastName: lastName,
role: 'user'
};
}
// new school
const createUser = (firstName, lastName) => ({ firstName, lastName });
Don't take simple things like this for granted. Be thankful for all the typing they save you in the long run.
6. Spread Operator
I actually not sure if it's an operator or a syntax change, but I use it all the time.
It's a very useful feature and if you have worked with Redux or any other modern Javascript library you have probably used the pattern, or operator, or syntax.
It often used to create shallow copies of objects and arrays, or to merge objects or arrays.
Let's start with the arrays, since A comes first in the English alphabet.
const one = ['a', 'b', 'c'];
const two = ['d', 'e', 'f'];
// old school
// copy array
const copy = one.slice(0);
// combine arrays
const combined = one.concat(two);
// new school
// copy array
const copy = [...one];
// combine arrays
const combined = [...one, ...two];
Now, here is what we can do with spread operator and objects.
let user = { uid: 'jdoe', name: 'Jane Doe' };
let status = { loggedIn: true };
// create a new object by combining other objects
// properties with the same name will be overwritten
// by the last object in argument chain
let state = Object.assign({}, user, status, { createdAt: Date.now() });
// or by simply using the spread operator
state = { ...user, ...status, createdAt: Date.now() };
Spread operator (or syntax) is very versatile. I only scratched the surface of what it can do.
If you learn the inside and outs of it, you will have a powerful tool in your toolbox.
How could I even work effectively before it existed? I actually don't remember right now, but I suspect it was painful.
7. async/await
Promises are also new to Javascript and they might be the most significant new addition to the language. Before promises we had callbacks, which could lead to Callback Hell and the dreaded Pyramid of Doom.
Promises improved that, but I still found them a little hard to read. Today, I often find myself reaching for the async/await pattern, which makes my code easy to read and reason about.
Plus, async/await pattern can make the code easier to structure.
// fake function to return a user by id
const fetchUser = id => new Promise((resolve, reject) => {
if (id === 1) {
resolve({ id: 1, username: 'jdoe' });
} else {
reject(new Error('no such user'));
}
});
// the promise way
const getUserInfo = id =>
fetchUser(id)
.then(user => fetchInfomation(user))
.then(userInfo => {
if (userInfo.address) {
// how do we get user? we need to pass it down somehow
enrichProfile(user, userInfo);
}
if (userInfo.age && userInfo.age < 18) {
// hmm ... how do we access user here?
addAgeRestrictions(user);
}
})
.catch(err => console.log(err));
// the async/await way
const getUserInfo = async id => {
// error handling with try/catch blocks
try {
const user = await fetchUser(id);
const userInfo = await fetchInformation(user);
// both user and userInfo are available in the function scope
if (userInfo.address) {
enrichProfile(user, userInfo);
}
if (userInfo.age && userInfo.age < 18) {
addAgeRestrictions(user);
}
} catch (err) {
console.log(err.message);
throw err;
}
}
As you can see above, prefixing your function with async
keyword and using await
to get the promise result can make your code easier to structure, read and reason about.
8. Optional Chaining (ES2020)
This new feature is my recent favorite as it saves me a lot of typing. It actually comes from Typescript, but has now been accepted into ECMAScript standard spec.
You will not get runtime errors if some deeply-nested property does not exist on the object.
Errors like the one below, that you have probably seen many times before.
const customer = { name: 'Jane' };
console.log(customer.address.doorCode);
// Uncaught TypeError: Cannot read property 'doorCode' of undefined
Here is a better example of what this operator brings to the table (or screen).
// old school
const sendInstructions = function (customer) {
// old, cumbersome way to check for a property
const hasDoorCode =
customer && customer.address && customer.address.doorCode;
if (hasDoorCode) {
messageToCourier(carryIn());
}
}
// new school
const sendInstructions = customer => {
// optional chaining operator at work
const hasDoorCode = customer?.address?.doorCode;
if (hasDoorCode) {
messageToCourier(carryIn());
}
}
Simply explained, the optional chaining operator short circuits and returns undefined
for the first non-existent property.
9. Nullish Coalescing Operator (ES2020)
This operator with a weird name is new, so thread carefully and use a bundler setup that supports it.
What it does it to strictly check if the value is either null
or undefined
. Why is that important? Because one of Javascript's quirks is that many values can be falsy or truthy. Empty string, zero and boolean false
are all falsy values, so is undefined
and null
.
Sometimes we need to strictly check if value is null
or undefined
and not only falsy.
Let me demonstrate with a few examples to make it more clear.
const user = {
name: 'John Doe',
settings: { showWelcomeScreen: false, animation: 0 },
};
// old school
// the actual value is false, but it incorrectly defaults to true,
// which can lead to hard to find bugs
const showWelcomeScreen =
(user && user.settings && user.settings.showWelcomeScreen) || true;
// the animation value is actually 0, but we incorrectly set it to 100
// since 0 is a falsy value in Javascript
const duration = (user && user.settings && user.settings.animation) || 100;
// new school
// this behavior is correct. We now only set a value if
// the property is null or undefined
const showWelcomeScreen = user?.settings?.showWelcomeScreen ?? true;
const animation = user?.settings?.animation ?? 100;
Even though this operator is very new, I find myself using it more often. It's really useful when dealing with user-controlled settings in user interfaces.
Conclusion
There you go. 9 Javascript features I am glad exist.
Of course, there are many more new cool and useful ES features, but these are the ones I use the most.
I strongly recommend you to get familiar with them all. They will make you level-up your game and help you impress your colleagues.
If you want to dig deeper, here are some great learning resources:
An ECMAScript explainer - ECMAScript spec, that Javascript is based on, is constantly evolving. If you are curious about how Javascript versioning works and what feature requests get accepted, here is a great article explaining the process.
MDN - Many great in-depth tutorials from the trusty Mozilla Developer Network.
javascripttutorial.net - another great resource. More like a reference with good examples than a tutorial.
ES6+ Cheatsheet - sweet, short and good looking reference from devhints.io.
Learn ES2015 (ES6) - dense, but good tutorial from Babel.js that covers many modern JS features.
Even though these new syntax features and operators make your code more concise, always remember the trade-off between conciseness and readability. It's you who might be reading the code in the future.
Top comments (2)
Still turning backflips every time I get to use parameter defaults. Recently shared this on just that subject:
dev.to/stereoplegic/i-m-officially...
I'm sure you, old timer like me, can recall plenty of nonsensical "MVC for the sake of MVC" projects and refactors which were supposed to magically make our JS "object oriented" (because JS isn't already? 🙃) at the early-mid portion of the last decade.
Ahh, MVC/MVP. The good old days! 😆