According to a Stack Overflow survey, JavaScript was the most popular language among developers in 2023. JavaScript was initially developed for Netscape Navigator - a web browser that was developed in the middle of 1990s - and now is being used in almost every domain of programming - Web Development, Mobile app development, Game development, Machine Learning and many others.
But how did a language which was developed in 10 days by Brendan Eich become so popular? In this article, we will go through the life of JavaScript from ES6, which was released in 2015 and was the second major and the biggest release for the language, until today, the year of 2023. We will see why ES6 was important for the future of JavaScript, how it changed JavaScript and how it has influenced it over time.
What was new and important in ES6?
ES6, also known as ECMAScript 2015, was the second major release for JavaScript after ES5 and was the largest release since the language was first released in 1997. It introduced several new features and syntax improvements that enhanced the language's functionality and made it more efficient.
Some of the key features introduced in ES6 include arrow functions, template literals, destructuring, and classes. These additions allowed developers to write cleaner and more concise code, improving readability and maintainability. The lack of some of these features was one of the main reasons why developers were choosing other languages over JavaScript. Another important point of this release was the further release schedule for JavaScript. According to the new schedule, a new version of JavaScript should be released each year which ensures the technology’s further development.
Introduction of let
and const
keywords
One of the main features of ES6 was the introduction of the let
and const
keywords. You might wonder why these keywords were important if we already had var
. The main difference between var
and new keywords was that var
has a global scope, while let
and const
have a block scope. Another important difference is that the variables declared with var
are hoisted to the top of their scope (which is global).
The new keywords helped to solve a common issue with variables being accidentally redefined, which resulted in a big number of bugs.
The new let
and const
keywords are block scoped and they cannot be redefined. While the variable defined with let
can be reassigned new values, the ones created with const
can only be assigned once.
JavaScript Modules
JavaScript modules were a crucial step in allowing the creation of big JavaScript applications. They allowed separating the code into different files, which would allow creating a cleaner codebase. In the early stages of JavaScript, it was used in only some web applications and only a small amount of JavaScript code was written. The pages were not very interactive and having modules was not necessary.
Over the years, different JavaScript libraries and packages were created and applications became more and more interactive, which required more JavaScript code. The lack of modules made this task harder and required additional packages, such as RequireJS.
JavaScript Classes
Although it was possible to implement class-like functionalities before ES6 using function
, it wasn’t very easy to do so. Before ES6, creating classes required updating the prototype
object, which is a part of every object in JavaScript.
// Before ES6
function User(name: string, birthYear: number): void {
this.name = name;
this.birthYear = birthYear;
}
User.prototype.calculateAge = function (): number {
return new Date().getFullYear() - this.birthYear;
};
var user = new User('Farid', 2002);
// IDE does not see calculateAge method
console.log(user.calculateAge());
We needed to assign each method to a prototype
object. Another disadvantage of this method is the fact that IDE does not see the methods we add to a prototype
object. The alternative of the same object using ES6 class
would be:
// After ES6
class User {
private name: string;
private birthYear: number;
public constructor(name: string, birthYear: number) {
this.name = name;
this.birthYear = birthYear;
}
public calculateAge(): number {
return new Date().getFullYear() - this.birthYear;
}
}
const user = new User('Farid', 2002);
// IDE sees calculateAge method
console.log(user.calculateAge());
As you can see, the new syntax makes creating classes much easier, faster and clear.
Arrow functions
Arrow functions were another major addition to JavaScript with ES6. These functions allowed developers to write shorter and cleaner function expressions, and they also solved some issues with the this
keyword in JavaScript.
In traditional function expressions (using function
keyword), the this
keyword would refer to the object that called the function. However, arrow functions capture the surrounding this
context lexically, meaning that they inherit the this
value from their surrounding scope. This eliminates the need for using .bind()
, .call()
, or .apply()
to preserve the this
context.
class User {
public name: string;
public age: number;
public constructor(name: string, age: number) {
this.name = name;
this.age = age;
// We have to bind `this` to be able to use its correct value within our method
this.updateAge = this.updateAge.bind(this);
}
public updateAge(age: number) {
this.age = age;
}
}
In this example, .bind
has to be called in order to have a correct value of this
within our method. The same method would not require .bind
to be called if the method is created using an arrow function.
class User {
public name: string;
public age: number;
public constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public updateAge = (age: number) => {
this.age = age;
};
}
What has changed since ES6?
Although ES6 was one of the biggest updates of JavaScript, it didn’t fix all the issues. Since ES6, lots of important features were added to JavaScript, which greatly improved the language.
Async Await
The introduction of async functions made it much easier to work with Promises. By implementing asynchronous functions, we can wait for Promises to settle, before proceeding with the rest of the logic. Before the introduction of async functions, the same logic could be implemented by using .then
chaining, which would reduce the readability of the code.
Here is an example of fetching the list of users without using async await
:
const getUsers = () => {
fetchUsers()
.then((response) => {
return response.json();
})
.then((users) => {
console.log(users);
return users;
});
};
And this is how simplified the same piece of code is when using async await
syntax:
const getUsers = async () => {
const response = await fetchUsers();
const users = await response.json();
console.log(users);
return users;
};
Optional chaining
Optional chaining is a powerful feature, especially if we often access nested properties or functions, which are optional. It helps to avoid errors such as Cannot read properties of undefined
or Cannot read properties of null
by returning undefined
when the property is not available.
type Coordinates = {
lat: number;
lng: number;
};
type UserLocation = {
country: string;
coordinates?: Coordinates;
};
type User = {
name: string;
surname: string;
location?: UserLocation;
};
// Without optional chaining
const getUserCoordinates = (user?: User): Coordinates | null => {
if (user && user.location && user.location.coordinates) {
return user.location.coordinates;
}
return null;
};
Without optional chaining, we need to check each nested object and make sure that the nested object exists. Optional chaining helps us to avoid unnecessary if
checks and use inline checks.
// With optional chaining
const getUserCoordinates = (user?: User): Coordinates | null => {
return user?.location?.coordinates ?? null;
};
Logical assignment operators
Starting from older versions of JavaScript, we can use different assignments such as +=
or -=
, but the similar assignments did not work for logical checks such as ||
, &&
and ??
. The logical assignment operators assign the value on the right to the value on the left if the left value is falsy, truthy or nullish.
Logical OR (||=
)
The logical OR operator only assigns the value if the value on the left is falsy. In case it is truthy, its value will not change.
// Logical OR
let profilePictureUrl = '';
profilePictureUrl ||= 'default_url';
console.log(profilePictureUrl); // "default_url"
Logical AND
As opposed to the logical OR operator, the logical AND operator will only assign the value if the value on the left is truthy. This comes handy when we try to assign a value to a nested variable in an object.
// Logical AND
type User = {
name?: string;
};
const user: User = {};
user.name &&= 'Farid';
Nullish coalescing assignment
Nullish coalescing assignment is very similar to the logical OR assignment, but it will only assign a value if the left part is nullish. In JavaScript, nullish values are null
and undefined
// Nullish coalescing assignment
const user: User = {
name: 'Farid',
};
user.name ??= 'Guest';
Top level await
One of the most significant changes in JavaScript since ES6 is the introduction of top level await. This feature allows you to use the await
keyword outside of async functions, at the top level of your code. It has made the handling of async operations more straightforward, especially in module initializations and configurations where async functions were not allowed before.
import fs from 'fs/promises';
const readData = async () => {
try {
const content = await fs.readFile('data.txt', 'utf-8');
return content;
} catch (error) {
throw new Error('Could not read file', { cause: error });
}
};
const content = await readData();
Before, the same code would require using .then
callbacks, which would make the code messy and hard to read
import fs from 'fs/promises';
const readData = async () => {
try {
const content = await fs.readFile('data.txt', 'utf-8');
return content;
} catch (error) {
throw new Error('Could not read file', { cause: error });
}
};
readData().then((content) => {
// handle content further
});
What awaits JavaScript in future
JavaScript is improved day by day and a crucial role in this process is played by TC39 (Technical Committee 39). This committee is responsible for evolving the JavaScript language further, maintaining and updating the language standards, analyzing proposals and other processes that help JavaScript to continuously improve.
As mentioned before, TC39 is responsible for analyzing proposals. Proposals are the contributions from the community to add new features to JavaScript. Some of the popular proposals are Temporal
, import attributes
and pipeline operator
.
Temporal
The Temporal API, which is currently in Stage 3, is being developed to improve the current Date object, which is mostly known for its unexpected behavior. Today there are lots of date-time libraries for JavaScript, such as date-fns
, moment
, js-joda
and a huge number of others. They all try to help with unpredictable and unexpected behavior of JavaScript Date object by adding features such as timezones, date parsing and almost everything else.
With different objects like Temporal.TimeZone
, Temporal.PlainDate
and others, Temporal API is trying to address all these issues and replace JavaScript Date object. You can start testing the new API using an npm
package named @js-temporal/polyfill
Import attributes
The proposal for import attributes aims to improve the import statement in JavaScript by allowing developers to assert certain conditions about the imported module. This can help catch errors at compile-time instead of runtime and make the code more robust. Currently this proposal is in Stage 3.
With import attributes, you can specify the type of the imported module or ensure that a specific version of the module is used.
import json from "./foo.json" assert { type: "json" };
import("foo.json", { with: { type: "json" } });
Pipeline operator
The pipeline operator proposal, which is currently in Stage 2, introduces a new operator |>
that allows developers to chain multiple function calls together in a more readable and concise way. Together with the pipeline operator, the placeholder operator %
is being introduced which will hold the previous function’s value. It should enhance the readability and maintainability of code, especially when performing a series of operations on a value.
Instead of nested function calls or long chains of dot notation, the pipeline operator allows developers to write code in a more linear and intuitive manner. Here is a real-world example from a React repository, which can be improved by using the pipeline operator:
console.log(
chalk.dim(
`$ ${Object.keys(envars)
.map((envar) => `${envar}=${envars[envar]}`)
.join(' ')}`,
'node',
args.join(' '),
),
);
As you can see, by adding nested function calls it becomes much harder to read and understand the code. By adding the pipeline operator, this code can be updated and improved:
Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> `$ ${%}`
|> chalk.dim(%, 'node', args.join(' '))
|> console.log(%);
Decorators
Although decorators are not yet available in JavaScript, they are actively used with the help of such transpilers as TypeScript, Babel and Webpack. Currently, the decorators proposal is in Stage 3, which means it is getting closer to being a part of native JavaScript. Decorators are the functions, that are called on other JavaScript elements, such as classes, class elements, methods, or other functions, by adding an additional functionality on those elements.
Custom decorators can be easily created and used. In the bottom, the decorator is just a function, that accepts specific arguments:
- Value - the element, on which the decorator is used
- Context - The context of the element, on which the decorator is used.
Here’s the total type of the decorator:
type Decorator = (
value: Input,
context: {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
};
private?: boolean;
static?: boolean;
addInitializer(initializer: () => void): void;
},
) => Output | void;
A useful example of using the decorators would be for protecting the methods with a validation rule. For that, we can create the following decorator:
const validateUser = (rule: (...args: number[]) => boolean) => {
const decorator: Decorator = (_target, _context) => {
return function (...args: number[]) {
const isValidated = rule(...args);
if (!isValidated) {
throw new Error('Arguments are not validated');
}
};
};
return decorator;
};
And use it the following way:
const ADMIN_ID = 1;
const checkIsAdmin = (id: unknown): boolean => {
return id === ADMIN_ID;
};
class User {
@validateUser(checkIsAdmin)
deleteUser(id: unknown) {
// Delete logic
}
}
These are just a few examples of the proposals that are being considered for the future of JavaScript. With the continuous efforts of TC39 and the active participation of the JavaScript community, we can expect JavaScript to evolve and improve even further in the coming years.
Conclusion
JavaScript has come a long way since the release of ES6 in 2015. ES6 introduced important features such as let
and const
keywords, JavaScript modules, classes, and arrow functions. Since then, JavaScript has continued to improve, with the introduction of features like async/await, optional chaining, and logical assignment operators.
Looking ahead, JavaScript has a promising future with proposals like Temporal
, import
attributes, the pipeline operator
, and decorators
. With the continuous efforts of TC39 and the active participation of the JavaScript community, we can expect JavaScript to continue improving and remain a popular language in the years to come.
Top comments (24)
This is a great article talking about the updates and changes of JavaScript!
This is my one quibble: ES6 was the fourth major release. Undoubtedly the largest release, though. I would consider both the third and fifth versions to be major releases in their own right.
ES3 introduced function expressions, object literals, and try/catch. It doesn't get a lot of fanfare, but it permitted the creation of JSON and made JavaScript robust enough to build complete applications like Outlook Web Access and Gmail.
ES5 added getters and setters. This enabled a large number of advancements, including the chaining style like Chai's
expect(value).to.be(2);
Indeed ES3 was a very important one, but as you said "It doesn't get a lot of fanfare" and it is not really considered as a major one. Probably it's just hidden in the shades of bigger releases, such as ES5 and ES6. But there's no doubt, that it played a great role in further development of the technology!
Yes, it´s amazing how much Javascript evolved from the first versions. And how fast it is executed in a modern Javascript engine.
On the other hand, a browser engine has to be always backwards compatible, so it is very unlikely that any old feature ever will be marked as "depeciated". This can make it very confusing for beginners. And it makes some new features less powerful, as they are a compromize of old and new. A good examples are Javascript classes, that somehow managed to adopt elements of other class based languages, but still generate Javascript objects. As a downside, class objects are not fully encapsulated, which makes them less secure to use.
A main reason for the popularity is, that Javascript is available everywhere. We will have to accept, that many of the historical misconceptions will stay alongside with all the good progress ist makes.
That's right, JavaScript classes are also called "syntax sugar", because they are "not actually classes", if we compare with other languages.
It is important to keep backward compatibility, because there are millions of websites online. And most of them use old syntax, old features. For example, 90% of all websites still use jQuery. Although jQuery is a powerful tool, we can consider it as outdated, because there are more powerful tools, that are better to use
The most important next step would be integrating TypeScript into the official ECMA / JavaScript standard, maybe also an official transpiler like Babel.js for all of us web developers having to compile our elegant modern software into unreadable spaghetti code that we hope will work as expected, fall back to unsafe pre-ES6 notation, or make our website inaccessible to customers who have to use outdated devices and browsers. A more standardized transpilation and fallback/polyfill process might make our code use native type-safe modern syntax if supported but without failing otherwise.
Thanks for your article summing up important milestones of JS history!
Integrating TypeScript into JS would be a huge change, but considering the different purposes of the two, it would need lots of discussions. Considering the fact that there are lots of developers, who are against TypeScript, it would be a difficult decision to make. I believe the developers should be able to select the tools they want to use - for example TypeScript or JSDoc, and keep TypeScript as an additional tool, not the part of JavaScript
Thank you for your comment and your interest!
great article,I miss the array and object destruction which is also great ES6 features.
my favorite is pipeline operator, I was used a lot ( with babel ) and that is give a great revers direction thinking which is at the end much more logical in a functional programming. Even I was decided to make a typescript to capable use pipeline operator ... forked, but I don't have too much spare time to develop it ... sometimes.
Array and object destructuring is really a great example! It makes some parts of the code a lot cleaner and easy to understand. There are really a lot of important changes, that were added in ES6, but if we mention everything, the article will be too big :)
I agree, that the pipeline operator is a nice feature. As it can be seen from the example, it can reduce the nesting, which always makes it hard to understand the logic. With the pipeline operator you just follow the variable and it is much cleaner!
Don’t forget that even though javascript is a fundamental language, typescript is also actively involved in the development of the standard, but it is also ahead of its time, many features are already available, and only after that it is adapted to modern browsers and js
Exactly! TypeScript indeed played a great role in improving JS. There are some important features, that are available in TypeScript, but not yet available in JS. A great example would be Decorators, Private Class Fields (#) and many others.
Also important to mention, that not only TypeScript played a role in improving JS, but other technologies as well
Since ES6 (2015), JavaScript has undergone significant evolution. ES6 introduced major features like arrow functions, classes, modules, and promises, transforming the way developers write JavaScript. Subsequent versions, from ES7 to ES13 (2022), added enhancements such as async/await, BigInt, optional chaining, nullish coalescing, and more. These updates have improved performance, developer productivity, and the language’s versatility, solidifying JavaScript's role in modern web development and beyond.
ES6 was indeed a turning point in the development of JavaScript. Not only it introduced lots of major features, but also it was an important base for future development of the language. And since ES6, lots of important and useful features were added, just as you mentioned
I was almost forgot about logical assigment operators. Really nifty! Great article by the way.
Although they are not used as often as other features, they are indeed really useful!
Thank you for your comment and interest!
Thank you for a well written article. It was a nice recap of the history and insight into what’s next.
Thank you for your comment and your interest!
You are talking about JS evolution into ES6+, but your code is all TS.
Funny.
Well, TS in the end transpiles to JS 🙂
Hi Farid Shabanov,
Top, very nice !
Thanks for sharing
Thank you for your comment!