- https://reinteractive.com/posts/235-es6-classes-and-javascript-prototypes
- https://levelup.gitconnected.com/using-classes-in-javascript-e677d248bb6e?gi=5ba413dea026
Why do we need classes?
Classes are simply templates used to create new objects. The most important thing to remember: Classes are just normal JavaScript functions and could be completely replicated without using the class syntax. It is special syntactic sugar added in ES6 to make it easier to declare and inherit complex objects.
We create a class like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class CircularList { constructor(listName) { // uses scope variables for privacy var _name = listName; // private variable // public function 'setName' // sets private variable _name this.setName = function(listName) { _name = listName;} // public function 'getName' // gets private variable _name this.getName = function() { return _name; } } // end of constructor // function definitions } |
We declare scoped variable in constructor for privacy.
So, a class like this:
1 2 3 4 |
class Example { static iex() {} ex() {} } |
translates to this:
1 2 3 |
function Example() {} Example.prototype.ex = function() {} Example.iex = function() {} |
Constructor
There can be only one special method with the name “constructor” in a class. Having more than one occurrence of a constructor method in a class will throw a SyntaxError error.
A constructor can use the super keyword to call the constructor of a parent class.
If you do not specify a constructor method, a default constructor is used.
Then we implement getter/setter functions for it and attach them to the this object.
Whenever we attach functionality and property to this, its public.
We can then declare other functions that use private properties such as _name.
For example, if we want to implement private removeLastNode that accesses these private properties/functions, we do so in the constructor because removeLastNode can access _name, _head, _tail by scope. removeLastNode is not visible to the outside.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
class CircularList { constructor(listName) { // uses scope variables for privacy var _name = listName; // private variable // public function 'setName' // sets private variable _name this.setName = function(listName) { _name = listName;} // public function 'getName' // gets private variable _name this.getName = function() { return _name; } let removeLastNode = (data, ref) => { console.log("---removeLastNode---") if (dataMatches(data.toUpperCase(), ref.data.toUpperCase())) { console.log("data matches!"); if (_head === _tail) { console.log("head and tail are same") _head.prev = null; _head.next = null; _head = null; _tail.prev = null; _tail.next = null; _tail = null; ref.clean(); return true; } } return false; } } // end of constructor // function definitions } |
public functions that uses private properties/functionalities
Now, say we want to implement remove function that wants to access our private function and private properties, we simply implement the public function inside of the constructor, and access those private functionality and properties via scoping.
Notice our remove function is public because it is attached to this.
It accesses private properties such as _head due to scoping.
It accesses private functions such as removeNodeAtHead(…) due to scoping.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// public function that uses private this.remove = function(data){ if (this.isEmpty()) { console.log("Remove: Nothing to remove because list is empty"); return; } try { let traversal = _head; if (removeLastNode(data, traversal)) { return;} do { if (dataMatches(data.toUpperCase(), traversal.data.toUpperCase())) { if (removeNodeAtHead(traversal)) return; if (removeNodeAtTail(traversal)) return; if (removeNodeInBetween(traversal)) return; } traversal = traversal.next; } while (traversal != _head); } catch(error) { console.log("Warning: "+ error); } } |
functions
functions are attached to the this object, and are public.
If the functions want to use use private properties, they must be in the same scope as the private properties/functionalities, which are in the constructor as shown previously.
In this case, if the functions are outside of the constructor, they use private variables through get/set functions that are exposed on the this object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class CircularList { constructor(listName) { ... } setCurrentToHead() { this.setNow(this.getHead()); return this.getNow(); } step(steps, step_ENUM) { if(!isNaN(steps) && steps > 0) { do { this.setNow((step_ENUM === StepEnum.NEXT) ? this.getNow().next : this.getNow().prev); steps--; } while (steps > 0); } } isEmpty() { return (this.getHead() == null && this.getTail() == null); } print() { if (this.getHead() === null) { console.log("-- LIST IS EMPTY --"); return; } var traversal = this.getHead(); do { traversal.display(); traversal = traversal.next; } while (traversal !== this.getHead() && traversal != null); } } |
functions attached to this are added to the prototype
Take this example, class Test.
Any functions/properties added to Test.prototype is shared among all instances.
Any functions/properties added outside of the constructor function is added to Test.prototype and is hared among all instances.
Any functions/properties added inside of the constructor belongs to the instance itself.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Test { // for construction constructor(newName) { var name = newName; // added to the object itself this.print2 = function() { console.log("print2()"); } } // added to the prototype print() { console.log("print()"); } } // custom prototype function Test.prototype.haha = function() { console.log("--- haha ---"); } let a = new Test("ricky test"); |
you can check like so:
1 |
console.log(a.__proto__.hasOwnProperty('print')); // true |
you can also simply input the code into Firebug and then simply log a.__proto__
you will see that the prototype object will have haha, print, and constructor.
The object itself will have print2.
Thus the methods is being shared by all instances of Test.
Adding methods on the prototypes of objects in JavaScript is an efficient way
to conserve memory (as opposed to copying the methods on each object).
Hi-Jacking these functions on the fly
Prototypes are just objects that can be changed at runtime
1 2 3 4 5 6 |
a.__proto__.print = function() { console.log("HAHA!"); } a.print(); // YIKES! :( b.print(); // DOUBLE YIKES! :( |
Changing the method on the prototype of ‘a’ changes the result on ‘b’ as well.
This is because they share the same prototype and we are changing that object.
Again, prototypes are just objects that can be changed at runtime.
1 2 |
let c = new CircularList("rihanna"); c.print(); |
Oops, there is no going back, we have already modified the prototype for all instances of CircularList, now and in the future.
Remember, classes in JavaScript are not a blueprint like in other languages.
They just define objects that can be modified at will in runtime.
TO RECAP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Car { constructor(name) { // - this is the newly created object // - anything assigned here will be assigned // to the object directly this.kind = 'Car'; this.name = name; } // - class methods are assigned to the prototype of // the newly created object // - this prototype object will be shared by all instances printName() { console.log('this.name'); } } |
Inheritance
Continuing from our Test class example…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Test { // for construction constructor(newName) { var name = newName; // added to the object itself this.print2 = function() { console.log("print2()"); } } // added to the prototype print() { console.log("print()"); } } // custom prototype function Test.prototype.haha = function() { console.log("--- haha ---"); } |
And OralText to extend from Text:
1 2 3 4 5 6 |
class OralTest extends Test { constructor(oralName) { console.log("OralTest - constructor"); super(oralName); } } |
this is what it looks like diagram form:
So we start off with class Test. We declare function print2 inside the constructor, so its a property of the class. Every instance created from this class will have its own print2.
We then declare function print within the class, but outside of the constructor. print will be included in Test’s prototype object, which is simply represented as {}. This is because it derives (has __proto__ referencing) the default Object Prototype Object. There is no class name it derives from so its empty. Then, we directly attach the function haha to Test’s prototype object. Also, constructor is the special function that all instances of this class need to call before instantiation. Thus, that is why Test Prototype Object has haha, print, and constructor.
> Test.prototype
{haha: ƒ, constructor: ƒ, print: ƒ}
haha: ƒ ()
constructor: class Test
print ƒ print()
__proto__:Object
Then we have OralText, which extends from Test. It copies over the constructor properties, hence we get print2.
More importantly, this Test{} is independent from Test’s prototype object. OralText.prototype has a constructor property, which points back to OralTest. Thus, all instances created from OralTest will have OralTest as its type.
Another important thing is that OralTest.prototype is an object instantiated from Test.prototype. That’s why it has a __proto__ pointing to Test.prototype. This is so that we have a hierarchy in case instances of OralTest wants to access its parent’s prototype.
> OralTest.prototype
Test {constructor: ƒ}
constructor: class OralTest
__proto__: Object
As you can see OralTest’s prototype has a constructor pointing to OralTest (itself). This is so that all instances of OralTest will have OralTest as the type.
We also wee that the name of our prototype object is called Test This is because we extend from Test. Its __proto__ is referencing an Object. Let’s analyze this Object:
OralTest.prototype.__proto__
{haha: ƒ, constructor: ƒ, print: ƒ}
haha: ƒ ()
constructor: class Test
print: ƒ print()
__proto__: Object
So as we see, its the Test’s Prototype object with the haha and print functions.
So now
OralFinal.prototype will give a literal object with the name OralTest. Because our OralFinal extends from OralTest. You’ll see the special constructor function.
We are now at the lowest level of the hierarchy. We want to go up the hierarchy. So we follow the __proto__ path. You’ll see its __proto__ is that of Test. This is because OralFinal’s Prototype Object OralTest, is an object instantiated from Test.
OralTest {constructor: ƒ}
constructor :class OralFinal
__proto__: Test
If you go up one more __proto__, aka __proto__ of Test, you’ll see the no name Prototype Object with the functions haha, print, constructor.
Then if you go up one more, you’ll reach the default object Prototype object. That is the highest point.
- Classes are NOT hoisted
- Classes are first-class citizens
- Classes are executed in strict mode
Another Example
Given class PersonCl:
1 2 3 4 5 6 7 8 9 10 11 |
class PersonCl { constructor(fullname, birthYear) { this._fullName = fullName; this.birthYear = birthYear; } calcAge() {...} get age() { return 2037 - this.birthYear; } set fullName(name) { this._fullName = name; } get fullName() { return this._fullName; } static hey() { console.log('hey there'); } } |
1 2 3 4 5 6 7 8 9 10 |
class StudentCl extends PersonC1 { constructor(fName, birthYear, course) { super(fullName, birthYear); this.course = course; } // this.fullName() // calls parent's method // this.course. // calls own property // this.calcAge() // calls parent's method } |