Jonathan Newton

Agile ECMAScript and What's next?

JavaScript in 2015 was a hectic time. It had been 6 years since ES5; it’s previous release had been abandoned, meaning the world hadn’t seen a core update to JavaScript in 10 years.

ECMAScript is not the same as JavaScript, ECMAScript is the standard and JavaScript is an implementation of ECMAScript.

The weird and wonderful release pattern of ECMAScript pre ES7:

ES Version Release Date
ES1 June 1997
ES2 June 1998
ES3 December 1999
ES4 Cancelled
ES5 December 2009
ES6 / ES2015 June 2015

ES => ECMAScript

This kind of schedule was having a tole on JavaScript. The massive wait time on releases causes a big bang effect when they eventually reach maturity. Features that are not ready are postponed and don’t make it into a release, with so much time between releases this is a big deal.

Releases needed to be more frequent, so features that didn’t make it into the current draft wouldn’t have to wait long for the next iteration.

TC39 (Technical Committee 39)

TC39 is the lean evolution of ECMAScript developing new features with more collaboration and better transparency to get feedback in early (sounds pretty agile!).

Who are TC39?

Delegates from companies, experts, etc. (The kind of clever people that you would expect).

What do they talk about?

ECMAScript stuff, you get the minutes here.

The process

I thought it would be interesting to show potentially new features at each stage of the TC39 process, to do some of these examples you will need to grab Canary

  • Stage 0: Strawman

  • Stage 1: Proposal

  • Stage 2: Draft

  • Stage 3: Candidate

  • Stage 4: Finished


Stage 0 - Strawman

The purpose of this stage is to get the proposal to the table. All that is required is enough documentation for a conversation around the proposed feature, although it’s not required to have code examples at this stage, they are helpful to the committee.

As of writing this, Promise.any is in stage 0.

If you had multiple promised calls and the order and timing of them resolving is not important, you could use Promise.any as your callback handler, meaning you don’t have to wait for all the promises in a promise array to resolve.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const promises = [
new Promise((res, rej) => {
res("Promise: 1")
}),
new Promise((res, rej) => {
res("Promise: 2")
})
];

Promise.any(promises).then(
(first) => {
// Any of the promises was fulfilled.
},
(error) => {
// All of the promises were rejected.
}
);

Stage 1 - Proposal

Once the feature has been promoted to stage 1, the owner of the change will make the case for the addition and describe the shape of a solution. At this point the change will be assigned a “champion”, who will advance the addition. A champion is someone from the committee, assigned to a new successful stage 0 feature to help guide it to maturity. They will write spec text and will represent the proposed features at the TC39 meetings.

Currently new.initialize() is in stage 1, although the repo hasn’t been updated:

Status: Not yet presented at TC39; not at a stage

If you read the minutes from January, it is currently in stage 1.

JavaScript allows you classes to be created that extend another class, this is great and pretty common in OOP languages.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Car {
constructor() {
this.colour = "White";
this.topSpeed = 100;
}
}
class RedCar extends Car {
constructor() {
super();
this.colour = "Red";
}
}

const car = new RedCar();

console.log(car.colour); //Red
console.log(car.topSpeed); //100

But, this can be mutated, stopping the original base class from being called from RedCar constructor super() call.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Car {
constructor() {
this.colour = "White";
this.topSpeed = 100;
}
}
class RedCar extends Car {
constructor() {
super();
this.colour = "Red";
}
}

RedCar.__proto__ = class { constructor() { this.price = 15000; } };
const car = new RedCar();

console.log(car.colour); // Red
console.log(car.price); // 15000
console.log(car.topSpeed); // undefined

We can reconstruct our Car base class from inside our RedCar constructor, but then we can’t extend any of RedCar’s properties.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Car {
constructor() {
this.colour = "White";
this.topSpeed = 100;
}
}

class RedCar extends Car {
constructor() {
const instance = Reflect.construct(Car, [], new.target);
instance.colour = "Red";
return instance;
}
}

RedCar.__proto__ = class { constructor() { this.price = 15000; } };

const car = new RedCar();
console.log(car.colour); // Red
console.log(car.price); // Undefined
console.log(car.topSpeed); // 100

The solution to this problem would be to omit the extends on the RedCar class and instead call new.initialize() from inside the constructor and against a new object and return the new instance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Car {
constructor() {
this.colour = "White";
this.topSpeed = 100;
}
}
class RedCar {
constructor() {
const instance = Reflect.construct(Car, [], new.target);
new.initialize(instance);
this.colour = "Red";
return instance;
}
}

RedCar.__proto__ = class { constructor() { this.price = 15000; } };

const car = new RedCar();
console.log(car.colour); // Red
console.log(car.price); // 15000
console.log(car.topSpeed); // 100

This theoretically would work, but since this is at stage 1, the syntax and semantics could change.

Stage 2 - Draft

Once the feature has been promoted to stage 2, the precise syntax and semantics are expected to be drafted using formal spec language. Once a feature gets to this stage, it is more than likely that it will be developed into the ECMAScript standards.

Promise.allSettled is a new feature set as stage 2.

When awaiting an array of promises you don’t know which have resolved and which have been rejected (if any). If we don’t filter the promise results we can’t handle some of the promises resolving and some of them failing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const promises = [
new Promise((res, rej) => {
res("Promise: 1")
}),
new Promise((res, rej) => {
rej("Promise: 2")
})
];

Promise.all(promises).then((res) => {
console.log(res); //Nothing in here
}).catch((rej) => {
console.log(rej); //Promise: 2
});

To get round this, it’s common to see a filter resolve function that will return an object which contains a status property and a result of each promise in the promise array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const promises = [
new Promise((res, rej) => {
res("Promise: 1")
}),
new Promise((res, rej) => {
rej("Promise: 2")
})
];

function Resolver(promise) {
return promise.then((res) => {
return { status: "resolved", value: res };
}).catch((rej) => {
return { status: "rejected", value: rej };
});
}

Promise.all(promises.map(Resolver)).then((result) => {
var resolvedPromises = result.filter((x) => {
return x.status == 'resolved';
});
var rejectedPromises = result.filter((x) => {
return x.status == 'rejected';
});

console.log(resolvedPromises[0].value); //Promise: 1
console.log(rejectedPromises[0].value); //Promise: 2
});

This can be common if you can handle some promises resolving, but not all of them. The proposed solution we would use Promise.allSettled to mitigate this.

1
2
3
4
5
6
7
8
9
10
11
Promise.allSettled(promises).then((result) => {
var resolvedPromises = result.filter((x) => {
return x.status == 'resolved';
});
var rejectedPromises = result.filter((x) => {
return x.status == 'rejected';
});

console.log(resolvedPromises[0].value); //Promise: 1
console.log(rejectedPromises[0].value); //Promise: 2
});

Stage 3 - Candidate

For a feature to enter stage 3, all designated reviewers have signed off on the current spec text and all ECMAScript editors have signed off on the current spec text. All work is complete and any addition changes to the spec or api would require significant reviewing.

Private Getters and Setters is currently sat in stage 3 and has some pretty cool syntax.

You can start to play with private getters and setters in Canary

In my opinion, the reason this feature is needed, is to allow developers to adhere to SOLID principles. The open/closed principle is simple - “open for modification, closed for extension”. Some properties are better kept internal for safety and for clear API definitions.

1
2
3
4
5
6
7
8
9
10
11
class RedCar {
#colour = "Red"; //Private

getColour() {
return this.#colour;
}
}

var car = new RedCar();

console.log(car.getColour()); //Red

To declare a property in a class as private, we put a # before the property name. Trying to set the value on a private field will throw an exception.

1
2
3
4
5
6
7
8
9
10
11
12
class RedCar {
#colour = "Red";

getColour() {
return this.#colour;
}
}

var car = new RedCar();

car.#colour = "Yellow";
//Uncaught SyntaxError: Undefined private field #colour: must be declared in an enclosing class

Stage 4 - Finished

Once a feature makes it to this stage, it indicates that the addition is ready for inclusion in the formal ECMAScript standard. It’s a big deal! For it to to be sent out with the next update, all Test262 acceptance tests have to been written for mainline usage scenarios and all ECMAScript editors have signed off on the pull request. Features at this stage are often being implemented in a polyfill, awaiting it’s official release.

Currently pending an official release is Object.fromEntries. This static method will transform a list of key-value pairs into an object.

1
2
3
4
5
const results = [['1', 'John'], ['2', 'David']];

const resultObj = Object.fromEntries(results);

console.log(resultObj); //{ 1: "John", 2: "David" }

Whats Next?

Since ES5 ECMAScript changed it’s release process, it has also changed it’s version naming. They moved away from ES{X} and now use ES{Year}. So you will see new revisions named ES2015, ES2016, ES2017 …

You can keep up with developing changes in ECMAScript and what to expect in JavaScript here.