Thursday, November 14, 2024

Class Member Encapsulation in JavaScript: Method Hiding

In the “Advanced Data Hiding Techniques” article on emulating Class member encapsulation in JavaScript, we built on our Person class from the introduction article by adding a prototype interface, created a variable scope using a closure, as well as added constructor arguments and default values.

Recall that Encapsulation is the ability of an object to be a container (or capsule) for its member properties, including variables and methods. Although JavaScript does not explicitly support many OOP constructs such as encapsulation, there are ways to mimic them to a certain extent. We explored a few ways to create private class member variables. Today we’ll be moving on to method hiding, which come with its own caveats and challenges.

The Person Class So Far

Using JavaScript-specific idiosyncrasies such as object notation, closures, the prototype object, as well as variable assignment using the OR (||) operator, we were able to construct a Person class which possessed private member variables with default values, a constructor that accepts a variable number of arguments, as well as public accessor and mutator functions:

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); //social insurance number will use default. 
alert("My name is " + aPerson.getName() + and my BMI is " + aPerson.calculateBmi());

A public method, calculateBmi(), that accesses private class data, was included to demonstrate that it could indeed access the class’s private data members. Likewise, private data members can call public methods as well. In the next example, the Person class contains a private variable to store an initial BMI number. It is immutable, which is to say uneditable, because you would never want to update its value (unless you wanted to fudge some later result!). To call a public method, you only need to preface the call with the “this” pointer. It doesn’t matter that the prototype is placed after the Person constructor function in the code. As long as the “this” pointer references the Person class at runtime, all is well. Being located in the constructor makes this all but a certainty.

A new public method called howAmIDoing() references the _initialBmi variable to make a progress report based on the latest BMI value. It uses the switch (true) { condition 1; condition 2 } style to group results within ranges. A progress message is returned based on the range:

var Person = (function() {
  var _initialBmi;
  var _name;
  var _height; //in inches
  var _weight;
  var _socialInsuranceNumber;
  
  function Person(name, height, weight, socialInsuranceNumber) {
    //properties/fields
    _name = name || "Rob Gravelle";
    _height = height || 68; //in inches
    _weight = weight || 170;
    _socialInsuranceNumber = socialInsuranceNumber || "555 555 555";
    _initialBmi = this.calculateBmi();
    alert("My initial BMI is " + _initialBmi);
  }

  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)); },
    howAmIDoing: function() { 
      var msg, diff = _initialBmi - this.calculateBmi();
      switch (true) {
        case (diff <= -10):
          msg = "You've really let yourself go!";
          break;
        case (diff <= -1 && diff > -10):
          msg = "You've slacked off a bit.";
          break;
        case (diff == 0): 
          msg = "Nothing's changed.";
          break;
        case (diff >= 1 && diff < 10):
          msg = "Some improvement";
          break;
        case (diff >= 10):
          msg = "Amazing improvement!";
          break;
      };
      return msg;
    }
  };
	
  return Person;
})();

//instantiate the Person class
var aPerson = new Person("Ted Smith", 70, 175);
aPerson.setWeight(220);
alert(aPerson.howAmIDoing()); //displays "You've slacked off a bit."

The above example displays the initial BMI value to show that it was set as expected as well as the “You’ve slacked off a bit.” howAmIDoing() message. Never mind the fact that gaining 55 pounds is more than a little slacking off.

Here’s another take on the above class which goes a step further by hiding the calculateBmi() method. It follows the same technique as the _initialBmi variable by assigning the function to a local variable. The now private method is still called by the public howAmIDoing() function to create a report that compares the initial BMI value to the current one:

var Person = (function() {
  //private data members
  var _initialBmi;
  var _name;
  var _height; //in inches
  var _weight;
  var _socialInsuranceNumber;

  //private methods
  var _calculateBmi = function() { return Math.round(( _weight * 703) / ( _height *  _height)); }

  //constructor
  function Person(name, height, weight, socialInsuranceNumber) {
    //properties/fields
    _name = name || "Rob Gravelle";
    _height = height || 68; //in inches
    _weight = weight || 170;
    _socialInsuranceNumber = socialInsuranceNumber || "555 555 555";
	  _initialBmi = _calculateBmi();
	  alert("My initial BMI is " + _initialBmi);
 }

  //public interface
  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; },
    howAmIDoing: function() { 
      var newBmi = _calculateBmi(),
          diff   = _initialBmi - newBmi,
          msg    = "BMI REPORTn"
                 + "--------------------n"
                 + "Starting BMI: " + _initialBmi + "n"
                 + "Latest BMI value: " + newBmi + "nn"
                 + "Conclusion:n";
								 
      switch (true) {
        case (diff <= -10):
          msg += "You've really let yourself go!";
          break;
        case (diff <= -1 && diff > -10):
          msg += "You've slacked off a bit.";
          break;
        case (diff == 0): 
          msg += "Nothing's changed.";
          break;
        case (diff >= 1 && diff < 10):
          msg += "Some improvement";
          break;
        case (diff >= 10):
          msg += "Amazing improvement!";
          break;
      };
      return msg;
    }
  };
	
  return Person;
})();

//instantiate the Person class
var aPerson = new Person("Ted Smith", 70, 175);
aPerson.setWeight(220);
alert(aPerson.howAmIDoing()); 
//	displays:
//	BMI REPORT
//	--------------------
//	Starting BMI: 25
//	Latest BMI value: 32
//
//	Conclusion:
//	You've slacked off a bit.

A Chink in the Armor!

As stated in the intro, these techniques are a way to emulate class encapsulation in JavaScript. There is one reason that it is impossible to implement true class encapsulation in JavaScript and that is that it’s a scripting language. A major difference between interpreted languages like JavaScript and a compiled one such as Java is that the code is run from a plaintext state. That makes it really, really easy to manipulate the code at runtime.

Any function’s source code is readily available for all to see because of the toString() function which all objects implement by default. Here’s an example that does some simple string manipulation to overwrite the Person class with a hacked one! There are actually a few ways to create a function from a string. This code uses the eval() function. Another way is the Function() constructor. I used a simpler version of the Person class so that we could easily see all of its source code

The result of “alert( Person.toString() );”:

Person_class_source_code_in_ie8_alert (33K)

Some simple code hijacking!

var searchString = "555";"
var sourceCode   = Person.toString();
var insertPoint  = sourceCode.indexOf(searchString) + searchString.length;
eval(Person = sourceCode.substr(0, insertPoint) + "nn  name = "You've been hacked!";" + sourceCode.substr(insertPoint + 1));
alert( Person.toString() );

var aPerson = new Person();
alert("My name is " + aPerson.getName());

The Person class with the injected code:

Person_class_hacked_source_code_in_ie8_alert (34K)

Calling the getName() function now displays this:

modified_private_name_variable_in_ie8_alert (6K)

You could make it more difficult to get at the function code by overriding the static toString() method. This is accomplished by assigning it to the generic Person object as opposed to a specific instantiation:

Person.prototype.toString = function() { return "[Person Class v 1.2.1]"; }

That would be a definite improvement, but still not an infallible one as any part of the script can be overwritten at runtime. What’s important to keep in mind is that OOP constructs such as class encapsulation are not meant to act as security features but rather as best practices for developers to follow. As such, simply hiding class members from other developers is more than enough to tell them what’s available and what isn’t.

 

Conclusion

I hope that you found this series to be a useful one. I think that you’ll agree that as scripts become more powerful, the more the following of OOP practices becomes imperative.

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