Object-Oriented JavaScript (Part - 2)

Object-Oriented JavaScript (Part - 2)

ยท

6 min read

In the previous article we learned about some concepts of object-oriented javascript. Let's continue that article and learn more about it.

Overriding Derived Properties

You can add properties to an object. If the property already exists, it will not affect the global property and the object uses the new property.

Let's understand what does it mean with our previous rabbit example.

Rabbit.prototype.teeth = "small";
console.log(killerRabbit.teeth); 
-> small

killerRabbit.teeth = "long";
console.log(killerRabbit.teeth);
-> long

Overriding property is a useful thing as it shows exceptional behavior in the generic class of objects. Also, it doesn't affect the other objects to take the standard form.

console.log(blackRabbit.teeth);
-> small

Overriding gives a different type of toString() method to standard functions and arrays.

console.log(Array.prototype.toString() == Object.prototype.toString);
->false

console.log([1, 2].toString());
-> 1, 2

toString works same as .join(", "). It joins the values of an array by comma and produces a different string.

Maps

We already learned about the map function but it is different from that function. It's a class to map keys and values of any type. Although we already have the object for key-value mapping, the drawback of it is that it only supports string keys.

So, if we want to map whose values cannot be converted to string then "maps" is a good option.

let ages = new Map();
ages.set("Borris", 39);

console.log(ages.has("Jack"));
-> false

console.log(ages.has("toString"));
-> false

Also, it doesn't have toString property but in the case of Object.prototype it has in-built in it. So, if you do something like

let ages = {
    Borris: 39,
    Liang: 40
}

console.log("toString" in ages);
-> true

toString will give true. Even though we don't name any key as toString. One way to make it false is by using Object.create and pass null to it. Then it will not derive from Object.prototype.

console.log("toString" in Object.create(null));
-> false

Back to Map class, the methods set, get, and has are part of the interface of the Map object.

Polymorphism

Polymorphism is writing code with values of different shapes. Like string, function call the toString method to create a meaningful string.

We can also write our own version of the toString method.

Rabbit.prototype.toString = function(){
     return `a ${this.type} rabbit`;
};

console.log(String(blackRabbit));
-> a black rabbit

This is a very powerful idea. Now you can attach this method to any interface that supports it and it will work.

Symbol

Symbol is a built-in object which is used to create unique property keys to an object. It prevents colliding a property key in an object added by any other code.

Every Symbol() gives a unique Symbol. Every Symbol.for("key") gives the same Symbol for a given "key". It always looks in the Symbol registry for the key and if it didn't find it then create a new Symbol and add that to the registry.

let sym1 = Symbol();
let sym2 = Symbol('foo');
let sym3 = Symbol('foo');

Symbol('foo') === Symbol('foo')
-> false

Also, if we use new keyword to explicitly create Symbol object. It throws an error. If you want to create, you have to use Object() function.

let sym = Symbol('foo');
typeof sym                           //Symbol

let symObj = Object(sym);
typeof symObj                    //object

Iterator Interface

Iterator is an object which defines a sequence and potentially returns a value upon its termination. It has a next method that returns an object with two properties.

value: next value in iteration
done: returns true when the last value of a sequence get consumed

Once created, an iterator object can be iterated explicitly by repeatedly calling next. Most common iterator is Array iterator.

Iterators are consumed only as necessary. Because of this, iterators can express sequences of unlimited sizes, such as the range of integers between 0 and infinity.

function makeRangeIterator(start=0, end=Infinity, step=1){
     let nextIndex = start;
     let count= 0;
     const rangeIterator = {
            next: function(){
                  let result;
                  if(nextIndex < end){
                        result = {value: nextIndex, done: false}
                        nextIndex += step;
                        count++;  
                  return result;
                 }
                 return {value: count, done: true}
            }
      };
      return rangeIterator;
}

Now, you can use it like this ๐Ÿ‘‡

const it = makeRangeIterator(1, 10, 2);
let result = it.next();
while(!result.done)
{
     console.log(result.value);         //1 3 5 7 9
     result = it.next();
}

In the above example, we made a simple range iterator that defines the sequence of integers from start(inclusive) to end(exclusive) by step apart. Its final return value will be the size of the sequence tracked by iterationCount.

Getters, Setters and Statics

You can use getters methods to access the properties that hide the method call. They defined by writing get keyword in front of the method in an object or class.

let varyingSize = {
     get size(){
        return Math.floor(Math.random()*100);
     }
};

console.log(varyingSize.size());

You can do a similar thing when a property is written to, using the setter method.

class Temperature{
     constructor(celsius){
         this.celsius = celsius;
    }
}

get fahrenheit()
{
       return this.celsius*1.8 + 32;
}

set fahrenheit(value){
     return this.celsius = (value - 32)/1.8;
}

static fromFahrenheit(value)
{
     return new Temperature((value-32) / 1.8 );
}

let temp = new Temperature(22);
console.log(temp.fahrenheit);

temp.fahrenheit= 86;
console.log(temp.celsius);

The Temperature class allows you to read and write the temperature in either Celsius or Fahrenheit and converts to and from Celsius in the fahrenheit getter and setter method.

Sometimes you want to attach some properties directly to the constructor. The method that has static keyword written before their name is stored in the constructor.

Inheritance

Imagine we need a data structure that behaves like a matrix. We can write from scratch but it likes repeating the whole code.

So, we will use something called "inheritance". Now, what inheritance is?

With the help of this concept, we can derive the properties and behavior of an old class to the new class but with new definitions.

class SymmetricMatrix extends Matrix {
  constructor(size, element = (x, y) => undefined) {
    super(size, size, (x, y) => {
      if (x < y) return element(y, x);
      else return element(x, y);
    });
  }

  set(x, y, value) {
    super.set(x, y, value);
    if (x != y) {
      super.set(y, x, value);
    }
  }
}

let matrix = new SymmetricMatrix(5, (x, y) => `${x},${y}`);
console.log(matrix.get(2, 3));

The use of the keyword extends shows that it is derived from some other class. The class from which it is derived is called superclass and this class will be called subclass.

To initialize this class, it uses the superclass's constructor using super keyword. It is important because if it wants to behave like a Matrix class it needs the properties of the Matrix class.

The set method again uses the super but this time to class a specific method of Matrix class.

Inheritance allows us to build slightly different data types from existing data types with relatively little work. It is a fundamental part of the object-oriented.

instanceof Operator

It checks whether the particular object was derived from a particular class or not.

console.log(new SymmetricMatrix(2) instanceof SymmetricMatrix);
-> true

console.log(new SymmetricMatrix(2) instanceof Matrix);
-> true

console.log([1] instanceof Array);
-> true

instanceof will set through the inherited types that's why SymmetricMatrix is instanceof Matrix and the operator can also be applied to standard constructors like Array.

So, this is all about object-oriented JavaScript.

thankyou.gif

ย