javascript-decorators icon indicating copy to clipboard operation
javascript-decorators copied to clipboard

Instance level decorators

Open megawac opened this issue 10 years ago • 9 comments

Under this proposal there doesn't seem possible to define a decorator that is instance specific. Should the specification define a way to make decorators be applied at class instantiation time?

Instance level decorators could be powerful (see example below and others such as @debounce, @throttle, etc) and I think logically more sense in many cases. On the other hand, decorators become more than simply sugar and harder to ES5ify. A possible implementation may be through having this addressed in a WeakMap or wrapping the constructor

Example

function once(target, name, descriptor) {
  let {get} = descriptor;
  var called = false, result;
  descriptor.get = function() {
    if (!called) {
        result = get.apply(this, arguments);
        called = true;
    }
    return result;
  }
  return descriptor;
}

let x = 0;

class X {
    @once
    get y() {
      return ++x;
    }
}

var x1 = new X;
var x2 = new X;

console.log(x1.y, x2.y) // What should x2 be

megawac avatar Apr 22 '15 23:04 megawac

:+1:

I think class methods can be defined on prototype. But with JavaScript there's not real way to say whether a function is a class method or property of type function.

I think it is best to leave this option to the programmer.

@@once // defined on prototype
@once // defined on instance

ming-codes avatar May 15 '15 17:05 ming-codes

I found a way to get instance level decorators. Here's an example:

import { memoize } from 'lodash';

function decorator(target, name, descriptor) {
  const { value, get } = descriptor;

  return {
    get: function getter() {
      const newDescriptor = { configurable: true };

      // If we are dealing with a getter
      if (get) {
        // Replace the getter with the processed one
        newDescriptor.get = memoize(get);

        // Redefine the property on the instance with the new descriptor
        Object.defineProperty(this, name, newDescriptor);

        // Return the getter result
        return newDescriptor.get();
      }

      // Process the function
      newDescriptor.value = memoize(value);

      // Redfine it on the instance with the new descriptor
      Object.defineProperty(this, name, newDescriptor);

      // Return the processed function
      return newDescriptor.value;
    }
  };
}

Not the easiest solution but you could create a simple factory for these ;)

steelsojka avatar May 22 '15 13:05 steelsojka

I'm also interested to apply the decorator when the class is instantiated, so we can get access to the instance it self (i.e. this).

For example, let's say we want to register a class member to an event:

import {registerEventListener} from 'some-event-system';

var triggerOnEvent = function(eventName) {
    return function(target, name, descriptor) {
        registerEventListener(eventName, descriptor.value.bind(target));
    }; 
};

class Foo {

    constructor() {
        this.baz = 'baz';
    }

    @triggerOnEvent('update')
    bar() {
        console.log(this.baz);
    }

}

I want the above code to desugaring to (ES5):

var Foo = (function () {
  function Foo() {
    this.baz = 'baz';

    // The decorator is executing at instantiation time
    var _temp;
    // target sets to this, instead of Foo.prototype
    _temp = triggerOnEvent("update")(this, "bar", Object.getOwnPropertyDescriptor(Foo.prototype, "bar")) || _temp;
    if (_temp) Object.defineProperty(Foo.prototype, "bar", _temp);

  }

  Foo.prototype.bar = function () { console.log(this.baz); }
  return Foo;
})();

It would be nice to distinguish these two use cases (design time and instantiation time), maybe as @lightblade suggested. I believe it exist many use cases that will use decorators both at design time and at instantiation time.

tjoskar avatar Jul 13 '15 09:07 tjoskar

+1 this. I implemented something that works but is more or less a hack here https://github.com/steelsojka/lodash-decorators and doesn't cover all the scenarios.

Instance decorators make sense for things like debounce and throttle methods.

steelsojka avatar Aug 24 '15 13:08 steelsojka

@steelsojka With your workaround is it possible to make a decorator that simply sets a variable on a class instance, without being bound to a function (I'd suggest decorating the constructor to do this but that is apparently not allowed)?

[edit] I did realize a bit later that you can just set the variable on the prototype in the decorator like so:

export const myDecorator = (data) => (target) => { return target.prototype.data = data), target; };

@myDecorator(myData)
export class MyClass {}

But I don't know if that's the desired way to do it.

seiyria avatar Sep 25 '15 18:09 seiyria

@seiyria the trick is getting it to apply to the instance. Applying anything to the prototype is simple, but applying decorators to the instance requires some work arounds. For instance properties 'class properties' is what you would use.

class MyClass {
  data = {};

  constructor() {
    console.log(this.data); //=> {}
  }
}

steelsojka avatar Oct 26 '15 15:10 steelsojka

@tjoskar I do not think that this is possible without being able to intercept the new XYZ() and then applying instance level decorators on that instance before it is returned by the runtime.

My best bet would be to move the registration of these events to the constructor, or use a class decorator, e.g. something along the line of

var triggerOnEvent = function(eventName, methodName) {
    return function(target) {
        const result = class {
            constructor(...args) {
               // TODO:classCallCheck
               registerEventListener(eventName, target.prototype[methodName].bind(this));
               super(...args);
            }
          };
          // TODO: make result inherit from target
          Object.setPrototypeOf(result, target);
          return result;
    }; 
};

@triggerOnEvent('foo', 'bar')
class Foo {
   bar() {}
}

See for example the @abstract decorator in corejs/core-decorators.js for how to inject a class inheriting from target.

@tjoskar alternatively you might want to consider the decoratedClassFactory over at pingo-common which allows you to provide your own function to be called upon instantiation/construction of a decorated class.

silkentrance avatar Feb 28 '16 21:02 silkentrance

@megawac While some extra indirection can give you instance level decorations, you are actually using a shared memory state x. So you will have to indirect accesses to x as well in order for all your instances to see their own personal state of x.

@steelsojka Since decorators are mainly meant for design time decoration, but might actually be used during runtime as well, they affect only the class or its prototype. And while one can use decorators also to decorate individual instance methods a/o properties, the decorator specification does not provide for any abstraction of shared state or memory as it is out of scope.

It is up to the application/framework to provide for such things.

silkentrance avatar Mar 26 '16 20:03 silkentrance

I implemented a light library in es5 to have decorators at instance scope (called when new instance is created) and decorators which are called at join points when some method is called.

It supports even chained asynchronous operations.

If you could please take a look https://github.com/ciroreed/kaop/blob/easy-decorators/test/showcase.js

k1r0s avatar Feb 03 '17 10:02 k1r0s