2013-06-14

ECMAScript 6: automatically binding extracted methods

This blog post demonstrates how to use ECMAScript 6 proxies to automatically bind methods that are extracted from an object.

The problem with extracting methods

It’s a common JavaScript gotcha: If you extract a method from an object, it becomes a normal function. It loses the connection to the object and acessing properties via this doesn’t work, any more. Let’s look at an example:
    var jane = {
        name: 'Jane',
        describe: function() {
            'use strict'; // (*)
            return "I’m "+this.name; // (**)
        }
    };
In line (*), we are switching on strict mode, which means that this in line (**) is undefined when describe is called as a function:
    > jane.describe()
    'I’m Jane'
    > var func = jane.describe;
    > func()
    TypeError: Cannot read property 'name' of undefined
The solution is to use Function.prototype.bind():
    > var func2 = jane.describe.bind(jane);
    > func2()
    'I’m Jane'

Adding and removing event listeners

One additional challenge is that bind produces a new function each time it is called. That poses a problem if you are, e.g., registering and unregistering even listeners. For example:
    domElement.addEventListener(
        'click', myWidget.handleClick.bind(myWidget));
The event listener that has been added above is not removed via the following code:
    domElement.removeEventListener(
        myWidget.handleClick.bind(myWidget)); // Does not work!
The reason is that we try to remove something different from what we have added. The solution is to store what we have added:
    var listener = myWidget.handleClick.bind(myWidget);
    domElement.addEventListener('click', listener);
    ...
    domElement.removeEventListener(listener);

Binding automatically

So far, we have only used ECMAScript 5, now we are entering ECMAScript 6 territory. We’ll make use of several ECMAScript 6 features. We initially won’t use classes [1], to demonstrate how the basics work. The next section explains how to use auto-binding with classes.

Thanks to a recent decision by TC39, ECMAScript 6 proxies allow us to fix the problem. We control access to methods via a proxy: If a method is called, the proxy is notified via invoke() and ignores the notification (meaning that the method call will progress normally). If, however, the method’s property is read, the proxy intercepts and binds the method. The following function wraps a proxy around an object.

    function autoBind(obj) {
        const handler = {
            // Ignore: invoke(target,key,args,receiver)

            /**
             * @param target   Same as obj.
             * @param receiver Left-hand side of dot operator.
             */
            get(target, key, receiver) {
                const result = target[key];  // (*)
                if (typeof result !== 'function') {
                    return result;
                }
                return result.bind(receiver); // (**)
            }
        };
        return Proxy(obj, handler);
    }
The wrapped object returned by autoBind may not be the first member in a chain of prototype objects. Then it only receives a get notification if the property hasn’t been found in a prior object. Thus, the result (*) must come from target. In contrast, this must be bound to receiver (**), so that the bound method has access to properties of prior objects.

To reduce memory consumption, it is best to wrap the proxy around the instance prototype [2] of a constructor. For example:

    function Person(name) {
        this.name = name;
    }
    Person.origProto = {
        constructor: Person,
        describe() {
            return `I’m ${this.name}`;
        }
    };
    Person.prototype = autoBind(Person.origProto);

    let jill = new Person('Jill');
Then the wrapped object comes second in a chain of prototypes:
    jill → wrapped(Person.origProto → Object.prototype)

Sub-constructors

Creating a sub-constructor now becomes (even) more complicated: In addition to being wrapped itself, the sub-prototype must extend the original super-prototype (not the wrapped object).
    function Employee(name, title) {
        super(name);
        this.title = title;
    }
    Employee.origProto = {
        __proto__: Person.origProto,
        constructor: Employee,
        describe() {
            return `${super()} (${this.title})`;
        }
    };
    Employee.prototype = autoBind(Employee.origProto);

    let john = new Employee('John', 'CFO');
Now the chain of prototypes looks like this:
    john → wrapped(Employee.origProto → Person.origProto → ...)
There is one disadvantage with this approach: instanceof works for direct instances, but not for indirect ones. That is:
    > jill instanceof Person
    true
    > john instanceof Employee
    true
    > john instanceof Person  // should be true
    false
We will fix that later, when we use classes.

Caching bound methods

Currently, each read access to a method creates a new bound method. We can help with registering event listeners and similar use cases by caching the returned results. We need to cache those results per receiver, because the same wrapped object may be the prototype of multiple instances.
    function autoBind(obj) {
        // instance → { key → function }
        const cachedMethodMap = new WeakMap();
        const handler = {
            /**
             * @param key Can be a string or a symbol.
             */
            get(target, key, receiver) {
                let cachedMethods = cachedMethodMap.get(receiver);
                if (cachedMethods === undefined) {
                    cachedMethods = Object.create(null);
                    cachedMethodMap.set(receiver, cachedMethods);
                }
                if ({}.hasOwnProperty.call(cachedMethods, key)) {
                    return cachedMethods[key];
                }
                const result = target[key];
                if (typeof result !== 'function') {
                    return result;
                }
                const boundResult = result.bind(receiver);
                cachedMethods[key] = boundResult;
                return boundResult;
            }
        };
        return Proxy(obj, handler);
    }
Due to ECMAScript 6’s WeakMap, caching won’t prevent the receivers from being garbage-collected.

Auto-binding in use

Let’s assume that myWidget2 is an instance whose prototype has been created by the last version of autoBind, above. You can use it like this:
    domElement.addEventListener('click', myWidget2.handleClick);
    ...
    domElement.removeEventListener(myWidget2.handleClick);
You don’t need to bind() the method and you can remove it without remembering the value that you have registerd via addEventListener.

Auto-binding and classes

Auto-binding a class would work as follows (obviously, you would write a utility function for this).
    class Person {
        constructor(name) {
            this.name = name;
        }
        describe() {
            return `I’m ${this.name}`;
        }
    }
    Person.origProto = Person.prototype;
    Person.prototype = autoBind(Person.origProto);

    class Employee extends Person.origProto {
        constructor(name, title) {
            super(name);
            this.title = title;
        }
        describe() {
            return `${super()} (${this.title})`;
        }
    }
    Employee.origProto = Employee.prototype;
    Employee.prototype = autoBind(Employee.origProto);

A better solution

André Bargull suggested another solution. The following is a modified version of that solution.
    // Import symbols
    import { create, hasInstance } from '@reflect'; // invented name

    class BoundClass {
        // Sub-classes are sub-prototypes of this class
        // => `this` refers to class that is operand of `new`
        static get _boundProto() {
            if (this === BoundClass) {
                throw new Error('BoundClass can’t be used directly!');
            }
            // Override the getter in the current class
            this._boundProto = autoBind(this.prototype);
            return this._boundProto; // overridden version
        }
        static [create]() {
            return Object.create(this._boundProto);
        }
        static [hasInstance](inst) {  // Fix instanceof
            // Direct instances
            if (this !== BoundClass && 
                Object.getPrototypeOf(inst) === this._boundProto) {
                return true;
            }
            // All other cases
            return super(inst);
        }
    }
You can use BoundClass like this:
    class Person extends BoundClass {
        ...
    }
    class Employee extends Person {
        ...
    }
In ECMAScript 6, we can customize instance allocation via the class method @@create [3]. Its name is a public symbol that we need to import from a standard module (whose actual name has yet to be determined). Now we don’t change the instance prototypes, any more. Instead, we allocate instances whose prototypes are auto-binding versions of the instance prototypes. As a result, subclassing works without the weird extends Person.origProto. And instanceof now works for indirect instances, but not for direct instances. Thankfully, we can fix that via the @@hasInstance method.

Conclusion

The approach for auto-binding presented here is only a proof of concept. Performance issues (hidden classes etc.) will probably make it unsuitable in practice. Caching, however, is not a performance issue, because methods will rarely be accessed via “get”.

As an aside, Python always auto-binds methods. Maybe JavaScript can provide built-in support for it in the future.

References

  1. ECMAScript.next: classes
  2. JavaScript terminology: the two prototypes
  3. Subtyping JavaScript builtins in ECMAScript 5

No comments: