Motivation

C# and modern JavaScript are widely used to write applications in OOP design. However, JavaScript was not born as a OOP language, so that it is relatively weaker than C# in terms of native language features to support OOP, although it has evolved quickly with many new language features that bridge the gap of C# or Java as OOP languages.

In this section, it compares C# with JavaScript in terms of Encapsulation, Inheritance and polymorphism, which are three pillars of OOP design to see how they support OOP in a different way.


1. Class and Object

C# is such a class-based language that it has to create a class before creating an object based on it. In terms of inheritance, a sub-class can inherit from its parent class.

JS is such a prototype-based language that it does not need to create a class before creating objects. Nearly all reference types are objects. It uses prototypal inheritance, meaning an object can inherit from another object.


2. Encapsulation

Encapsulation is like a black box that hides implementation details. It is assumed that you give input to this black box, it always produces the expected output, but anyone outside the the black box will never know how the result is come up from inside. This can help prevent inside members from accidental changes of outside operation, which improves the safety and reliability of applications.

For creating classes, the best practice is to encapsulate the fields (implementation details) and only expose their behaviour (what they do instead of how they do it) and properties to the public.

C#

Access modifier:

  • public, private, protected, internal are 4 common access modifiers to control the accessibility of class, fields, properties and methods in a class.

Property:

  • A property is a class’s member that can read, write and compute the value of a private filed (also called backing fields) by get and set methods. They are exposed to the public and help hide the private fields.

  • Auto-implemented property:

    If you do not need to compute the value by get and set methods, it is more elegant to declare auto-implemented property that automatically generates corresponding private backing fields behind the scene with less code.

    class Car 
    {
        // private field
        private int color; 
          
        // public property
        public int Color 
        {  
            get
            {
                return this.color;
            }
            set 
            {
                if (value > 0) {
                    this.color = value;
                }
            }
        }
          
            //Alternatively, use auto-implemented properties
            public int Color {get; set;}
    }                                                                                                       
    

JS

Access modifier:

  • JS does not have access modifiers for accessibility control. Fields and methods declared in a class are always accessible to the public (like public in C#).

    By adding # before fields and methods such as #field and #method, these fields and methods are private from the outside, but they have limited browser support at the moment. see more

  • Common workarounds:

    • **underscore_:** by prefixing _ such as **_field** and **_method**, these fields and methods are regarded as private members as a convention by other developers, but they are still accessible from the outside technically.

Property:

  • The definition of property is the same as C#‘s property. It is worth mentioning that fields written in constructor method will be initialised during instance creation. As such, these initialised fields are accessible from the outside and become this new instance’s properties in JS.
  • Below is the example of how property with get and set methods in class syntax or constructor function.
//class syntax
class Car {
  constructor(color) {
    this._color = color === undefined ? 0 : color;
  }

  get color() {
    return this._color;
  }
  set color(value) {
    this._color = value;
  }
}

let car = new Car(1);
car.color = 2;
console.log(car);
//output: Car { _color: 2 }

//Constructor Functions
function Car() {
    this._color = 0;
}

/* 
Add the property to the Car's prototype, because 
get and set methods in the property color should be reused
*/
Object.defineProperties(Car.prototype, {
    color: {
        get: function () {
            return this._color;
        },
        set: function (value) {
            this._color = value;
        }
    }
});

3. Inheritance (is-a relationship)

Inheritance is the mechanism that bases a class or object on another class(class-based inheritance) or object(prototype-based inheritance), retaining similar implementation. This can improve code reusability.

It is worth mentioning that inheritance might cause tightly-decoupling design in applications if used inappropriately. Often times, we may consider composition(has-a relationship), a design pattern that makes a class as the attribute of another class to improve code reusability along with maintaining code flexibility, which can produce a more loose-coupling design.

Constructor inheritance: base() VS super()

C#

class Inheritance {
    static void Main() {
        var bigCar = new Jeep("toyota", "jeep");
        bigCar.PrintInfo();
    }
}

public class Car{
    public string Make{get; set;}
    
    public Car(string make){
        this.Make = make;
        Console.WriteLine("the parent constructor executed");
    }
    
    public virtual void PrintInfo(){
        Console.WriteLine(Make);
    }
}

public class Jeep: Car {
    public string Type{get; set;}
    
    public Jeep(string make, string type): base(make) {
        Console.WriteLine("the child constructor executed");
        this.Type = type;
    }

    public override void PrintInfo(){
         base.PrintInfo();
         Console.WriteLine(Type);
         /*
         Alternation:
         Console.WriteLine(Make + "\n" +Type);
         */
    } 

}
/*
output:
the parent constructor executed
the child constructor executed
toyota
jeep
*/

JS

class Car{
    constructor(make){
        console.log("the parent constructor executed")
        this._make = make;
    };
    
    printInfo(){
     console.log(this._make);
    }
}

class Jeep extends Car{
    constructor(make, type){
        /*
        must call super before assessing 'this' or returning
        from derived constructor
        */
        super(make);
        console.log("the child constructor executed")
        this._type = type;
    }
    
    printInfo(){
        super.printInfo();
        console.log(this._type);
        /*
        Alternation:
        console.log(this._make);
        console.log(this._type);
        */
    }
}

   const jeep = new Jeep("toyota", "jeep");
   jeep.printInfo();

/*
output:
the parent constructor executed
the child constructor executed
toyota
jeep
*/

Prototypical inheritance in JS

Unlike a class-based language C# that use classes to create inheritance, JS does not have a real class implementation because of its dynamic feature. The “class” keyword in JS is only a syntactic sugar for creating a function (all functions are objects in JS). Under the hood, JS classes will be converted to constructor functions and use prototype object and prototype chain to create prototypal inheritance.

In JS, all reference types are objects, and all objects are instances of Object(the most base object) which sits on top of prototype chain.

For better performance, the constructor function should contain the fields (attributes) only, so that they can be filled with different data on each instance object created basing on this constructor function. Then, the methods (behaviour) should be assigned to this constructor function’s prototype object, so that these methods can be shared across either sub-classes or instance objects.

/*
The class style is converted to the original style for inheritance
using constructor function under the hood
*/ 

function Car(make){
	console.log("the parent constructor executed")
	this._make = make;
}

Car.prototype.printInfo = function(){
	console.log(this._make);
}

function Jeep(make, type) {
    Car.call(this, make);
		console.log("the child constructor executed")
		this._type = type;
}

Jeep.prototype = Object.create(Car.prototype);
Jeep.prototype.constructor = Jeep;

Jeep.prototype.printInfo = function () {
		Car.prototype.printInfo.call(this);
    console.log(this._type);
}

const jeep = new Jeep("toyota","jeep");
jeep.printInfo();

/*
output:
the parent constructor executed
the child constructor executed
toyota
jeep
*/

4. Polymorphism

Polymorphism is a mechanism that allows the parent class to have many forms during inheritance. In other words, sub-classes should have the ability to inherit the behaviour of their parent class and some of the behaviour (some methods) can be overridden to form a different implementation, which improve the flexibility and robustness of the code.

Method overriding

In C#, virtual and override keywords are used to override a parent class’s method, while JS does not require keywords to override.

Similarly, super.parentMethod in JS and base.parentMethod in C# can be used to refer to the parent method.

Abstract Class

Abstract classes are exclusive in C#. It blends the normal classes with interface, providing both concrete implementation of methods and abstraction of methods.

class Inheritance {
    static void Main() {
        var bigCar = new Jeep("toyota", "jeep");
        bigCar.PrintInfo();
        bigCar.Run();
    }
}

public abstract class Car{
    public string Make{get; set;}
    
    public Car(string make){
        this.Make = make;
        Console.WriteLine("the parent constructor executed");
    }
    
    public virtual void PrintInfo(){
        Console.WriteLine(Make);
    }
    
    public abstract void Run();
}

public class Jeep: Car {
    public string Type{get; set;}
    
    public Jeep(string make, string type): base(make) {
        Console.WriteLine("the child constructor executed");
        this.Type = type;
    }
    
    public override void PrintInfo(){
         base.PrintInfo();
         Console.WriteLine(Type);
    }
    
    public override void Run(){
        Console.WriteLine(Type + " is running");
    }
}

/*
the parent constructor executed
the child constructor executed
toyota
jeep
jeep is running
*/

Interface

An interface is a contract that declares the capabilities a class should provide. It is different from class and abstract class. An interface is purely a declaration and only can declare methods(no body) and properties, but no fields (because fields are implementation details).

class Interface {
    static void Main() {
        var car = new Car("toyota");
        car.PrintInfo(); //output: toyota
    }
}

public interface ICar{
    string Make{get; set;}
    
    void PrintInfo();
}

public class Car: ICar{
    public string Make{get; set;}
    
    public Car(string make){
        this.Make = make;
    }
    
    public void PrintInfo(){
        Console.WriteLine(Make);
    }
}