Turning a Javascript Class Back Into Code

January 20th, 2013 Permalink

In Javascript it is fairly straightforward to export a class as code: given a class definition (function, prototype, properties, etc) you can generate a code string that will recreate that class if fed into a Javascript engine. This is based on the use of Function.prototype.toString(), which returns the code for a function definition, inline comments included, and JSON.stringify() which can give you suitable code-like strings for everything else.

function return2 {
  // Return 2.
  return 2;
}
// This will log exactly the code above, including whitespace, comments, and layout.
console.log(return2.toString());

var obj = {
  a: 2,
  b: 3
};
/*
  This will log the following:

  {
    "a": 2,
    "b": 3
  }
*/
console.log(JSON.stringify(obj, null, "  "));

So let us say we have the following simple class definition - a prototype definition, functions, properties attached to the class function itself, and nothing potentially complicated like inheritance, references to other class, or circular definitions:

function Example (foo) {
  this.foo = foo;
}

Example.TYPES = {
  A: "a",
  B: "b"
}

Example.prototype.bar = function () {
  return this.foo;
}

Given this, we could generate code for this class using the following short function:

/**
 * Given a class definition, generate Javascript code for it.
 *
 * @param {function} classFunction
 *   A class definition.
 * @param {string} [indent]
 *   Indent string, e.g. two spaces: "  "
 * @param {string} [extraIndent]
 *   An additional indent to apply to all rows to get them to line up with
 *   whatever output they are inserted into. e.g. an extra two spaces "  ";
 * @return {string}
 *   Javascript code.
 */
p.classToCodeString = function (classFunction, indent, extraIndent) {
  indent = indent || "  ";
  var code = "";
  if (extraIndent) {
    code += extraIndent;
  }
  code += classFunction.toString();
  code += "nn";

  function getPropertyCode(property) {
    if (typeof property === "function") {
      return property.toString();
    } else {
      return JSON.stringify(property, null, indent);
    }
  }

  var prop, propCode;

  // "Static" items.
  for (prop in classFunction) {
    propCode = getPropertyCode(classFunction[prop]);
    code += classFunction.name + "." + prop + " = " + propCode + ";n";
  }

  code += "n";

  // Prototype functions.
  for (prop in classFunction.prototype) {
    propCode = getPropertyCode(classFunction.prototype[prop]);
    code += classFunction.name + ".prototype." + prop + " = " + propCode + ";nn";
  }

  // Add the extraIndent.
  if (extraIndent) {
    code = code.replace(/n/g, "n" + extraIndent);
  }

  return code;
};

So What is the Point of All This?

If you are using Node.js or other server-side Javascript, there is a strong incentive to share code between the server and the client. It isn't always the case that the Javascript code in question is going to be arranged so that you can achieve that goal by just using the same code files for client and server. Sometimes the class you want will be buried in third party code, or the file containing it has module.exports, require(), and other items that would have to be stripped out anyway, and so forth.

The example code above is one way to enable the delivery of small utility classes to the client. Since you can recreate the code, it is possible to include the class in templated Javascript. e.g.:

(function () {
{{{exampleClass}}}
  MyNamespace.Example = Example;
})();

Other Things to Consider

The example code given here doesn't accommodate classes that contain references to other classes, or which have properties that contain functions. E.g.:

function Example (foo) {
  this.foo = foo;
}

Example.OtherClass = OtherClass;

Example.FN = {
  A: function () {}
};

Both would require some form of recursion when dealing with properties or greater awareness of the context in which the class exists. If putting recursion in place for class properties, you would also have to watch for circular references.

This example also doesn't consider class inheritance in any way. It would not be too hard to include, but how it is done would depend on which method of inheritance you are using. I'll leave that as an exercise for the reader.