DEV Community

Farid Shabanov
Farid Shabanov

Posted on • Originally published at academy.binary-studio.com

The long path of JavaScript - from ES6 until today.

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());
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  };
}

Enter fullscreen mode Exit fullscreen mode

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;
    });
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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 undefinedwhen 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;
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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" } });
Enter fullscreen mode Exit fullscreen mode

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(' '),
  ),
);
Enter fullscreen mode Exit fullscreen mode

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(%);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
oculus42 profile image
Samuel Rouse

This is a great article talking about the updates and changes of JavaScript!

ES6, which was released in 2015 and was the second major and the biggest release for the language

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);

Collapse
 
fsh02 profile image
Farid Shabanov

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!

Collapse
 
efpage profile image
Eckehard

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.

Collapse
 
fsh02 profile image
Farid Shabanov

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

Collapse
 
ingosteinke profile image
Ingo Steinke, web developer • Edited

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!

Collapse
 
fsh02 profile image
Farid Shabanov

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!

Collapse
 
pengeszikra profile image
Peter Vivo

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.

Collapse
 
fsh02 profile image
Farid Shabanov

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!

Collapse
 
eshimischi profile image
eshimischi

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

Collapse
 
fsh02 profile image
Farid Shabanov

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

Collapse
 
mickhence profile image
Mark henry

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.

Collapse
 
fsh02 profile image
Farid Shabanov

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

Collapse
 
vladyn profile image
Vladimir Varbanov

I was almost forgot about logical assigment operators. Really nifty! Great article by the way.

Collapse
 
fsh02 profile image
Farid Shabanov

Although they are not used as often as other features, they are indeed really useful!
Thank you for your comment and interest!

Collapse
 
anderslundback profile image
Anders Lundback

Thank you for a well written article. It was a nice recap of the history and insight into what’s next.

Collapse
 
fsh02 profile image
Farid Shabanov

Thank you for your comment and your interest!

Collapse
 
wolfchamane profile image
Arturo Martínez Díaz

You are talking about JS evolution into ES6+, but your code is all TS.
Funny.

Collapse
 
fsh02 profile image
Farid Shabanov

Well, TS in the end transpiles to JS 🙂

Collapse
 
jangelodev profile image
João Angelo

Hi Farid Shabanov,
Top, very nice !
Thanks for sharing

Collapse
 
fsh02 profile image
Farid Shabanov

Thank you for your comment!

Collapse
 
avmantzaris profile image
Alex V. Mantzaris

the pipeline operator will be great, I use it in Julia Lang and makes things so much more clear.

Collapse
 
fsh02 profile image
Farid Shabanov

Agree! I love the way the pipeline operator helps to just follow the variable and see in order, what happens to it. Much cleaner, than nested code

Collapse
 
minhazhalim profile image
Minhaz Halim (Zim)

Very useful article.

Collapse
 
fsh02 profile image
Farid Shabanov

Thank you for your comment!