JavaScript OOP Course 6: ES6 Classes: Difference between revisions
(One intermediate revision by the same user not shown) | |||
Line 272: | Line 272: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Let's imagine we want to '''make the <code>radius</code> property''' from the | Let's imagine we want to '''make the <code>radius</code> property''' from the above class '''private''', so it can't be accessible from outside. Within ES6 Classes there are three approaches to do that. | ||
=== Using _Underscore Naming Convention === | === Using _Underscore Naming Convention === | ||
Line 291: | Line 291: | ||
=== Private Members Using ES6 Symbols === | === Private Members Using ES6 Symbols === | ||
'''In ES6 we have a new primitive type called Symbol. <code>Symbol()</code>''' is a ES6 function we called to generate a '''Symbol''' primitive value which represents | '''In ES6 we have a new primitive type called Symbol. <code>Symbol()</code>''' is a ES6 function we called to generate a '''Symbol''' primitive value which represents '''an unique identifier'''. Note it is not a constructor (or class) so we '''don't need to use the <code>new</code>''' operator, otherwise we will get an error. Every time we call the <code>Symbol()</code> function it generates an unique value. If we do a comparison like <code>Symbol() === Symbol()</code> we will get <code>false</code>. | ||
So let's define a new variable of this type (by using the underscore naming convention) and the unique value generated by the <code>Symbol()</code> function to name a property of an object by using a Bracket notation.<syntaxhighlight lang="javascript" class="code-continue"> | So let's define a new variable of this type (by using the underscore naming convention) and the unique value generated by the <code>Symbol()</code> function to name a property of an object by using a Bracket notation.<syntaxhighlight lang="javascript" class="code-continue"> |
Latest revision as of 08:23, 20 June 2024
References
- Code with Mosh: The Ultimate JavaScript Mastery Series – Part 2
- W3School: JavaScript Tutorial
- BabelJs.io a JavaScript compiler. Use next generation JavaScript, today.
See also:
- JavaScript Objects: Classes for Objects definition
- JavaScript Functions: This Keyword in JavaScript
- JavaScript Functions: Changing
this
- JavaScript OOP Objects: Constructors and Classes
- JavaScript OOP Objects: Function's Methods –
.call()
and.apply()
ES6 Classes
- Classes are a new way to create Objects and Prototypical Inheritance.
- Classes in JavaScript are not like classes in other languages like C#, Java and so on.
- Classes in JavaScript are syntactic sugar over Prototypical Inheritance. It is important to know how the Prototypical Inheritance works before learning this new syntax which is cleaner and simpler.
- Creating new objects by ES6 Classes enforce the use of the
new
operator. - JavaScript engine executes the body of the Classes in Strict Mode, no matter
'use strict';
is engaged or not.
Let's begin with the following Constructor function that will be converted to ES6 Class.
function Circle(radius) {
this.radius = radius;
this.draw = function() {
console.log('draw');
};
}
Circle.prototype.area = function() {
return this.radius * this.radius * Math.PI;
};
const circle1 = new Circle(1);
console.log(circle1);
Circle {radius: 1, draw: ƒ}
radius: 1
draw: ƒ ()
[[Prototype]]: Object
area: ƒ ()
constructor: ƒ Circle(radius)
[[Prototype]]: Object
We can rewrite this code using ES6 Classes in the following way. Step 1. Define the body of the class. In this body we can define properties and methods.
class Circle {
}
Step 2. There is a special method that is called constructor()
and it is used to initialize the objects. The constructor()
method is like the Constructor function shown above. When we define a method inside the special constructor()
method this new method will be part of the new
object instance.
class Circle {
constructor(radius) {
this.radius = radius;
this.draw = function() {
console.log('draw');
};
}
}
Step 3. Define the prototype members – in this case the .area()
method. When we define a method outside the special constructor()
method this new method will be part of the prototype of new
object. When we defining methods outside the constructor()
we can use the simplified syntax shown below.
class Circle {
constructor(radius) {
this.radius = radius;
this.draw = function() {
console.log('draw');
};
}
area() {
return this.radius * this.radius * Math.PI;
}
}
Step 4. Create a new object and inspect it.
const circle2 = new Circle(1);
console.log(circle2);
Circle {radius: 1, draw: ƒ}
radius: 1
draw: ƒ ()
[[Prototype]]: Object
area: ƒ area()
constructor: class Circle
[[Prototype]]: Object
Let's look for the typeof Circle
class. Tit is a function – the ES6 Classes are essentially functions!
typeof Circle
'function'
Hoisting
Let's discuss what we have learned about the Functions. In JavaScript are available two way to define a function:
- Function Declaration Syntax. These functions are hoisted – which means the JavaScript engine raise them to the top of the program when executing it. So we can use a function defined by Function Declaration Syntax before its definition.
sayHello(); function sayHello() { console.log('Hello'); }
Hello
- Function Expression Syntax. These functions are not hoisted. It is like when we defining a variable that contains a primitive – number, string, etc.
sayHello(); const sayHello = function() { console.log('Hello'); }; // Should be determined by semicolon
Uncaught ReferenceError: Cannot access 'sayHello' before initialization at index.js:1
We can define ES6 Classes by also using a Declaration or Expression Syntax – but note in both syntaxes the Classes are not hoisted!
- Class Declaration Syntax. This is the simpler and cleaner syntax.
class Circle { // Class definition }
- Class Expression Syntax. This is rarely used syntax – probably you won't see it in the practice.
const Circle = class { // Class definition }; // Should be determined by semicolon
Static Methods
In classical object oriented languages we have two tips of methods:
- Instance methods – in JavaScript they can be at the Object level or at the Prototype level – these methods are available at the instance of a Class which is an Object.
- Static methods. – these methods are available on the Class itself (not at the Object instance). We have often used them to create utility functions that are not specific to a given object.
class Circle {
constructor(radius) {
this.radius = radius;
}
draw() { // Instance method, it will be available at new Circle objects
console.log('draw');
}
static create(str) { // Static method, it will not be available at new Circle objects
const radius = JSON.parse(str).radius;
return new Circle(radius);
}
}
- In the above example the expression
JSON.parse(str).radius
tests whether the input argumentstr
is valid JSON format and extract theradius
property if it is available.
const circle = new Circle(1);
console.log(circle);
Circle {radius: 1}
radius: 1
[[Prototype]]: Object
draw: ƒ draw()
constructor: class Circle
[[Prototype]]: Object
circle.draw();
draw
We can see the .create()
method is not available at the class instance – the circle
object.
circle.create('{"radius": 1}');
Uncaught TypeError: circle.parse is not a function
at index.js:18
But the .create()
method is available at the class Circle
itself.
Circle.create('{"radius": 1}');
Circle {radius: 1}
radius: 1
[[Prototype]]: Object
draw: ƒ draw()
constructor: class Circle
[[Prototype]]: Object
As it can be seen above, with this implementation we can create new instances (objects) of the class Circle
in the following way.
const c = Circle.create('{"radius": 1}');
One more time:
- We use static methods to create utility functions that are not part of particular object.
The this
Keyword
The this
keyword references the Object that is executing the current Function. For example if a function is method of an Object this
references to that Object itself. Otherwise if that function is a regular function
(which means it is not part of certain Object) it references to the global object which is the Window Object in the browsers and Global Object in Node.js.
this
within Functions and Methods
Let's declare a constructor function (by using Function Expression Syntax) and create an object instance of Circle.
const Circle = function(radius) {
this.radius = radius;
this.logThis = function() { console.log(this); };
};
const c = new Circle(1);
If we use the Method Call Syntax for the .logThis()
method, we are going to see the new Circle object in the console, because this
of the .logThis()
method points to that object – remember the new
operator which creates a new object and set this
from the Constructor to points to that new object.
c.logThis(); // this is 'method call syntax'
Circle {radius: 1, draw: ƒ}
radius: 1
logThis: ƒ ()
[[Prototype]]: Object
constructor: ƒ (radius)
[[Prototype]]: Object
Now let's get a reference to the method in a variable.
const logThis = c.logThis; // by omitting () we just creating a reference to a function and not calling it
Note: By omitting ()
at the end of a function (or method) we just creating a reference to that function and not calling it. To prove that let's log the variable to the console – we will see the function itself not the output of its execution.
console.log(logThis);
ƒ () { console.log(this); }
Now if we use Function Call Syntax for the variable containing the reference – run it just as regular function, we will see the Window Object, that is the default context of the regular functions.
logThis(); // this is 'function call syntax'
Window {window: Window, self: Window, document: document, name: '', location: Location, …}
...
...
Strict Mode changing behavior of this
within Functions
When we enable the Strict Mode, by placing 'use strict';
in the beginning of our program, the JavaScript engine will be more sensitive, it will do more error checking and also it will change the behavior of of the this
keyword within functions. So if enabling the Strict Mode the output of the above function will be undefined
instead of the Window Object.
'use strict';
// the rest code of the program...
logThis();
undefined
Note:
'use strict';
cannot be used within the console, it should be part of our script file.- When we enable
'use strict';
optionthis
of the functions not longer point to the global object which is the Window Object in the bowsers and Global Object in Nod.js The reason for this is to prevent us from accidently modifying the global object.
this
Keyword and ES6 Classes
JavaScript engine executes the body of the Classes in Strict Mode, no matter 'use strict';
is engaged or not in the beginning of our program. Let's define a Circle
class with .logThis()
method and do repeat the above steps – we will see undefined
in the console.
class Circle {
constructor(radius) {
this.radius = radius;
}
logThis() { console.log(this); }
}
const c = new Circle(1);
const logThis = c.logThis;
logThis();
undefined
Abstraction and Private Members
Abstraction means hiding the details and complexity and showing only the essential parts. This is one of the core principle in OOP. In order to implement abstraction we use Private Properties and Methods. So certain members of an object wont be accessible from the outside.
class Circle {
constructor(radius) {
this.radius = radius;
}
}
const c = new Circle(10);
c.radius;
10
Let's imagine we want to make the radius
property from the above class private, so it can't be accessible from outside. Within ES6 Classes there are three approaches to do that.
Using _Underscore Naming Convention
Using underscore in the beginning of the name of a property is not actually a way to make the property private (it will be accessible from the outside) it is just naming convention used by some programmers. This is not abstraction this is a convention for developers. It doesn't prevent another developer from writing code against these underscored properties.
class Circle {
constructor(radius) {
this._radius = radius;
}
}
const c = new Circle(10);
c._radius;
10
Private Members Using ES6 Symbols
In ES6 we have a new primitive type called Symbol. Symbol()
is a ES6 function we called to generate a Symbol primitive value which represents an unique identifier. Note it is not a constructor (or class) so we don't need to use the new
operator, otherwise we will get an error. Every time we call the Symbol()
function it generates an unique value. If we do a comparison like Symbol() === Symbol()
we will get false
.
So let's define a new variable of this type (by using the underscore naming convention) and the unique value generated by the Symbol()
function to name a property of an object by using a Bracket notation.
const _radius = Symbol();
class Circle {
constructor(radius) {
this[_radius] = radius;
}
}
const c = new Circle(10);
console.log(c);
Circle {Symbol(): 10}
Symbol(): 10
[[Prototype]]: Object
constructor: class Circle
[[Prototype]]: Object
We can see our Circle object has one property named Symbol()
. If we set multiple properties using Symbols, the property names all will show up a Symbol()
but internally they are unique. This property is semi private - we cannot access this property directly in our code.
console.log(Object.getOwnPropertyNames(c));
[]
From the above command we see our object c
doesn't have any regular property.
console.log(Object.getOwnPropertySymbols(c));
[Symbol()]
0: Symbol()
length: 1
[[Prototype]]: Array(0)
The above command returns an array of Symbols() so if we get the first element of this array we can get the radius value by using this as property name of our Circle object c
.
const key = Object.getOwnPropertySymbols(c)[0];
const radius = c[key];
console.log(radius);
10
To make a Method private by using Symbol, we need to define another Symbol and use ES6 feature called Computed Property Names.
const _radius = Symbol();
const _draw = Symbol();
class Circle {
constructor(radius) {
this[_radius] = radius;
}
[_draw]() {
console.log('draw');
}
}
const c = new Circle(10);
console.log(c);
Circle {Symbol(): 10}
Symbol(): 10
[[Prototype]]: Object
Symbol(): ƒ [_draw]()
constructor: class Circle
[[Prototype]]: Object
Private Members Using ES6 WeakMaps
WeakMap is a new type in ES6 to implement Private Properties and Methods in an Object. A WeakMap is essentially a dictionary where keys are objects and values can be anything, and the reason we call them weak maps is because the keys are weak. So if there are no references to these keys, there will be garbage collector.
const _radius = new WeakMap();
class Circle {
constructor(radius) {
_radius.set(this, radius);
}
}
const c = new Circle(10);
console.log(c);
Circle {}
[[Prototype]]: Object
constructor: class Circle
[[Prototype]]: Object
In the above expression _radius.set(this, radius);
_radius
is our WeakMap,.set()
is a method of the WeakMap type,this
is the key (the current object in our case),radius
is the property we set to that key.
We can access a Property defined by a WeakMap in the following way.
const _radius = new WeakMap();
class Circle {
constructor(radius) {
_radius.set(this, radius);
}
getRadius() {
return _radius.get(this);
}
}
const c = new Circle(10);
console.log(c.getRadius());
10
In order to define a Private Method with this approach we need another WeakMap.
const _radius = new WeakMap();
const _move = new WeakMap();
class Circle {
constructor(radius) {
_radius.set(this, radius);
_move.set(this, function() {
console.log('move', this); // we added 'this' here in order to see what it means in this context
});
}
getRadius() {
return _radius.get(this);
}
getMove() {
return _move.get(this)(); // because it will return a function we are calling that function by ()
}
}
const c = new Circle(10);
c.getMove();
move undefined
We can see the private ._move()
method returns undefined
for this
. This is because the body of the class is executed in Strict Mode and the callback function is just a regular function – read the section above . We can solve this problem by using Arrow Function (as it is shown below) or we could change this
of the callback function in some other way.
const _radius = new WeakMap();
const _move = new WeakMap();
class Circle {
constructor(radius) {
_radius.set(this, radius);
_move.set(this, () => {
console.log('move', this); // we added 'this' here in order to see what it means in this context
});
}
getRadius() {
return _radius.get(this);
}
getMove() {
return _move.get(this)(); // because it will return a function we are calling that function by ()
}
}
const c = new Circle(10);
c.getMove();
move Circle {}
Getter and Setters with ES6 Classes
Let's remind you, in order to define Getter and Setters when we use Constructor functions, we've used the Object.defineProperty()
method like is shown below. Also we've made some properties private by defining variables inside the Constructor.
function Circle(radius) {
let _radius = radius;
Object.defineProperty(this, 'radius', {
get: function() {
return radius;
},
set: function(value) {
if (value <= 0) throw new Error('Invalid radus value!');
radius = value;
}
});
}
const c1 = new Circle(10);
c1.radius;
10
c1.radius = 15;
c1.radius;
15
Defining Getters and Setters within ES6 Classes is much easier. Note in the following example we have one property called radius
, made private by WeakMap, so it is not accessible from the outside.
const _radius = new WeakMap();
class Circle {
constructor(radius) {
_radius.set(this, radius);
}
get radius() {
return _radius.get(this);
}
set radius(value) {
if (value <= 0) throw new Error('Invalid radus value!');
_radius.set(this, value);
}
}
const c2 = new Circle(20);
c2.radius;
20
c2.radius = 25;
c2.radius;
25
Inheritance and ES6 Classes
Let's start with one class called Shape
, which has one method called .move()
.
class Shape {
move() {
console.log('move');
}
}
Now let's create another class called Circle
which has its own method called .draw()
, but we want to inherit also the .move()
method from the Shape
class. Within ES6 Classes this operation is much easier and cleaner than of the approach used with Constructor functions – we do not need to change the Prototype and reset the Constructor.
class Circle extends Shape {
draw() {
console.log('draw');
}
}
Now let's create a Circle object and inspect it.
const c1 = new Circle();
console.log(c1);
Circle {}
[[Prototype]]: Shape
draw: ƒ draw()
constructor: class Circle
[[Prototype]]: Object
move: ƒ move()
constructor: class Shape
[[Prototype]]: Object
Let's imagine all of our shapes need a color – so we nee to ad such property at the Shape
class.
class Shape {
constructor(color) {
this.color = color;
}
move() {
console.log('move');
}
}
At this point, if we have a constructor at the base class (in this example Shape
), when we creating derived class (in this example Circle
) which have its own constructor we mist initialize the base class constructor by using the super
keyword, otherwise we will get an error.
class Circle extends Shape {
constructor(color, radius) {
super(color);
this.radius = radius;
}
draw() {
console.log('draw');
}
}
Now let's create a new Circle object, set values for the color
and radius
properties and inspect it.
const c2 = new Circle('red', 10);
console.log(c2);
Circle {color: 'red', radius: 10}
color: "red"
radius: 10
[[Prototype]]: Shape
draw: ƒ draw()
constructor: class Circle
[[Prototype]]: Object
move: ƒ move()
constructor: class Shape
[[Prototype]]: Object
Method Overriding and ES6 Classes
class Shape {
move() {
console.log('Shape move');
}
}
class Circle extends Shape {
move() {
console.log('Circle move');
}
}
const c1 = new Circle();
Now if we call c1.move()
we will see the child implementation of the method is used.
c1.move();
Circle move
The reason for that goes back to the Prototypical Inheritance – so when accessing a property or method the JavaScript engine walks on the Prototypical Inheritance Tree from the child all the way to the parent and evaluate the first accessible property.
Let's imagine we have scenario where we want reuse some of the code that have been implemented at the parent move()
method – in that case we can call that by using the super
keyword.
class Circle extends Shape {
move() {
super.move();
console.log('Circle move');
}
}
const c2 = new Circle();
c2.move();
Shape move
Circle move