cortex

Property Encapsulation

In JavaScript it is not possible to create properties that have limited or controlled accessibility. It is possible to create non-enumerable and non-writable properties, but still they can be discovered and accessed. Usually so called "closure capturing" is used to encapsulate such properties in lexical scope:

function Foo() {
  var _secret = 'secret';
  this.hello = function hello() {
    return 'Hello ' + _secret;
  }
}

This provides desired result, but has side effect of degrading code readability, especially with object-oriented programs. Another disadvantage with this pattern is that there is no immediate solution for inheriting access to the privates (illustrated by the following example):

function Derived() {
  this.hello = function hello() {
    return _secret;
  }
  this.bye = function bye() {
    return _secret;
  }
}
Derived.prototype = Object.create(Foo.prototype);

Facade Objects

Alternatively constructor can returned facade objects - proxies to the instance's public properties:

function Foo() {
  var foo = Object.create(Foo.prototype);
  return {
    bar: foo.hello.bind(foo);
  }
}
Foo.prototype._secret = 'secret';
Foo.prototype.hello = function hello() {
  return 'Hello ' + this._secret;
}

function Derived() {
  var derived = Object.create(Derived.prototype);
  return {
    bar: derived.hello.bind(derived);
    bye: derived.bye.bind(derived);
  }
}
Derived.prototype = Object.create(Foo.prototype);
Derived.prototype.bye = function bye() {
  return 'Bye ' + this._secret;
};

While this solution solves given issue and provides proper encapsulation for both own and inherited private properties, it does not addresses following:

  • Privates defined on the prototype can be compromised, since they are accessible through the constructor (Foo.prototype._secret).
  • Behavior of instanceof is broken, since new Derived() instanceof Derived is going to evaluate to false.

Tamper Proofing with Property Descriptor Maps

In ES5 new property descriptor maps were introduced, which can be used as a building blocks for defining reusable peace of functionality. To some degree they are similar to a prototype objects, and can be used so to define pieces of functionality that is considered to be private (In contrast to prototype they are not exposed by default).

function Foo() {
  var foo = Object.create(Foo.prototype, FooDescriptor);
  var facade = Object.create(Foo.prototype);
  facade.hello = foo.hello.bind(foo);
  return facade;
}
Foo.prototype.hello = function hello() {
  return 'Hello ' + this._secret;
}
var FooDescriptor = {
  _secret: { value: 'secret' };
}

function Derived() {
  var derived = Object.create(Derived.prototype, DerivedDescriptor);
  var facade = Object.create(Derived.prototype);
  facade.hello = derived.hello.bind(derived);
  facade.bye = derived.bye.bind(derived);
  return facade;
}
Derived.prototype = Object.create(Foo.prototype);
Derived.prototype.bye = function bye() {
  return 'Bye ' + this._secret;
};
DerivedDescriptor = {};

Object.keys(FooDescriptor).forEach(function(key) {
  DerivedDescriptor[key] = FooDescriptor[key];
});

Cortex Objects

Last approach solves all of the concerns, but adds complexity, verbosity and decreases code readability. Combination of Cortex's and Trait's will gracefully solve all these issues and keep code clean:

var Cortex = require('cortex').Cortex;
var Trait = require('light-traits').Trait;

var FooTrait = Trait({
  _secret: 'secret',
  hello: function hello() {
    return 'Hello ' + this._secret;
  }
});
function Foo() {
  return Cortex(FooTrait.create(Foo.prototype));
}

var DerivedTrait = Trait.compose(FooTrait, Trait({
  bye: function bye() {
    return 'Bye ' + this._secret;
  }
}));
function Derived() {
  var derived = DerivedTrait.create(Derived.prototype);
  return Cortex(derived);
}

Function Cortex takes any object and returns a proxy for its public properties. By default properties are considered to be public if they don't start with "_", but default behavior can be overridden if needed, by passing array of public property names as a second argument.

Gotchas

Cortex is just a utility function to create a proxy object, and it does not solve the prototype-related issues highlighted earlier, but since traits make use of property descriptor maps instead of prototypes, there aren't any issues with using Cortex to wrap objects created from traits.

If you want to use Cortex with an object that uses a prototype chain, however, you should either make sure you don't have any private properties in the prototype chain or pass the optional third prototype argument.

In the latter case, the returned proxy will inherit from the given prototype, and the prototype chain of the wrapped object will be inaccessible. However, note that the behavior of the instanceof operator will vary, as proxy instanceof Constructor will return false even if the Constructor function's prototype is in the wrapped object's prototype chain.