In continuation with this question,
Animal
class has four fields:
name
sound
owner
favFood
Cat
derived class has one field
mode
Derived class
Cat
is created such that every instance ofCat
inherits instance members ofAnimal
. In addition,Cat
adds a new instance membermode
.Constructor pattern is used amidst construction and modification of objects.
Criteria is,
Animal
andCat
class should enforce encapsulation and proper inheritance.
Solution
/* example.js */
function Animal() {
var _name = "";
var _sound = "";
var _owner = "";
var _favFood = "";
Object.defineProperties(this, {
'name': {
get: function () {
return _name;
},
set: function (name) {
//check conditions before setting the name
_name = name;
}
},
'owner': {
get: function () {
return _owner;
},
set: function (ownerName) {
//check conditions before setting the owner
_owner = ownerName;
}
},
'sound': {
get: function () {
return _sound;
},
set: function (sound) {
//check conditions before setting the sound
_sound = sound;
}
},
'favFood': {
get: function () {
return _favFood;
},
set: function (favFood) {
//check conditions before setting the name favFood
_favFood = favFood;
}
}
});
}
//Below code is the interface for Animal and its subclass type objects
Animal.prototype.getName = function(){ return name;} // calls get property of name
Animal.prototype.setName = function(newName){name = newName;} //calls set property of name
Animal.prototype.getOwner = function(){ return owner;}
Animal.prototype.setOwner = function(newOwner){owner = newOwner;}
Animal.prototype.getSound = function(){ return sound;}
Animal.prototype.setSound = function(voice){ sound = voice;}
Animal.prototype.getFavFood = function(){ return favFood;}
Animal.prototype.setFavFood = function(food){ favFood = food;}
/* dog.__proto__ points to Animal.prototype */
var animal = new Animal();
animal.setName("Spot");
animal.setOwner("Paul");
animal.setSound();
document.write("Animal properties:" + "<br>");
document.write(animal.getName() + "<br>");
document.write(animal.getOwner() + "<br>");
document.write(animal.getSound() + "<br><br><br>");
/* Cat.__proto__ points to Function.prototype */
function Cat() {
/*
Below line will add definePropertiess name/sound/owner/favFood
to an instance i.e., Cat()
*/
Animal.call(this);
var _mode = "";
Object.defineProperties(this, {
'mode': {
get: function () {
return _mode;
},
set: function (newMode) {
//check conditions before setting the sound
_mode = newMode;
}
}
});
}
/*
After executing, below line of code,
Cat.prototype.__proto__ will point to Animal.prototype;
*/
Cat.prototype = Object.create(Animal.prototype);
/*
In the above line, when Cat.prototype.__proto__ points to Animal.prototype,
Cat.prototype.constructor automatically points to Animal, so this below line
*/
Cat.prototype.constructor = Cat;
Cat.prototype.getMode = function(){ return mode;}
Cat.prototype.setMode = function(Mode){ mode = Mode;}
/* sophie.__proto__ points to Cat.prototype */
var sophie = new Cat();
sophie.setName("Sophie");
sophie.setOwner("Derek");
sophie.setSound("Meow");
document.write("Cat properties:" + "<br>");
document.write(sophie.getName() + "<br />");
document.write(sophie.getOwner() + "<br />");
document.write(sophie.getSound() + "<br />");
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Constructor pattern</title>
<script type="text/javascript" src="example.js"></script>
</head>
<body>
<noscript>
Requires JS
</noscript>
</body>
</html>
As per debugging, Animal.call(this)
gets invoked on new Cat()
instance, private members _name
/_owner
/_sound
/_favFood
are created separately for Cat
instance.
Using constructor pattern, Does this design enforce encapsulation and inheritance?
Note: Constructor pattern reference
2 Answers 2
You have the right basic idea, but it feels like you are trying to mold JavaScript into Java. You would write a lot less boilerplate code if you loosened up and embraced the fluidity of JavaScript. If you are using the language support for getters/setters, then all of those getSomeProperty()
and setSomeProperty(value)
functions are missing the point of the language feature. You should drop them entirely in favour of obj.someProperty
for getters and obj.someProperty = value
for setters. Write the getters and setters explicitly only when you need to do some unusual processing, such as validation or calculation.
Note that the abstraction is still preserved despite the simplification. The object (such as kitty
in the example below) is indifferent to whether the property is just a plain variable or is backed by a getter/setter.
function Animal() {
this.sound =
this.owner =
this.favFood = "";
var name = "";
Object.defineProperty(this, 'name', {
get: function() { return name; },
set: function(n) {
if (n == 'Mohammed') {
throw new Exception('Blasphemous names not allowed');
}
name = n;
},
});
}
function Cat() {
// Chain initialization
Animal.call(this);
this.mode = undefined;
}
// Inheritance
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
/* animal.__proto__ points to Animal.prototype */
var animal = new Animal();
animal.name = "Spot";
animal.owner = "Paul";
animal.sound = undefined;
/* kitty.__proto__ points to Cat.prototype */
var kitty = new Cat();
kitty.name = "Sophie";
kitty.owner = "Derek";
kitty.sound = "Meow";
document.write("Animal properties:" + "<br>");
document.write(animal.name + "<br>");
document.write(animal.owner + "<br>");
document.write(animal.sound + "<br><br><br>");
document.write("Cat properties:" + "<br>");
document.write(kitty.name + "<br>");
document.write(kitty.owner + "<br>");
document.write(kitty.sound + "<br><br><br>");
try {
kitty.name = "Mohammed";
} catch (e) {
document.write("Blasphemy averted!<br>");
}
document.write("The name again: " + kitty.name + "<br />");
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Constructor pattern</title></head><body></body></html>
-
\$\begingroup\$ This is a pretty good answer, although I would have liked to have seen a nod towards ES6 classes and properties given they are The Future (tm) \$\endgroup\$Dan– Dan2015年12月28日 07:33:49 +00:00Commented Dec 28, 2015 at 7:33
-
\$\begingroup\$ so after Dan pantry's answer, correction is:
get: function() { return this.name; }
wherevername
is referred \$\endgroup\$overexchange– overexchange2015年12月28日 07:56:54 +00:00Commented Dec 28, 2015 at 7:56 -
\$\begingroup\$ @overexchange But don't mix up our answers when commenting. My
get: function() { return name; }
works just as intended. \$\endgroup\$200_success– 200_success2015年12月28日 08:03:08 +00:00Commented Dec 28, 2015 at 8:03 -
\$\begingroup\$ Do you mean, as
name
is in local scope of functionAnimal
,name
does not refer to global context? \$\endgroup\$overexchange– overexchange2015年12月28日 08:06:51 +00:00Commented Dec 28, 2015 at 8:06 -
\$\begingroup\$ Yes, that's right. My
get: function() { return name; }
refers to thevar name = "";
that occurs just above it. \$\endgroup\$200_success– 200_success2015年12月28日 08:08:44 +00:00Commented Dec 28, 2015 at 8:08
200's answer is more or less on point and covers things from an idiomatic point of view, so I'd like to just address a bug in your code. It's actually a fairly major one:
Your getXXX
/setXXX
methods are going to break your code because they reference globals. We'll use this one as an example:
Animal.prototype.getOwner = function(){ return owner;}
Animal.prototype.setOwner = function(newOwner){owner = newOwner;}
Let's, for example, use this code as a starting point (because it's simpler to put into a REPL):
var animalPrototype = {
getOwner: function() {
return owner;
},
setOwner: function(newOwner) {
owner = newOwner;
}
};
var animal = Object.create(animalPrototype);
This code does practically the same thing you've got above, just without the class nonsense. I'm going to execute all of this in a REPL first of all with strict mode off.
animal.setOwner('foo');
console.log(animal.getOwner());
> foo
This seems good so far. Now, let's create another animal. I'm going to give it a name - Thor. Because that was the name of my dog, and I miss him. Also, because Thor invokes the thought of fury and lightning, which is exactly what the V8 engine is about to do to me.
var thor = Object.create(animalPrototype);
thor.setOwner('loki'); // We all know it is true.
console.log(thor.getOwner());
> loki
Again, all seems fine, until..
console.log(animal.getOwner())
> loki
Crap! Massive bug. What's happened here?
In your getXXX/setXXX
methods you're assigning and returning a global. Now, if this was executing in strict mode this should procure a ReferenceError
. But because this is not being executed in strict mode, the browser sort of goes with it and in order to make the code work, it will simply assume that when you say owner
, you actually mean window.owner
(it's quite a bit more complicated than this, but this is the nutshell version).
Congratulations! You've just created a stateful global variable, which is one of the worst evils in the world, right up there next to kicking puppies and depriving people of Fallout 4.
This 'quirk' exists for (unfortunate) legacy reasons with JavaScript and is one of the reasons why we enable strict mode. Enabling strict mode is super simple. The above code sample looks like this:
'use strict';
var animalPrototype = {
getOwner: function() {
return owner;
},
setOwner: function(newOwner) {
owner = newOwner;
}
};
var animal = Object.create(animalPrototype);
Now your code will yell at you before you end up with nasty leakages..
animal.getOwner()
> Uncaught ReferenceError: owner is not defined
animal.setOwner('foo')
> Uncaught ReferenceError: owner is not defined
Oh, and be careful when using strict mode. You don't want to mix strict code with non-strict code, so if you aren't using a bundler like webpack, you'll want to wrap your entire code in an IIFE and then use the 'use strict'
directive in there instead. This prevents code that relies on that weird quirk from going bananas. Example:
(function() {
'use strict';
var animalPrototype = {
getOwner: function() { return this.owner; },
setOwner: function(newOwner) { this.owner = newOwner; }
}
var animal = Object.create(animalPrototype);
}());
Explore related questions
See similar questions with these tags.
prototype
property. Are you referring to terminology(Animal
class) used in this question? \$\endgroup\$setXXX
andgetXXX
methods are currently broken as they reference an implicit global object (in this case,window
). Your code will break once you have two instances of your class. \$\endgroup\$window['name'] = 'Spot'
is defined, on callingAnimal.prototype.setName('Spot');
\$\endgroup\$