My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Tuesday, July 09, 2013

Some JS Descriptor Trick

I am back home for 20 minutes after 10 days of vacation in Italy and already bored so which better way than talk about some wizardish trick about JS descriptors? (yes, they were showing again the great wizard of OZ in the United Airline flight from Frankfurt ... thanks for wondering...)

The Recycling Trap

Descriptors can be recycled without problems and reused to define same things here and there. However, there is an undesired side effect about recycled descriptors: these works with constant/static values, getters, or setters, but are unable to change behavior in their lifetime.
var propertiesDescriptor = {
  shared: {
    value: Math.random()
  }
};
Above descriptor is just a basic example where Object.defineProperties({}, propertiesDescriptor) will pollute the empty object with always the same random value.
var a = Object.defineProperties({}, propertiesDescriptor),
    b = Object.defineProperties({}, propertiesDescriptor);
a.shared === b.shared; // true !

Describe A Descriptor With Descriptors

This sounds like a descriptorception and it's actually exactly that one: a property described as property descriptor, able to be different every time the descriptor is used to define properties.
Here how we could enrich the propertiesDescriptor object in order to have a runtime property too.
Object.defineProperty(
  propertiesDescriptor,
  'runtime',
  {
    enumerable: true,
    // a getter is required
    get: function () {
      // so that every time the object
      // is used as properties descriptor
      // this returned value will be used
      // as "runtime" descriptor instead
      return {
        value: Math.random()
      };
    }
  }
);
We can perform the check one mor time now against same code.
var a = Object.defineProperties({}, propertiesDescriptor),
    b = Object.defineProperties({}, propertiesDescriptor);
a.shared === b.shared; // true !
a.runtime !== b.runtime; // true again, hooray!

Non Scalar Only Values: Achievement Unlocked

This is pretty much what we have achieved with latest trick: the possibility to recycle and reuse a descriptor being sure this will hold all static/scalar/constant properties as we wanted, and also create new objects, methods, or features, each time the same descriptor is used to define one or more property.

A Basic Counter Example

Let's say we'd like to know how many times the same properties descriptor has been used during a program lifecycle, you know what I mean ? All together:
var propertiesDescriptor = {
  shared: {
    value: Math.random()
  }
};

Object.defineProperty(
  propertiesDescriptor,
  'runtime',
  {
    enumerable: true,
    get: function () {
      this.__count__++;
      return {
        value: Math.random()
      };
    }
  }
);

Object.defineProperty(
  propertiesDescriptor,
  '__count__',
  {
    writable: true,
    value: 0
  }
);

var a = Object.defineProperties({}, propertiesDescriptor),
    b = Object.defineProperties({}, propertiesDescriptor);

alert([
  a.shared,  // 0.1234
  b.shared,  // 0.1234
  a.runtime, // 0.5678
  b.runtime  // 0.8901
].join('\n'));

alert(propertiesDescriptor.__count__); // 2

Lazy Value But Not Lazy Property

The last example is about creating a descriptor with a specific property that will create a new value once the descriptor has been used with a generic object.
This time is about creating a lazy property value only when accessed through the extended object and not through the property name descriptor ... right ?
var propertiesDescriptor = {
  shared: {
    value: Math.random()
  },
  lazy: {
    configurable: true,
    get: function () {
      return Object.defineProperty(
        this,
        'lazy',
        {
          value: []
        }
      ).lazy;
    }
  }
};

// OR
Object.defineProperty(
  propertiesDescriptor,
  'lazy',
  {
    enumerable: true,
    value: {
      configurable: true,
      get: function () {
        return Object.defineProperty(
          this,
          'lazy',
          {
            value: []
          }
        ).lazy;
      }
    }
  }
);
At this point the object, let's say a generic constructor.prototype, will be described as shared accessor so that each instance, together with the prototype itself, could redefine that property only when accessed.
This is in theory better for memory usage and GC operations but hey ... I've said since the beginning these were just tricks, isn't it?
Sim Sala Bim!

2 comments:

rauschma said...

Have you compared this performance-wise with not reusing? Probably faster, but may not be or not much.

Andrea Giammarchi said...

not yet but it's evident that Garbage Collector wil have less work to do and RAM will be slightly more preserved.

I expect the function invocation to be almost as fast as new object creation thought ... a test might come pretty soon, good point.