Via Addy Osmani
-----
I believe the day-to-day practice of writing JavaScript is going to
change dramatically for the better when ECMAScript.next arrives. The
coming year is going to be an exciting time for developers as features
proposed or finalised for the next versions of the language start to
become more widely available.
In this post, I will review some of the features I'm personally looking forward to landing and being used in 2013 and beyond.
ES.next implementation status
Be sure to look at Juriy Zaytsev's ECMAScript 6 compatibility table, Mozilla's ES6 status page as well as the bleeding edge versions of modern browsers (e.g Chrome Canary, Firefox Aurora) to find out what ES.next features are available to play with right now.
In Canary, remember that to enable all of the latest JavaScript experiments you should navigate to chrome:flags
and use the 'Enable Experimental JavaScript' option.
Alternatively, many ES.next features can be experimented with using Google's Traceur transpiler (useful unit tests with examples here) and there are shims available for other features via projects such as ES6-Shim and Harmony Collections.
Finally, in Node.js (V8), the --harmony
flag activates a number of experimental ES.next features including block scoping, WeakMaps and more.
Modules
We're
used to separating our code into manageable blocks of functionality. In
ES.next, A module is a unit of code contained within a module
declaration. It can either be defined inline or within an externally
loaded module file. A skeleton inline module for a Car could be written:
A module instance
is a module which has been evaluated, is linked to other modules or has
lexically encapsulated data. An example of a module instance is:
- module myCar at "car.js";
module
declarations can be used in the following contexts:
- module UniverseTest {};
- module Universe { module MilkyWay {} };
- module MilkyWay = 'Universe/MilkyWay';
- module SolarSystem = Universe.MilkyWay.SolarSystem;
- module MySystem = SolarSystem;
An export
declaration declares that a local function or variable binding is
visible externally to other modules. If familiar with the module
pattern, think of this concept as being parallel to the idea of exposing
functionality publicly.
- module Car {
-
- var licensePlateNo = '556-343';
-
- export function drive(speed, direction) {
- console.log('details:', speed, direction);
- }
- export module engine{
- export function check() { }
- }
- export var miles = 5000;
- export var color = 'silver';
- };
Modules import
what they wish to use from other modules. Other modules may read the module exports (e.g drive()
, miles
etc. above) but they cannot modify them. Exports can be renamed as well so their names are different from local names.
Revisiting the export example above, we can now selectively choose what we wish to import
when in another module.
We can just import drive()
:
We can import drive()
and miles
:
- import {drive, miles} from Car;
Earlier,
we mentioned the concept of a Module Loader API. The module loader
allows us to dynamically load in scripts for consumption. Similar to import
, we are able to consume anything defined as an export
from such modules.
-
- Loader.load('car.js', function(car) {
- console.log(car.drive(500, 'north'));
- }, function(err) {
- console.log('Error:' + err);
- });
load()
accepts three arguments:
moduleURL
: The string representing a module URL (e.g "car.js")
callback
: A callback function which receives the output result of attempting to load, compile and then execute the module
errorCallback
: A callback triggered if an error occurs during loading or compilation
Whilst
the above example seems fairly trivial to use, the Loader API is there
to provide a way to load modules in controlled contexts and actually
supports a number of different configuration options. Loader
itself is a system provided instance of the API, but it's possible to create custom loaders using the Loader
constructor.
What about classes?
I'm not going to be covering ES.next classes in this post in more, but for those wondering how they relate to modules, Alex Russell has previously shared a pretty readable example of how the two fit in – it's not at all about turning JavaScript into Java.
Classes
in ES.next are there to provide a declarative surface for the semantics
we're used to (e.g functions, prototypes) so that developer intent is
expressed instead of the underlying imperative mechanics.
Here's some ES.next code for defining a widget:
- module widgets {
-
- class DropDownButton extends Widget {
- constructor(attributes) {
- super(attributes);
- this.buildUI();
- }
- buildUI() {
- this.domNode.onclick = function(){
-
- };
- }
- }
- }
Followed
by today's de-sugared approach that ignores the semantic improvements
brought by ES.next modules over the module pattern and instead
emphasises our reliance of function variants:
- var widgets = (function(global) {
-
- function DropDownButton(attributes) {
- Widget.call(this, attributes);
- this.buildUI();
- }
- DropDownButton.prototype = Object.create(Widget.prototype, {
- constructor: { value: DropDownButton },
- buildUI: {
- value: function(e) {
- this.domNode.onclick = function(e) {
-
- }
- }
- }
- });
- })(this);
All the ES.next version does it makes the code more easy to read. What class
means here is function
,
or at least, one of the things we currently do with functions. If you
enjoy JavaScript and like using functions and prototypes, such sugar is
nothing to fear in the next version of JavaScript.
Where do these modules fit in with AMD?
If
anything, the landscape for modularization and loading of code on the
front-end has seen a wealth of hacks, abuse and experimentation, but
we've been able to get by so far.
Are ES.next modules a step in
the right direction? Perhaps. My own take on them is that reading their
specs is one thing and actually using them is another. Playing with the
newer module syntax in Harmonizr, Require HM and Traceur,
you actually get used to the syntax and semantics very quickly – it
feels like using a cleaner module pattern but with access to native
loader API for any dynamic module loading required at runtime. That
said, the syntax might feel a little too much like Python for some
peoples tastes (e.g the import
statements).
I'm part
of the camp that believe if there's functionality developers are using
broadly enough (e.g better modules), the platform (i.e the browser)
should be trying to offer some of this natively and I'm not alone in
feeling this way. James Burke, who was instrumental in bringing us AMD
and RequireJS has previously said:
I want AMD and
RequireJS to go away. They solve a real problem, but ideally the
language and runtime should have similar capabilities built in. Native
support should be able to cover the 80% case of RequireJS usage, to the
point that no userland "module loader" library should be needed for
those use cases, at least in the browser.
James has
however questioned whether ES.next modules are a sufficient solution. He
covered some more of his thoughts on ES.next modules back in June in ES6 Modules: Suggestions for improvement and later in Why not AMD? for anyone interested in reading more about how these modules fit in with RequireJS and AMD.
Isaac Schlueter has also previously written up thoughts on where ES6 modules fall short that are worth noting. Try them out yourself using some of the options below and see what you think.
Use it today
Object.observe()
The idea behind Object.observe
is that we gain the ability to observe and notify applications of
changes made to specific JavaScript objects. Such changes include
properties being added, updated, removed or reconfigured.
Property
observing is behaviour we commonly find in JavaScript MVC frameworks at
at the moment and is an important component of data-binding, found in
solutions like AngularJS and Ember.
This is a fundamentally
important addition to JS as it could both offer performance improvements
over a framework's custom implementations and allow easier observation
of plain native objects.
-
- var todoModel = {
- label: 'Default',
- completed: false
- };
-
- Object.observe(todoModel, function(changes) {
- changes.forEach(function(change, i) {
- console.log(change);
-
-
-
-
-
- });
- });
-
- todoModel.label = 'Buy some more milk';
-
-
-
-
-
- todoModel.completeBy = '01/01/2013';
-
-
-
-
-
- delete todoModel.completed;
-
-
-
-
-
Availability:
Object.observe will be available in Chrome Canary behind the "Enable
Experimental JS APIs" flag. If you don't feel like getting that setup,
you can also checkout this video by Rafael Weinstein discussing the proposal.
Use it today
Read more (Rick Waldron).
Default Parameter Values
Default
parameter values allow us to initialize parameters if they are not
explicitly supplied. This means that we no longer have to write options = options || {};
.
The syntax is modified by allowing an (optional) initialiser after the parameter names:
- function addTodo(caption = 'Do something') {
- console.log(caption);
- }
- addTodo();
Only trailing parameters may have default values:
- function addTodo(caption, order = 4) {}
- function addTodo(caption = 'Do something', order = 4) {}
- function addTodo(caption, order = 10, other = this) {}
Traceur demo
Availability: FF18
Block Scoping
Block scoping introduces new declaration forms for defining variables scoped to a single block. This includes:
let
: which syntactically is quite similar to var
, but defines a variable in the current block function, allowing function declarations in nested blocks
const
: like let
, but is for read-only constant declarations
Using let
in place of var
makes it easier to define block-local variables without worrying about
them clashing with variables elsewhere in the same function body. The
scope of a variable that's been declared inside a let
statement using var
is the same as if it had been declared outside the let
statement. These types of variables will still have function scoping.
- var x = 8;
- var y = 0;
- let (x = x+10, y = 12) {
- console.log(x+y);
- }
- console.log(x + y);
let
availability: FF18, Chrome 24+
const
availability: FF18, Chrome 24+, SF6, WebKit, Opera 12
Maps and sets
Maps
Many
of you will already be familiar with the concept of maps as we've been
using plain JavaScript objects as them for quite some time. Maps allow
us to map a value to a unique key such that we can retrieve the value
using the key without the pains of prototype-based inheritance.
With the Maps set()
method, new name-value pairs are stored in the map and using get()
, the values can be retrieved. Maps also have the following three methods:
has(key)
: a boolean check to test if a key exists
delete(key)
: deletes the key specified from the map
size()
: returns the number of stored name-value pairs
- let m = new Map();
- m.set('todo', 'todo'.length);
- m.get('todo');
- m.has('todo');
- m.delete('todo');
- m.has('todo');
Availability: FF18
Read more (Nicholas Zakas)
Use it today
Sets
As
Nicholas has pointed out before, sets won't be new to developers coming
from Ruby or Python, but it's a feature thats been missing from
JavaScript. Data of any type can be stored in a set, although values can
be set only once. They are an effective means of creating ordered list
of values that cannot contain duplicates.
add(value)
– adds the value to the set.
delete(value)
– sets the value for the key in the set.
has(value)
– returns a boolean asserting whether the value has been added to the set
- let s = new Set([1, 2, 3]);
- s.has(-Infinity);
- s.add(-Infinity);
- s.has(-Infinity);
- s.delete(-Infinity);
- s.has(-Infinity);
One possible use for sets is reducing the complexity of filtering operations. e.g:
- function unique(array) {
- var seen = new Set;
- return array.filter(function (item) {
- if (!seen.has(item)) {
- seen.add(item);
- return true;
- }
- });
- }
This
results in O(n) for filtering uniques in an array. Almost all methods
of array unique with objects are O(n^2) (credit goes to Brandon Benvie
for this suggestion).
Availability: Firefox 18, Chrome 24+
Read more (Nicholas Zakas)
Use it today
Proxies
The
Proxy API will allow us to create objects whose properties may be
computed at run-time dynamically. It will also support hooking into
other objects for tasks such as logging or auditing.
- var obj = {foo: "bar"};
- var proxyObj = Proxy.create({
- get: function(obj, propertyName) {
- return 'Hey, '+ propertyName;
- }
- });
- console.log(proxyObj.Alex);
Also checkout Zakas' Stack implementation using ES6 proxies experiment.
Availability: FF18, Chrome 24
Read more (Nicholas Zakas)
WeakMaps
WeakMaps
help developers avoid memory leaks by holding references to their
properties weakly, meaning that if a WeakMap is the only object with a
reference to another object, the garbage collector may collect the
referenced object. This behavior differs from all variable references in
ES5.
A key property of Weak Maps is the inability to enumerate their keys.
- let m = new WeakMap();
- m.set('todo', 'todo'.length);
-
- m.has('todo');
-
- let wmk = {};
- m.set(wmk, 'thinger');
- m.get(wmk);
- m.has(wmk);
- m.delete(wmk);
- m.has(wmk);
So again, the main difference between WeakMaps and Maps is that WeakMaps are not enumerable.
Use it today
Read more (Nicholas Zakas)
API improvements
Object.is
Introduces a function for comparison called Object.is
. The main difference between ===
and Object.is
are the way NaN
s and (negative) zeroes are treated. A NaN
is equal to another NaN
and negative zeroes are not equal from other positive zeroes.
- Object.is(0, -0);
- Object.is(NaN, NaN);
- 0 === -0;
- NaN === NaN;
Availability: Chrome 24+
Use it today
Array.from
Array.from
:
Converts a single argument that is an array-like object or list (eg.
arguments, NodeList, DOMTokenList (used by classList), NamedNodeMap
(used by attributes property)) into a new Array()
and returns it;
Converting any Array-Like objects:
- Array.from({
- 0: 'Buy some milk',
- 1: 'Go running',
- 2: 'Pick up birthday gifts',
- length: 3
- });
The following examples illustrate common DOM use cases:
- var divs = document.querySelectorAll('div');
- Array.from(divs);
-
- Array.from(divs).forEach(function(node) {
- console.log(node);
- });
Use it today
Conclusions
ES.next
is shaping up to potentially include solutions for what many of us
consider are missing from JavaScript at the moment. Whilst ES6 is
targeting a 2013 spec release, browsers are already implementing
individual features and it's only a matter of time before their
availability is widespread.
In the meantime, we can use (some)
modern browsers, transpilers, shims and in some cases custom builds to
experiment with features before ES6 is fully here.
For more examples and up to date information, feel free to checkout the TC39 Codex Wiki
(which was a great reference when putting together this post)
maintained by Dave Herman and others. It contains summaries of all
features currently being targeted for the next version of JavaScript.
Exciting times are most definitely ahead.