ConstantScope

ConstantScope is a metaprogramming mixin that alters the way constants are stored inside modules and classes. It does not add any methods to the including class, but it changes its internals in certain useful ways.

// In the browser
JS.require('JS.ConstantScope', function(ConstantScope) { ... });

// In CommonJS
var ConstantScope = require('jsclass/src/constant_scope').ConstantScope;

In JavaScript, there is no such thing as a constant but convention dictates that any variable name that begins with a capital letter is to be treated as such. The same convention exists in Ruby, with the added bonus that the interpreter will warn you when a constant is redefined. To understant what this module does, we need first to examine some differences in constant lookup between Ruby and jsclass.

While jsclass makes every attempt to support Ruby’s object inheritance system, such that method lookup works the same in both systems, the same cannot be said of constant lookup. This is because Ruby’s constant lookup system makes use of the lexical scope of constant names, which you really need a language parser for if you want it to work properly. jsclass is not a code parser, which means it can’t support the same constant semantics as Ruby. Let’s take a concrete example:

class Outer
  CONST = 45

  class Item          # (1)
  end

  class Inner
    class Item        # (2)
      def initialize
        puts CONST    # (3)
      end
    end

    def create_item
      Item.new        # (4)
    end
  end

  def create_item
    Item.new          # (5)
  end
end 

In Ruby, any name beginning with a capital letter is considered a constant: the class Outer contains three constants: CONST, Item and Inner. They are referred to externally as Outer::CONST, Outer::Item and Outer::Inner. Likewise, Outer::Inner contains a single constant, Outer::Inner::Item. When you refer to a constant, Ruby looks up that name in the lexical scope of the reference, i.e. it looks in the enclosing class, then the class enclosing that, and so on until it reaches the global scope. So, the line marked (4) refers to Outer::Inner::Item (defined on line (2)) and line (5) refers to Outer::Item, defined on line (1). Line (3) has to go back out to Outer to find the constant CONST.

To write this in jsclass, we need to make the constants properties of the classes that contain them by extend-ing the classes:

Outer = new Class({
    extend: {
        CONST: 45,

        Item: new Class(),

        Inner: new Class({
            extend: {
                Item: new Class({
                    initialize: function() {
                        alert(Outer.CONST);
                    }
                })
            },

            create_item: function() {
                return new this.klass.Item();
            }
        })
    },

    create_item: function() {
        return new this.klass.Item();
    }
}); 

This is noisy as it contains a lot of nesting that isn’t present in the Ruby version. Also, we end up with more name duplication (Outer.CONST) since classes have no awareness of their lexical nesting, and we have some funny-looking this.klass.X references where instance methods need to refer to constants stored in their class.

ConstantScope is a module that allows classes and any classes nested inside them to support Ruby’s constant lookup system, in that any reference to a ‘constant’ (a property beginning with a capital letter) can be made simply using the syntax this.X. The value of such a reference will follow the same lexical rules as exist in Ruby, so that we can rewrite the above code as:

Outer = new Class({
    include: ConstantScope,

    CONST: 45,

    Item: new Class(),

    Inner: new Class({
        Item: new Class({
            initialize: function() {
                alert(this.CONST);
            }
        }),

        create_item: function() {
            return new this.Item();
        }
    }),

    create_item: function() {
        return new this.Item();
    }
}); 

Notice we’ve got rid of the extra Outer reference to look up CONST, there are no extend blocks, and we no longer have any this.klass.X references. This also means that the syntax for referring to a constant is the same in both class and instance methods:

SomeClass = new Class({
    include: ConstantScope,

    MY_CONST: 'cheese',

    fetch: function() {
        return this.MY_CONST;
    },

    extend: {
        get: function() {
            return this.MY_CONST;
        }
    }
});

SomeClass.get();      // -> "cheese" 

var s = new SomeClass();
s.fetch();            // -> "cheese"

Warnings

JavaScript is simply unable to support the same constant syntax as Ruby without resorting to a great deal of messing around with global variables. To support the this.X syntax in all nested classes and methods, the ConstantScope module needs to perform a great deal of reflection and creates a fair few extra modules to make sure that constants are inherited properly by nested classes and that they are available as both class and instance properties. All this is fairly expensive and you may run into performance issues; treat this module as experimental for the time being, and if you do use it beware of the following.

ConstantScope works by propagating constants to a multitude of different objects so that they appear to be lexically scoped. Changing a constant using a simple attribute accessor will not cause the new value to propagate, so make sure you reassign constants by extend-ing their containing class. Taking the above example:

// This will not propagate as expected
SomeClass.MY_CONST = 'cake';

// Do this instead
SomeClass.extend({MY_CONST: 'cake'});