indexzero
9/19/2014 - 10:43 PM

Javascript, objects, object keys, named functions, and chaos

Javascript, objects, object keys, named functions, and chaos

Javascript, objects, object keys, named functions, and chaos

  • Fact: All keys to Objects in Javascript are strings
  • Fact: When declared properly functions have a .name property

Implementation

We can use this to our advantage if we wish to perform lookups based on functions instead of by keys


function setNamedFunction(obj, fn) {
  var keys = Object.keys(obj),
      name = fn.name;
  
  //
  // If `fn` __isn't__ a named function
  // then this will never work, so return.
  //
  if (!name) {
    return;
  }
  
  //
  // No need to return `obj` since it is
  // passed by reference
  // 
  obj[name] = fn;
}

Named vs. Anonymous functions

When a function is declared without a name it is an anonymous function. e.g.:

$.on('load', function () {
  console.log('I am inside an anonymous function');
});

Anonymous functions are OK but they have two serious drawbacks:

  1. They make your stacktraces ugly if an error occurs inside them
  2. They lack a name property so they can be difficult to reflect across

The solution is to use named functions:

$.on('load', function onReady() {
  console.log('I am inside the onReady function');
});

A directed event graph

Lets suppose that instead of wanting to trigger a linear set of functions such as the core Node.js EventEmitter, you wanted to be able to trigger events as a (potentially cyclic) directed graph.

Scenario 1. A causes B & C to fire

A
├─B
│
└────C

Scenario 2. A causes B & C to fire. B causes D to fire

A
├──B
│  └─D
└────C

This could be implemented by modifying the underlying EventEmitter (in this example EventEmitter3 thusly:

EventEmitter.prototype.connect = function (in, out) {
  var self = this;
  this.on(in, function () {
    var args = Array.prototype.slice.call(arguments);
    args.unshift(out);
    self.emit.apply(self, args);
  });
  
  return this;
};

In this way we could then setup the above scenarios:

Scenario 1. A causes B & C to fire

var emitter = new EventEmitter();

emitter.on('A', function a() { console.log('A fired'); });
emitter.on('B', function b() { console.log('B fired'); });
emitter.on('C', function c() { console.log('C fired'); });

emitter.connect('A', 'B');
emitter.connect('A', 'C');

Scenario 2. A causes B & C to fire. B causes D to fire

var emitter = new EventEmitter();

emitter.on('A', function a() { console.log('A fired'); });
emitter.on('B', function b() { console.log('B fired'); });
emitter.on('C', function c() { console.log('C fired'); });
emitter.on('D', function c() { console.log('C fired'); });

emitter.connect('A', 'B');
emitter.connect('A', 'C');
emitter.connect('B', 'D');

We could implement disconnect but we would have to add a naming convention to our functions in .connect() and implment .disconnect():

EventEmitter.prototype.connect = function (in, out) {
  var self = this,
      listener;
  
  listener = function () {
    var args = Array.prototype.slice.call(arguments);
    args.unshift(out);
    self.emit.apply(self, args);
  };
  
  listener.name = [in, out].join('_');
  this.on(in, listener);
  
  return this;
};

EventEmitter.prototype.unconnect = function (in, out) {
  var name = [in, out].join('-'),
      self = this;
  
  this.listeners(in)
    .filter(function (fn) {
      return fn.name === name;
    })
    .forEach(function (fn) {
      self.removeListener(in, fn);
    });
    
  return this;
};