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 undefinedThe solution is to use Function.prototype.bind():
> var func2 = jane.describe.bind(jane); > func2() 'I’m Jane'
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);
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)
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 falseWe will fix that later, when we use classes.
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.
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.
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);
// 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.
As an aside, Python always auto-binds methods. Maybe JavaScript can provide built-in support for it in the future.