Contents


The busy JavaScript developer's guide to ECMAScript 6, Part 4

New objects and types in the standard library

Using modules, collections, proxies, and more in the new JavaScript

Comments

Content series:

This content is part # of 4 in the series: The busy JavaScript developer's guide to ECMAScript 6, Part 4

Stay tuned for additional content in this series.

This content is part of the series:The busy JavaScript developer's guide to ECMAScript 6, Part 4

Stay tuned for additional content in this series.

Over the last three articles, you've learned about some of the biggest changes to JavaScript with the ECMAScript 6 specification. If you've been following along, you've sampled a handful of syntactic changes, discovered the functional-like features of the new arrow functions, and experimented with using traditional class syntax in your JavaScript programs. If you're anything like me, you might be feeling pretty relieved to discover that the ECMAScript Technical Committee has managed to make significant changes to everyone's favorite 20-year-old scripting language, without sacrificing its ease of use or prototype-based object system.

This final article in the series introduces a handful of objects and types that are now part of the standard library. Some of these you've definitely used before, either in JavaScript or in other languages, while others might stretch your mind a bit—or even a lot. But they're all worthy additions that I'm betting will find their way into your toolkit over time.

Modules

For everyday programming, modules are likely the most obvious library enhancement to ECMAScript 6. Up till now, following the Node.js convention, we've requireed files using a named exports global variable object to describe the values returned. No longer! ECMAScript 6 uses import and export statements to formalize the concept of modules. As you might infer, export is used to declare named values (usually classes or functions, but sometimes variables) from an ECMAScript file, while import is used to pull in those exported names from that file into a different one.

Basically, if you have a file that you want to treat as a module—let's call it output—you'll simply export the symbols you want to be able to use elsewhere, like so:

Listing 1. Exporting an output function
    // output.js
    export function output() {
      console.log("OUT: ", arguments);
    }

Entering the keyword export before the function tells ECMAScript that this file is to be treated as a module. Thereafter, the function will be made available to any other file that imports it:

Listing 2. Importing output.js
    import { out } from 'output.js';
    out("I'm using output!");

You might have noticed that the import syntax suffers from one major flaw: in order to use the module, you have to know all the names you wish to import. (Though some might consider this a benefit, because it ensures that no symbols will be imported without the importer's knowledge.) If you want to grab all the exported names from a module, you can use the wildcard (*) import syntax, but then you need to define a module name where you want to scope them:

Listing 3. Exporting with wildcard
    import * as Output from 'output.js';
    Output.out("I'm using output!");

Requiring the module name enforces a scoping mechanism; if two modules each exported an out without the module name, the newer one would silently replace its predecessor. Each name entered is wrapped in the module name, further reducing ambiguity.

Symbols

One of the subtler features introduced with ECMAScript 6 is the new Symbol type. On the surface, it seems rather unremarkable: essentially, an instance of Symbol is a unique name that cannot be duplicated anywhere else. That's it.

Recall that an ECMAScript object is just a collection of name-value pairs, where the value can be either data (strings, numbers, object references, and so on) or behavior (in the form of a function reference). Normally, if you know the name of the thing, you can get at its value, no questions asked.

In some situations, however, the owner of an object needs to be able to ensure that a chosen name doesn't clash with other names. In this case, instead of using a traditional String-based name, you could use a Symbol instance. Symbol ensures the names won't clash.

As an example, let's start with a typical Person type:

Listing 4. A Person object
    class Person {
      constructor(firstName, lastName, age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
      }
    };
    var p = new Person("Fred", "Flintstone", 45);
    console.log(p["firstName"]);
    for (let m in p) {
      console.log(m, "=", p[m]);
    }

The three object fields are easily accessible to anyone who knows their names. The names are accessible via simple iteration across the contents of the object. While ECMAScript has never been known as a high-security language, this example definitely fails even basic levels of encapsulation.

Using Symbol for access control

Suppose you needed to keep some fields hidden? You could start by making them accessible through Symbol names, rather than standard strings:

Listing 5. Symbol replaces String
    var firstNameS = Symbol("firstName");
    var lastNameS = Symbol("lastName");
    var ageS = Symbol("age");

    class Person {
      constructor(firstName, lastName, age) {
        this[firstNameS] = firstName;
        this[lastNameS] = lastName;
        this[ageS] = age;
      }
    };
    var p = new Person("Fred", "Flintstone", 45);
    console.log(p["firstName"]); // "undefined"
    for (let m in p) {
      console.log(m, "=", p[m]);
    }

    p.firstName = "Barney";
    console.log(p["firstName"]);

    console.log(p[firstNameS]); // "Fred"

As shown, you can use the Symbol function to create instances of a Symbol. Each of those instances can then be used as the name on the objects in question. If someone tried to access the field using a normal String-based name like firstName, the results would be undefined, because the data doesn't live under that name anymore. Under the new specification, JavaScript won't even show Symbol-based names during standard object iteration. Any attempt to use traditional reflection across the object would essentially fail.

It's also important to note that if someone wanted to add new members to the object from the outside (an example of metaobject programming), the use of the string firstName wouldn't conflict or replace the existing member. This is crucial for libraries and frameworks where it's necessary to add additional behavior or members to existing objects—which is almost every modern framework in use right now.

However, as the last line in Listing 5 shows, if the caller has the Symbol instance, it can be used to access the data just as before, with no hesitation. Unlike the private keyword in other languages, Symbol doesn't quite cut it for enforcing access control.

Member names

JavaScript supports a number of well-known member names, which are useful for creating objects that follow environment-specific patterns. An example would be iterator, which you can use to name functions on objects supporting iteration behavior. If you wanted to create a Fibonacci-generating object that masqueraded as a standard ECMAScript iterator, you would need to create an object with an iterator function on it. Because anybody could be using that name, however, ECMAScript 6 insists that you use the iteratorSymbol instead:

Listing 6. Symbol.iterator
    let fibonacci = {
      [Symbol.iterator]: function*() {
        let pre = 0, cur = 1;
        for (;;) {
          let temp = pre;
          pre = cur;
          cur += temp;
          yield cur;
        }
      }
    }

    for (let n of fibonacci) {
      // truncate the sequence at 1000
      if (n > 1000)
        break;
      console.log(n);
    }

Again, Symbol's primary function is to help programmers avoid name clashes across libraries. It's a little awkward to grasp at first, but try thinking of Symbol as a unique hash based on the string name it's fed.

Collection types

If you've been using ECMAScript for more than 10 minutes you know that the language supports arrays—it's been a core part of the specification since 1.0. Despite their limitations (fixed-size most of all), arrays have served us well; they'll likely continue to do so for years to come.

But it's time to admit something, just among us, even if we never would say it to anyone else: Arrays . . . can't do everything.

To help pick up the slack, ECMAScript 6 adds two collection types to the standard JavaScript environment: Map and Set.

A Map is a set of name/value pairs, much as ECMAScript objects are. The difference is that a Map contains methods that make it easier to work with than a raw ECMAScript object would be:

  • get() and set() find and set key/value pairs, respectively
  • clear() will empty the collection entirely
  • keys() returns an iterable collection of the keys in the Map
  • values() does the same for the values

Also, like Array, Map includes functional-language-inspired methods like forEach(), which operate on the Map itself.

Listing 7. A Map at work
    let m = new Map();
    m.set("key1", "value1");
    m.set("key2", "value2");
    m.forEach((key, value, map) => {
      console.log(key,"=",value," from ", map);
    })
    console.log(m.keys());
    console.log(m.values());

Set, meanwhile, looks more like a traditional object collection, in that objects can simply be added to the collection. But Set will examine each object in turn to ensure that it is not a duplicate of a value already present within the collection:

Listing 8. Set at work
    let s = new Set();
    s.add("Ted");
    s.add("Jenni");
    s.add("Athen");
    s.add("Ted"); // duplicate!
    console.log(s.size); // 3

Like Map, Set has methods on it that allow for functional-style interaction, such as forEach. Fundamentally, Set is like an array, but without the angle brackets. It grows dynamically, and it lacks any sort of ordering mechanism. Using Set, you can't look up an object by index, as you could with an array.

Weak references

ECMAScript 6 also introduces WeakMaps and WeakSets, which are Maps and Sets that hold their values through weak references, rather than the traditional strong reference. The ECMAScript specification best describes what happens to objects held inside the WeakMap; the explanation applies equally to WeakSet:

If an object that is being used as the key of a WeakMap key/value pair is only reachable by following a chain of references that start within that WeakMap, then that key/value pair is inaccessible and is automatically removed from the WeakMap.

I won't get into the utility of WeakMaps and WeakSets in this article. They'll mainly be used for library code (particularly with respect to caching), and won't likely show up much in application code.

Promises

Asynchronous operations are a core part of the Node.js story (usually under the tagline of event-driven programming) but they've never been easy to implement. At first, the Node.js community seemed to settle on using event subscriptions, but over time developers have migrated to a more callback-driven style. This has brought us the now-dreaded callback hell, wherein Node.js code seems to "march" across the screen:

Listing 9. This is callback hell
    fs.readdir(source, function (err, files) {
      if (err) {
        console.log('Error finding files: ' + err)
      } else {
        files.forEach(function (filename, fileIndex) {
          console.log(filename)
          gm(source + filename).size(function (err, values) {
            if (err) {
              console.log('Error identifying file size: ' + err)
            } else {
              console.log(filename + ' : ' + values)
              aspect = (values.width / values.height)
              widths.forEach(function (width, widthIndex) {
                height = Math.round(width / aspect)
                console.log('resizing ' + filename + 'to ' + height + 'x' + height)
                this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
                  if (err) console.log('Error writing file: ' + err)
                })
              }.bind(this))
            }
          })
        })
      }
    })

Note that for the above example I used an indentation set to two spaces. Imagine the scrolling for developers who insist on four- or eight-space indentations.

After much wailing and gnashing of teeth, the ECMAScript community floated an alternative to callbacks in asynchronous computation: the now standard Promise type.

Using a Promise in JavaScript is two-fold. First, whoever builds the code to "go off and do something" (as shown in Listing 9) will now return Promises rather than using the traditional synchronous execution or the Node.js callback idiom. This enables the caller to use a Promise's then() method to chain sequential calls and catch() to define what should happen in the event of failure:

Listing 10. Asynchronous calling
    doSomething.then(function(result) {
      console.log(result); // It did something
    }).catch(function(err) {
      console.log(err); // Error
    });

On the doSomething() side, the code can now be written to yield a Promise instance, usually by wrapping a Promise around the function doing the asynchronous execution:

Listing 11. Asynchronous doing
    let promise = new Promise(function(resolve, reject) {
      let result = // do a thing, usually async in nature

      if (success) {
        resolve(result);
      }
      else {
        reject(Error("It didn't work"));
      }
    });

We're clearly in "your mileage will vary" territory here. In my experience developers will use Promises handed back from libraries, so I expect that most will start by using rather than constructing them. Over time, more developers will likely build their own Promises for use by other modules.

There's more to be said about Promises, but it will have to happen in another article. Fortunately for those who don't want to use a thing until they've explored it in depth, raw callbacks and events aren't going away, so you don't need to adopt this feature right away.

Dynamic proxies

JavaScript programming with dynamic proxies was already popular, but ECMAScript 6 standardized the new Proxy type. Having a standardized method helps us avoid accidental clashes and/or confusion across libraries. In essence, a Proxy implements "interception" behavior, enabling one object to stand in front of another. The intercepting object then gets first crack at any method calls or property references intended for the original target.

Being able to replace an object's method with another definition is nothing new for ECMAScript, but the Proxytype does so much more. It can even intercept requests that don't exist on a target object—think method calls, property references, and the like.

An example is worth a thousand words, so let's spin up some code. It's conventional to use method-call logging to demonstrate the power of proxies, so I'll follow along. First, here's Person, a plain ol' ECMAScript object—a POESO, if you will:

Listing 12. You say Person, I say POESO
    let ted = new Person("Ted", "Neward", 45);

I'll stick a few methods of interest on my Person after construction, just for fun. Adding these here will also show that dynamic proxies can work with any ECMAScript object, regardless of how it was constructed or defined:

Listing 13. sayHowdy?
    ted.sayHowdy = function() {
      console.log(this[firstNameS] + " says howdy!");
    };
    ted.waveGoodbye = function(msg) {
      return "" + msg + " Buh-bye!";
    };
    ted.sayHowdy();
    console.log(ted.waveGoodbye("See you next time!"));

We now have two methods: one taking no parameters, and the other taking one parameter and returning a result. By themselves, they're not all that interesting, but they'll do to represent the methods we want to trap. Each time one of these methods is invoked, we want to see a message in the console that says "method invoked," ideally along with some interesting information about the method call.

While we're at it, wouldn't it be nice to be able to see when properties are accessed? We could use this feature to either retrieve the value or set it. Proxies can do that, as well as intercepting constructor invocations and other less commonly used invocations. In essence, a proxy can intercept anything you can do to an object, giving you the opportunity to do something else, like slip in some additional behavior.

Using proxies in ECMAScript 6

To set up a proxy, we first need to identify the target, also known as the object whose methods or properties we want to intercept. In this case, we'll intercept the Person object from Listing 12. Next, we need to identify the behavior we want to insert between the caller and target. In ECMAScript parlance, this is called the handler. Each handler defines one or more methods, as shown below.

Listing 14. Handler methods
    ** get(): for getting property values
    ** set(): for setting property values
    ** apply(): for a function call
    ** construct(): for the new operator
    ** has(): for the in operator
    ** ownKeys(): for the Object method getOwnPropertyNames()
    ** getPrototypeOf(), setPrototypeOf(): for the Object methods of the same name
    ** isExtensible(), preventExtensions(): for the Object methods of the same name
    ** defineProperty(), getOwnPropertyDescriptor(): for the Object methods of the same name
    ** deleteProperty(): for the delete operator

We'll start by trapping any requests to get and set properties on the Person (target) object. We do that by creating a handler object that provides methods named get and set, respectively:

Listing 15. A handler object
    let handler = {};
    handler.get = function(target, propName) {
      console.log("Handler.get() invoked",
        target,"returning value for",propName);
      return target[propName];
    }
    handler.set = function(target, propName, newValue) {
      console.log("Hander.set() invoked",
        target,"changing",target[propName],"to",newValue);
      target[propName] = newValue;
    }

Notice how the set handler not only logs the message to the console, but also does the work of property assignment. This is necessary because the handler is completely in control: if you don't assign the property to the target, it won't be set. The same is true for the get handler, which must return the target's property value. If you don't assign the property, nothing (or, rather, undefined) will be the only property returned.

The last step is to wire up a Proxy object around the target and the handler. In Listing 16 we capture the Proxy object back into the original variable.

Listing 16. A Person proxy
    ted = new Proxy(ted, handler);

In certain scenarios, you'll want to hold on to the original, so you can access the target without having to go through an interceptor. Most of the time, however, you'll use the Proxy as a silent processor, such that clients using the target object don't even realize there's anything between them and the target.

If you added waveGoodbye and sayHowdy to the object after the handler was in place, the handler would be invoked for the property-set operations. This is because waveGoodbye and sayHowdy are technically properties of the function type. If you run this code, you'll see the following:

Listing 17. Proxy output
    Hander.set() invoked Person {} changing undefined to function () {
      console.log(this[firstNameS] + " says howdy!");
    }
    Hander.set() invoked Person { sayHowdy: [Function] } changing undefined to function (msg) {
      console.log(msg, "Buh-bye!");
    }
    Handler.get() invoked Person { sayHowdy: [Function], waveGoodbye: [Function] } returning value for sayHowdy
    Handler.get() invoked Person { sayHowdy: [Function], waveGoodbye: [Function] } returning value for Symbol(firstName)
    Ted says howdy!
    Handler.get() invoked Person { sayHowdy: [Function], waveGoodbye: [Function] } returning value for waveGoodbye
    See you next time! Buh-bye!

Note that the get() handler is invoked. Accessing the method means obtaining that method (so as to invoke it) and then (in the case of sayHowdy) obtaining the values for any properties referenced in that method.

A proxy handler on function

To be clear, the get handler is always being invoked, regardless of how the property was defined. This would be true even if we'd defined a method on the above Person class, like so:

Listing 18. A new Person method
    class Person {
      // ... as before
      eat() {
        console.log("Chomp chomp");
      }
    }

If the Person-typed object has its eat() method invoked, ECMAScript's first task is to resolve the property name "eat" as a property that yields a function. First it will first obtain that function, then immediately invoke it. If we want to see more details about the function being invoked, we'll need to slip a new handler into the middle of the invocation process, after the function has been located and returned. The easiest way is to return a function that wraps the original function:

Listing 19. Introducing a new handler
    handler.get = function(target, propName) {
      let original = target[propName];
      if (typeof original === 'function') {
        return (...args) => {
          console.log("Executing", propName, "with", args);
          const result = original.apply(target, args);
          console.log(">>> Returns",result);
          return result;
        }
      }
      else {
        const result = target[propName];
        console.log("Property",propName,"holds", result);
        return result;
      }
    }

The procedure might look complicated, but it's really not. If the property being accessed is something other than a function, just get the result and return it. If the property is a function, create a function literal and return that instead. The function literal returned will invoke the original function. Using ECMAScript's apply method on function objects ensures that we bind the right this into place. (If we used this literally, instead of target, the this would be the handler, not the target.)

Easy-peasy, right?

Actually, if you've never seen this type of code before, it's pretty mind-blowing. Using Proxy, you can do things like type-safe property validation (write a handler that ensures that values being set are of the right type for a given property); remote execution (return a proxy that knows how to make remote calls via an HTTP API, serializing the arguments into a JSON array and deserializing the results); or even putting in authorization boundaries (wrapping a domain object with a proxy that will check a given user's credentials in memory). Formally, all of these uses fall under the heading of aspect-oriented programming. Together they introduce a whole new world of thinking about how to capture concerns in JavaScript.

Conclusion

ECMAScript 6 is by far the most ambitious revision of JavaScript to date, which inevitably requires an adjustment period. ECMAScript interpreters haven't all caught up to the specification yet. Don't be surprised if your code sometimes fails; just check your interpreter to see what's not supported and adjust your code as needed.

Remember, too, that not all is lost if your code won't compile: you can use one of the popular Node.js transpilers to tame your code into a somewhat less-bleeding-edge ECMAScript.

The beautiful thing is how the ECMAScript Technical Committee has pushed the language forward, while still enabling considerable backward compatibility. Because of this you can take a baby steps approach to adopting ECMAScript 6: just pick a feature you like and integrate it into your code. Once you're comfortable with that, you can pick up the next thing you want to try, and so on. You never have to wade in deeper than you—or your productivity—can handle, but you are encouraged to keep going forward. Gradually, you can start taking advantage of the many powerful new features and conventions that are part of standard JavaScript.

That wraps this series, so without further ado, I'll simply say . . .

    return "Enjoy!";

Catch you next time!


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development
ArticleID=1040642
ArticleTitle=The busy JavaScript developer's guide to ECMAScript 6, Part 4: New objects and types in the standard library
publish-date=12082016