Monday, November 11, 2024

Class Member Encapsulation in JavaScript: Advanced Data Hiding Techniques

In my previous article on emulating Class member encapsulation in JavaScript, I defined encapsulation as the ability of an object to be a container (or capsule) for its member properties, including variables and methods. I went on to point out how in scripting languages, where types and structure are not actively enforced by the compiler or interpreter, it is all-too-easy to fall into bad habits and write code that is brittle, difficult to maintain, and error-prone.

In that article, we saw a couple of ways to create private class member variable. Today we’ll be building on our Person class from the last article by adding a prototype interface, use a closure to create a variable scope, as well as add constructor arguments and default values.

The Person Class Revisited

When we last saw the Person class, it possessed four private member variables and seven accessor and mutator functions:

function Person() {
  //properties/fields
  var name = "Rob Gravelle";
  var height = 68;
  var weight = 170;
  var socialInsuranceNumber = "555 555 555";

  return {
    setHeight: function(newHeight) {height=newHeight;},
    getHeight: function() { return height; },
    setWeight: function(newWeight) {weight=newWeight;},
    getWeight: function() { return weight; },
    setName:   function(newName) {name=newName;},
    getName:   function() { return name; },
    setSocialInsuranceNumber: function(newSocialInsuranceNumber) { socialInsuranceNumber=newSocialInsuranceNumber; }
  };
}

//instantiate the Person class
var aPerson = new Person();
var myName = aPerson.getName();
alert(myName); //prints "Rob Gravelle"

The object notation style of class creation is considered to be a very good practice right now and is employed by most JS frameworks. Another best practice in class creation is to add public members to the object’s prototype. The drawback to using the above style is that public members have to be recreated every time an object is instantiated. There aren’t a whole lot of good reasons to do that, so adding the public members to the object’s prototype is a lot more efficient. To do that, you have to create the basic object with all of its private data and then assign an interface object to the prototype property. Of course, the object is not a real interface, as there aren’t any in JavaScript, but it helps to think along those lines:

function Person() {
  //properties/fields
  var name = "Rob Gravelle";
  var height = 68; //in inches
  var weight = 170;
  var socialInsuranceNumber = "555 555 555";
}

Person.prototype = {
    setHeight: function(newHeight) {height=newHeight;},
    getHeight: function() { return height; },
    setWeight: function(newWeight) {weight=newWeight;},
    getWeight: function() { return weight; },
    setName:   function(newName) {name=newName;},
    getName:   function() { return name; },
    setSocialInsuranceNumber: function(newSocialInsuranceNumber) { socialInsuranceNumber=newSocialInsuranceNumber; } 
};

//instantiate the Person class
var aPerson = new Person();
aPerson.setWeight(220);
alert(aPerson.getWeight());

var aPerson2 = new Person();
aPerson2.setWeight(185);  
alert(aPerson2.getWeight());  //same function, different results

Overcoming Scoping Issues

Although it might look sound, the above code has a fatal flaw! None of the functions in the prototype object can see the Person’s private variables. This is because the curly braces that contain the Person object also limit the visibility of its variables to the function body. Looking closely at the test code, you’ll notice that we called the setter before the getter. What that is doing is appending the _weight variable to the global namespace. Now that’s not very private! As stated in “JavaScript: the Definitive Guide”: “If you don’t declare a variable explicitly, JavaScript will declare it implicitly for you.” Removing the call to the setter will generate a “‘_weight’ is undefined” error.

The way to make the private variables visible to the prototype methods is to wrap the Person constructor and prototype inside a closure. It’s created by including a set of parentheses around an anonymous function ( (function(args) {})(); ) that cause it to execute as inline code. It returns a Person class with the prototype already set:

var Person = (function() {
  //properties/fields
  var name = "Rob Gravelle";
  var height = 68; //in inches
  var weight = 170;
  var socialInsuranceNumber = "555 555 555";
  
  function oPerson() {}

  oPerson.prototype = {
    setHeight: function(newHeight) {height=newHeight;},
    getHeight: function() { return height; },
    setWeight: function(newWeight) {weight=newWeight;},
    getWeight: function() { return weight; },
    setName:   function(newName) {name=newName;},
    getName:   function() { return name; },
    setSocialInsuranceNumber: function(newSocialInsuranceNumber) { socialInsuranceNumber=newSocialInsuranceNumber; } 
  };
	
  return oPerson;
})();

Now the public methods can utilize the Person’s private data:

//instantiate the Person class
var aPerson = new Person();
//this displays "I am Rob Gravelle and I am 68 inches in height."
alert("I am " + aPerson.getName() + " and I am " + aPerson.getHeight() " inches in height.");

Providing Constructor Arguments with Default Values

If we think of the initial Person object creation above (function oPerson() {}) as a constructor, there is no reason why we can’t assign values to the private data members via function arguments. Just be careful that you don’t name the arguments the same as the private variables! Whereas this.name = name; works, var name = name; does not as the latter creates duplicate variables in the same scope. You can either name the parameters slightly differently than their associated variables as in the previous setters (name = newName;) or you can prepend an underscore (_) to the variables like _name = name;. There is a longstanding tradition of using this naming convention in Java so it makes perfect sense to use it here as well. We should do the same within the setters for consistency:

var Person = (function(name, height, weight, socialInsuranceNumber) {
  //properties/fields
  var _name = name || "Rob Gravelle";
  var _height = height || 68; //in inches
  var _weight = weight || 170;
  var _socialInsuranceNumber = socialInsuranceNumber || "555 555 555";

  function oPerson() {}
		
  Person.prototype = {
    setHeight: function(height) { _height=height; },
    getHeight: function() { return  _height; },
    setWeight: function(weight) { _weight=weight; },
    getWeight: function() { return  _weight; },
    setName:   function(name) { _name=name; },
    getName:   function() { return  _name; },
    setSocialInsuranceNumber: function(socialInsuranceNumber) {  _socialInsuranceNumber=socialInsuranceNumber; },
    calculateBmi: function() { return Math.round(( _weight * 703) / ( _height *  _height)); }
  };

  return oPerson;
})();

//instantiate the Person class with some arguments
var aPerson = new Person("Ted Smith", 70, 175, "123 456 789");
//this displays "I am Ted Smith and and my BMI is 25."
alert("My name is " + aPerson.getName() + and my BMI is " + aPerson.calculateBmi() + ".");

Notice how the ORs (||) set a default value where no argument is provided. That’s one of those cool JavaScript idiosyncrasies!

Conclusion

The next article will deal with the exciting subject of method hiding, including how to call private methods from the prototype object and vice versa.

Robert Gravelle
Robert Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured