JavaScript OOP Course 9: Exercises
References
- Code with Mosh: The Ultimate JavaScript Mastery Series – Part 2
- W3School: JavaScript Tutorial
Stopwatch
Use Constructor function and create an Object sw
that has the following members:
- Properties:
duration
- Method:
reset()
,start()
,stop()
.
function Stopwatch() {
let startTime = null; let endTime = null; let running = false; let duration = 0;
this.start = function() {
if (running) throw new Error('The Stopwatch is already startrd.');
running = true;
startTime = new Date();
};
this.stop = function() {
if (!running) throw new Error('The Stopwatch is not running.');
running = false;
endTime = new Date();
const seconds = (endTime.getTime() - startTime.getTime()) / 1000;
duration += seconds;
};
this.reset = function() {
startTime = null; endTime = null; running = false; duration = 0;
};
Object.defineProperty(this, 'duration', {
get: function() { return duration; }
});
}
const sw = new Stopwatch();
sw.start();
sw.stop();
sw.duration;
8.56
sw.reset();
Stopwatch (move all methods at prototype level)
In order to do that we need to give public access to the private properties (defined by the let
keyword) as it is done below, but note this breaks the abstraction principle, for example: In order to set the duration
value from the prototype level we need to create setter that private variable, but in the same time this setter function is accessible from outside… This example is designed to show that sometimes it is a bad practice to move some members at the prototype level – Premature optimization is the root of all evils.
function Stopwatch() {
let duration = 0; let startTime = null; let endTime = null; let running = false;
Object.defineProperty(this, 'duration', {
get: function() { return duration; },
set: function(value) { duration = value; }
});
Object.defineProperty(this, 'startTime', {
get: function() { return startTime; },
set: function(value) { startTime = value; }
});
Object.defineProperty(this, 'endTime', {
get: function() { return endTime; },
set: function(value) { endTime = value; }
});
Object.defineProperty(this, 'running', {
get: function() { return running; },
set: function(value) { running = value; }
});
}
Stopwatch.prototype.start = function() {
if (this.running) throw new Error('The Stopwatch is already started.');
this.running = true;
this.startTime = new Date();
};
Stopwatch.prototype.stop = function() {
if (!this.running) throw new Error('The Stopwatch not started.');
this.running = false;
this.endTime = new Date;
const seconds = (this.endTime.getTime() - this.startTime.getTime()) / 1000;
this.duration += seconds;
};
Stopwatch.prototype.reset = function() {
this.duration = 0;
this.startTime = null;
this.endTime = null;
this.running = false;
};
Prototypical Inheritance
Create two constructors HtmlElement()
and HtmlSelectElement()
with the following specification:
HtmlElement()
should have two methods:
click()
that will produce the text 'clicked' in the console and is defined at the Constructor function level.focus()
that will produce the text 'focused' in the console and is defined at the Prototype level of the Constructor function.
HtmlSelectElement()
should have the following members:
- A property called
items
which is an array that could be an empty array or array passed as argument to the constructor function when a new object is created by it. - Two methods:
addItem(item)
that will add the item passed as argument to the array ofitems
.removeItem(item)
that will remove the item passed as argument from the array ofitems
.
- Also should Prototypically Inherit the two methods –
click()
andfocus()
– from theHtmlElement()
constructor.
1. Let's create and test HtmlElement()
.
function HtmlElement() {
this.click = function() {
console.log('clicked');
};
}
HtmlElement.prototype.focus = function() {
console.log('focused');
};
const e = new HtmlElement();
console.log(e);
HtmlElement {click: ƒ}
click: ƒ ()
[[Prototype]]: Object
focus: ƒ ()
constructor: ƒ HtmlElement()
[[Prototype]]: Object
e.click();
e.focus();
clicked
focused
2. Let's create and test the basic implementation of the HtmlSelectElement()
constructor.
function HtmlSelectElement(...args) {
if (Array.isArray(args[0])) this.items = args[0];
else this.items = args;
this.addItem = function(item) {
this.items.push(item);
};
this.removeItem = function(item) {
let reducedItems = [];
for (let thisItem of this.items) if (thisItem !== item) reducedItems.push(thisItem);
this.items = reducedItems;
};
}
const s1 = new HtmlSelectElement([1, 2, 3]);
console.log(s1);
HtmlSelectElement {items: Array(3), addItem: ƒ, removeItem: ƒ}
items: (3) [1, 2, 3]
addItem: ƒ (item)
removeItem: ƒ (item)
[[Prototype]]: Object
constructor: ƒ HtmlSelectElement(...args)
[[Prototype]]: Object
3. Let's implement the Inheritance and do a test. The follow is the most important part of the exercise.
Here we can't use the approach described in the lesson JavaScript Prototypical Inheritance – e.g. HtmlSelectElement.prototype = Object.create(HtmlElement.prototype);
Because in this case we will assign only the Prototype of HtmlElement()
to the Prototype of HtmlSelectElement()
. But, as we can see above, we can't inherit the click()
method in this way, because it's defined at the HtmlElement()
Constructor not at its prototype level. So in this case we need to:
HtmlSelectElement.prototype = new HtmlElement();
HtmlSelectElement.prototype.constructor = HtmlSelectElement;
const s2 = new HtmlSelectElement(['a', 'b', 'c']);
console.log(s2);
HtmlSelectElement {items: Array(3), addItem: ƒ, removeItem: ƒ}
items: (3) ['a', 'b', 'c']
addItem: ƒ (item)
removeItem: ƒ (item)
[[Prototype]]: HtmlElement
click: ƒ ()
constructor: ƒ HtmlSelectElement(...args)
[[Prototype]]: Object
focus: ƒ ()
constructor: ƒ HtmlElement()
[[Prototype]]: Object
s2.click();
s2.focus();
clicked
focused
Polymorphism
1. Continuing from the last exercise and extend HtmlSelectElement()
and implement a .render()
method that will output <select>
element and a number of <option>
elements that matching the elements from the items
array.
HtmlSelectElement.prototype.render = function() {
if (Array.isArray(this.items) && this.items.lengtt !== 0)
return '<select>\n'
+ this.items.map(item => `\t<option>${item}</option>`).join('\n')
+ '\n</select>';
};
// Note here we can use template string inside another like this:
return `<select>${this.items.map(item => `\t<option>${item}</option>`).join('\n')}</select>`;
console.log(s3);
HtmlSelectElement {items: Array(3), addItem: ƒ, removeItem: ƒ}
items: (3) ['a', 'b', 'c']
addItem: ƒ (item)
removeItem: ƒ (item)
[[Prototype]]: HtmlElement
click: ƒ ()
render: ƒ ()
constructor: ƒ HtmlSelectElement(...args)
[[Prototype]]: Object
focus: ƒ ()
constructor: ƒ HtmlElement()
[[Prototype]]: Object
console.log( s3.render() );
<select>
<option>a</option>
<option>b</option>
<option>c</option>
</select>
2. Now let's create another constructor that will generate <img>
elements, name it HtmlImageElement()
. It must inherit .click()
and .focus()
from HtmlElement()
and must have its own .render()
method. The new objects must have src
property that will be used within the .render()
method.
function HtmlImageElement(src = '#') {
this.src = src;
}
HtmlImageElement.prototype = new HtmlElement();
HtmlImageElement.prototype.constructor = HtmlImageElement;
HtmlImageElement.prototype.render = function() {
if (this.src.lengtt !== 0) return `<img src="${this.src}" />\n`;
}
const img = new HtmlImageElement('https://metalevel.tech');
console.log(img);
HtmlImageElement {src: 'https://metalevel.tech'}
src: "https://metalevel.tech"
[[Prototype]]: HtmlElement
click: ƒ ()
render: ƒ ()
constructor: ƒ HtmlImageElement(src = '#')
[[Prototype]]: Object
focus: ƒ ()
constructor: ƒ HtmlElement()
[[Prototype]]: Object
img.click();
img.focus();
clicked
focused
console.log(img.render());
<img src="https://metalevel.tech" />
3. Finally create an array of two or more HTML Select and Image elements and print to the console the output of the .render()
method of each of them.
const elements = [
new HtmlSelectElement(['1', '2', '3']),
new HtmlImageElement('https://szs.space')
];
for (let element of elements)
console.log(element.render());
<select>
<option>1</option>
<option>2</option>
<option>3</option>
</select>
<img src="https://szs.space" />
Create Stack with ES6 Classes
By using ES6 Classes, implement a LIFO Stack that has the following members:
push()
add element to the stack,pop()
remove element from the stack,peek()
show the element on top,count
return the length of the stack,show
print the stack.
const _items = new WeakMap();
class Stack {
constructor() {
_items.set(this, []);
}
get show() { return _items.get(this); }
get count() { return _items.get(this).length; }
push(obj) {
const items = _items.get(this);
if (obj === undefined) throw new Error('Invalid value.');
items.push(obj);
}
pop() {
const items = _items.get(this);
if (items.length === 0) throw new Error('Stack is empty.');
return items.pop();
}
peek() {
const items = _items.get(this);
if (items.length === 0) throw new Error('Stack is empty.');
return items[items.length - 1];
}
}
const stack = new Stack();
stack;
Stack {}
count: 0
show: Array(0)
[[Prototype]]: Object
constructor: class Stack
count: 0
show: Array(0)
get count: ƒ count()
get show: ƒ show()
peek: ƒ peek()
pop: ƒ pop()
push: ƒ push(obj)
[[Prototype]]: Object
stack.push('a');
stack.push('b');
stack.push('c');
stack.count;
3
stack.pop();
'c'
stack.peek();
'b'
stack.show;
(2) ['a', 'b']