A Note About Private Class Members in CoffeeScript/JavaScript
JavaScript is the programming language of the web. With the growth of the JavaScript engine V8 (the google chrome browser JavaScript engine), which enable JavaScript to run faster, JavaScript is increasingly utilized on the server side, e.g. with node.js, phantomjs, etc.
CoffeeScript is an innovative language that compiles into JavaScript code. With CoffeeScript we can write our program logic much more faster with less and readable code.
CoffeeScript is a little language that compiles into JavaScript. Underneath that awkward Java-esque patina, JavaScript has always had a gorgeous heart. CoffeeScript is an attempt to expose the good parts of JavaScript in a simple way.
Most mainstream programming languages adopt the Object-oriented Programming (OOP) paradigm, however, JavaScript does not support OOP natively. But we can still implement classes and objects in its own way. In this article we’ll discuss about classes, objects and encapsulation in JavaScript/CoffeeScript.
Classes and Objects in JavaScript/CoffeeScript
In JavaScript, we use function
and prototype
to implement classes.
function Square(side) {
this.side = side; // initialize class variable
}
Square.prototype.area = function() {
return this.side * this.side;
}
var a = new Square(2);
console.log(a.area()); // output: 4
var b = new Square(3)
console.log(b.area()); // output: 9
Usually we wrap the definition of a class or a block of code with function scope (function() { /* class definition */ })()
mainly for the following considerations:
- Let the browser load the function and execute it as a whole
- Create a new scope to avoid creating/overriding fields in the global object
window
In CoffeeScript, defining a class is much more simpler.
class Square
constructor: (side) ->
@side = side
area: ->
@side * @side
a = new Square(2)
console.log a.area() # output 4
b = new Square(3)
console.log b.area() # output 9
Which compiles into:
var Square, a, b;
Square = (function() {
function Square(side) {
this.side = side;
}
Square.prototype.area = function() {
return this.side * this.side;
};
return Square;
})();
a = new Square(2);
console.log(a.area());
b = new Square(3);
console.log(b.area());
We can see that CoffeeScript wrap the definition of a class with function scope automatically. Actually, CoffeeScript uses a function scope to wrap every JavaScript file compiled from CoffeeScript in case of collision.
In classes defined with JavaScript function and prototype, there is no private methods and variables. All class methods and class variables are public.
Private Field Encapsulation
The basic thought of implementing private field is to encapsulate variables and methods in anonymous function scope. The following example adds private static variables and methods in a class.
# CoffeeScript
class Counter
# private static variable
counter = 0
# private static method
countInstance = ->
counter++
# public static method
@instanceCount = ->
counter
constructor: ->
countInstance()
c1 = new Counter()
c2 = new Counter()
console.log Counter.instanceCount() # output 2
Which compiles into:
var Counter, c1, c2;
Counter = (function() {
var countInstance, counter;
counter = 0;
countInstance = function() {
return counter++;
};
Counter.instanceCount = function() {
return counter;
};
function Counter() {
countInstance();
}
return Counter;
})();
c1 = new Counter();
c2 = new Counter();
console.log(Counter.instanceCount());
The first function scope to create the class Counter is created only once; so that the variables and methods inside has only one instance and they are accessible from all class methods and instance methods. In order to implement private instance fields, we have to create a new function scope every time the class is instantiated. Considering that all private instance fields have to be accessible from all instance methods, public methods must be either inside the same function scope of the private fields or in one of the descendants, which means that we have to define public methods every time a instance is created so that prototype is not suitable for such situation. Here is an example of class definition with public and private fields and static fields.
# CoffeeScript
class Square
# private static variable
counter = 0
# private static method
countInstance = ->
counter++; return
# public static method
@instanceCount = ->
counter
constructor: (side) ->
countInstance()
# side is already a private variable, we define a private variable `self` to avoid evil `this`
self = this
# private method
logChange = ->
console.log "Side is set to #{side}"
# public methods
self.setSide = (v) ->
side = v
logChange()
self.area = ->
side * side
s1 = new Square(2)
console.log s1.area() # output 4
s2 = new Square(3)
console.log s2.area() # output 9
s2.setSide 4 # output Side is set to 4
console.log s2.area() # output 16
console.log Square.instanceCount() # output 2
Which compiles into:
var Square, s1, s2;
Square = (function() {
var countInstance, counter;
counter = 0;
countInstance = function() {
counter++;
};
Square.instanceCount = function() {
return counter;
};
function Square(side) {
var logChange, self;
countInstance();
self = this;
logChange = function() {
return console.log("Side is set to " + side);
};
self.setSide = function(v) {
side = v;
return logChange();
};
self.area = function() {
return side * side;
};
}
return Square;
})();
s1 = new Square(2);
console.log(s1.area());
s2 = new Square(3);
console.log(s2.area());
s2.setSide(4);
console.log(s2.area());
console.log(Square.instanceCount());
That’s a way to implement class member encapsulation in normal cases (not too many instances and do not need inheritance). What about protected members and class inheritance?
Class Inheritance and Protected Fields
Without encapsulated fields, class inheritance is easy to implement with function and prototype. And that’s how protoype.js implement class inheritance. For example:
# CoffeeScript run with prototype.js
Rectangle = Class.create {
initialize: (a, b) ->
this.a = a
this.b = b
area: ->
this.a * this.b
}
r = new Rectangle(2, 3)
console.log r.area() # output 6
Square = Class.create Rectangle, {
initialize: ($super, side) ->
$super side, side
}
s = new Square(4)
console.log s.area() # output 16
Which compiles into:
var Rectangle, Square, r, s;
Rectangle = Class.create({
initialize: function(a, b) {
this.a = a;
return this.b = b;
},
area: function() {
return this.a * this.b;
}
});
r = new Rectangle(2, 3);
console.log(r.area());
Square = Class.create(Rectangle, {
initialize: function($super, side) {
return $super(side, side);
}
});
s = new Square(4);
console.log(s.area());
To learn more about the prototype.js implementation, please read the source code.
We’ve discussed about private members in JavaScript classes above. What about inheritance without prototype and protected fields accessible from sub-classes?
To create a subclass B from class A, we have to create a scope chain like this:
A_private_static_scope <--- A_protected_scope <--- A_private_scope <--- A_public_scope ^ | B_private_static_scope <--- B_protected_scope <--- B_private_scope <--- B_public_scope
This is theoretically impossible. Reasons:
- Scope
B_protected_scope
cannot be inA_protected_scope
ANDB_private_static_scope
- Even if we do not need static members, B is not defined inside the definition of class A (of course subclass is defined somewhere else), so that
B_protected_scope
cannot be inA_protected_scope
- Methods in
B_protected_scope
should have access to members inA_public_scope
and have no access to members inA_private_scope
, which is impossible with scope chain
Conclusion is: we cannot implement class inheritance and encapsulation with JavaScript scope chain. However, we can implement our own class helper to implement class inheritance and protected members. I’ve created a lightweight Class Helper example to explain the means by which to implement private/protected/public static/instance variables/methods and subclass inheritance.
(->
classChain = []
getSuperClass = (subClass) ->
return null if !subClass
for cls in classChain
return cls.superClass if cls.cls == subClass
getInitializer = (_cls) ->
return null if !_cls
for cls in classChain
return cls.initialize if cls.cls == _cls
window.createClass = (superClass, classCreator) ->
(classCreator = superClass; superClass = null) if arguments.length == 1
return null if typeof classCreator != 'function'
$parent = superClass
$parents = []
$parents.push superClass if superClass
while getSuperClass($parent)
$parent = getSuperClass($parent)
$parents.push $parent
cls = ->
self = this
$self = {}
classInitializers = []
originalInitializers = []
for $parent in $parents
originalInitializers.unshift getInitializer($parent)
originalInitializers.push initialize
for _initializer in originalInitializers
((_initializer) ->
classInitializers.push(->
$self.initialize = classInitializers.pop() || (->)
args = [self, $self]
for arg in arguments
args.push arg
_initializer.apply self, args
delete $self.initialize
return if _initializer == initialize
$super = {}
$super.$super = $self.$super if $self.$super
for k, v of $self
$super[k] = v if typeof v == 'function'
$self.$super = $super
)
)(_initializer)
classInitializers.pop().apply(self, arguments)
return
initialize = classCreator(cls)
classChain.push {
cls: cls
superClass: superClass
initialize: initialize
}
return cls
)()
With the class creator helper method, we can create our own classes. For example:
A = createClass((Class)->
# private static variable
counter = 0
# private static method
countInstance = ->
counter++; return
# public static method
Class.instanceCount = ->
counter
# class initiation method, where:
# self: the instance object
# $self: the context for protected members
(self, $self, id) ->
$self.initialize() # call super class constructor.
countInstance()
# private variable: id
# private method
getClassName = ->
"A"
# protected variables
$self.id = id
# protected method
$self.present = ->
"#{getClassName()} # #{$self.id}"
# public methods
self.print = ->
console.log "<#{$self.present()}>"
)
B = createClass(A, (Class) ->
counter = 0
countInstance = ->
counter++; return
Class.instanceCount = ->
counter
(self, $self, id) ->
$self.initialize(id) # call super class constructor.
countInstance()
getClassName = ->
"B"
# override
$self.present = ->
"#{getClassName()} : #{$self.$super.present()}"
)
a = new A('001')
b = new B('002')
a.print() # output: "<A # 001>"
b.print() # output: "<B : A # 002>"
console.log A.instanceCount() # output: 2
console.log B.instanceCount() # output: 1
Protected members in this example are not strictly ‘protected’, but are ‘protected abstract’ and can be override in subclass. That’s because protected methods are not implemented with scope chain in this class helper. And there might be better implementation. If you don’t understand the example listed above, maybe you should learn more about JavaScript scope chain, prototype and closure.
Fortunately, in most cases we do not need protected members, at least we usually do not need protected methods that cannot be override. So that the class helper like above is sufficient for most use cases. Please note that the class helper above is not well tested for production use and is available on github.
Performance Issues
To encapsulate private members, we create public methods every time a class is initiated, so that all private members are accessible to public methods. That’s how we protect the ‘privacy’. Price is that it causes performance issues when thousands of instances are being created in our application. In such cases we fall back to the function & prototype scheme which does not implement private members, which however, is able to avoid defining public methods repeatedly.
In fact, ‘privacy’ is not strictly ‘privacy’ in JavaScript; with the modern tools such as the development tool in google chrome, we can inspect almost everything at ease. The ‘privacy’ here actually refers to the scheme to prevent unexpected access to encapsulated variables and methods. With that in mind, a proper naming convention to distinguish private and public members is sufficient, though everything is ‘public’.
However, if you’re still interested in the class helper in this article, please let me know.