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()
andfoo(bar)
andfoo(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:
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".Cat.prototype.toString.call(this);
The line
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).Cat.prototype = new Animal("FourLegs");
The line
is for a proper constructor function, so that calling a Cat's constructor would create aCat.prototype.constructor = Cat;
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.
1 Kommentar:
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);
Kommentar veröffentlichen