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'});