Blog-Archiv

Donnerstag, 16. Oktober 2014

JS Inheritance contra Encapsulation

Object-oriented languages have following features:

  • Data and functionality (that works on them) are grouped together in classes
  • A class is an abstraction of concrete objects that all have the same methods and properties (which can have different values)
  • Inheritance - a class can derive another class, and reuse and modify behaviour
  • Dynamic dispatch (overrides) - at runtime, the most specialized method in an inheritance hierarchy is executed
  • Method overloading - you can have methods foo() and foo(bar) and foo(bar1, bar2) ... in a class
  • Encapsulation - access modifiers like (at least) public and private
  • Open recursion - the "this" keyword that provides access to methods and properties of the same object

There are other less shortly explainable features, but lets sum up now for JavaScript.

  • Data and methods are grouped together: optionally yes, but functions are not bound to objects
  • A class as abstraction of concrete objects: no classes exist in JS
  • Inheritance: a frequently discussed topic, I would say "several kinds of, but no real"
  • Dynamic dispatch: yes, for prototypal inheritance
  • Method overloading - definitely not
  • Encapsulation - no, only local variables are some kind of "private"
  • Open recursion - "this" exists, but it points to the caller, not the owning object (although that's mostly the same, it's not always the same:-)

I would say, three of seven.

"Of course, JS is object-oriented , isn't everything in JS an object?"
"Yes", I answered, "and its functional too, everywhere you see functions ..."

The following is a result of my studies about JS inheritance, but it is nothing I can recommend, and you won't find a working inheritance solution here. You could skip this and read about functional inheritance, which is what I would advice.

Inheritance (pseudo-classical? prototypal?)

The only thing that is sure about JavaScript is that it is very flexible. So some kind of inheritance must be there, right?
If you search the web for JavaScript inheritance examples, you find things like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Animal = function(species) {
  this.species = species;
  this.food = "none";

  this.eat = function(givenFood) {
    this.food = givenFood;
  };
  this.toString = function() {
    return this.species+" ("+this.food+")";
  };
};

var Cat = function(name) {
  this.name = name;

  this.toString = function() {
    var superToString = Cat.prototype.toString.call(this); // super.toString()
    return this.name+" <"+superToString+">";
  };
};
Cat.prototype = new Animal("FourLegs");
Cat.prototype.constructor = Cat;

Animal and Cat are called constructor functions, and thus have capitalized names.
(You should never call such a function without the new operator, because such could cause unintended global variables.)

In Cat we have a super-object call which looks (and is) a little tedious:

Cat.prototype.toString.call(this);
Translated this reads as "take the overwritten function toString() from Cat's prototype object and execute it as if it was bound to 'this' Cat".

The line

Cat.prototype = new Animal("FourLegs");
is the "inheritance installation", meaning this is what you are supposed to do when you want an object to inherit from another (as there are no classes, you need objects that you can inherit from).

The line

Cat.prototype.constructor = Cat;
is for a proper constructor function, so that calling a Cat's constructor would create a Cat instance and not an Animal instance.

Here is some test code:

1
2
3
4
5
6
7
8
var garfield = new Cat("Garfield");
console.log("Garfield = "+garfield);
var catbert = new Cat("Catbert");
console.log("Catbert = "+catbert);
garfield.eat("mouse");
catbert.eat("cheese");
console.log("Garfield after meal = "+garfield);  // has he cheese?
console.log("Catbert after meal = "+catbert);

This code outputs:

Garfield = Garfield <FourLegs (none)>
Catbert = Catbert <FourLegs (none)>
Garfield after meal = Garfield <FourLegs (mouse)>
Catbert after meal = Catbert <FourLegs (cheese)>

What is in (parentheses) is what the cat has in its belly.
We need to make sure that Catbert has nothing in its belly that Garfield has eaten ;-)

Encapsulation

No problem until now, except that everything is public. Now I bring in what I would call a "private variable":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var Animal = function(species) {
  this.species = species;
 
  this.food = "none";
  var privFood = "empty"; // private
 
  this.eat = function(givenFood) {
    this.food = givenFood; // write to public field
    privFood = givenFood; // write to private field
  };
  this.toString = function() {
    return species+" ("+this.food+" / "+privFood+")";
  };
};

My expectation is that every instance of Cat has its own private privFood variable. The public food variable is for checking whether that assumption holds.
Running the same test lines as above, guess what's the result!

Garfield = Garfield <FourLegs (none / empty)>
Catbert = Catbert <FourLegs (none / empty)>
Garfield after meal = Garfield <FourLegs (mouse / cheese)>
Catbert after meal = Catbert <FourLegs (cheese / cheese)>

How can Garfield have "cheese" as its privFood variable?
When you consider that Garfield first eats a mouse, than Catbert eats "cheese", and Garfield then has "cheese" in its belly - this can not be a very private belly :-)

Isn't this a nice gotcha?

The privFood variable comes from the Animal instance in the prototype of Cat that Garfield and Catbert have in common! Not even always putting a new Animal instance to the Cat prototype when creating a new Cat helps.
This is the moment when JS experts start to talk about the "prototype chain", and complex diagrams are drawn.
At some point the technical issues become so overwhelming that you forget about the nice web-page you wanted to implement; that point is trespassed here and now.

Lesson Learnt

  • When you want objects that inherit from other objects using prototype, encapsulation (private variables) is becoming an expert work. Thus mostly everything will be public and mutable, at any time and from anywhere.
Especially on the way to modules this is a quite frustrating experience.



1 Kommentar:

fritzthecat hat gesagt…

This is the inheritance technique shown on
Mozilla JS page:


var Animal = function(species) {
this.species = species;
this.food = "none";
};
Animal.prototype.eat = function(givenFood) {
this.food = givenFood; // write to public field
};
Animal.prototype.toString = function() {
return this.species+" ("+this.food+")";
};

var Cat = function(name) {
Animal.call(this, "FourLegs");
this.name = name;
};
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Cat.prototype.toString = function() {
var superToString = Animal.prototype.toString.call(this);
return this.name+" <"+superToString+">";
};

var garfield = new Cat("Garfield");
var catbert = new Cat("Catbert");
alert("Garfield = "+garfield+"\nCatbert = "+catbert);
garfield.eat("mouse");
catbert.eat("cheese");
alert("After meal:\nGarfield = "+garfield+"\nCatbert = "+catbert);