OOP Javascript

OOP in JavaScript is dynamic, meaning it doesn't provide a native class implementation.

The class keyword is introduced in ES2015 (ES6) just as a form of syntactical sugar. Apart from that, JavaScript remains prototype-based.

//prototypal OOP
var x = 5
window.x //prints 5

Of course, it does implement OOP principles such as:

  • Encapsulation: Bundling data (properties) and methods within objects, limiting access to certain properties using closures or private fields.

  • Abstraction: Hiding the complexity of an object's internal implementation while exposing only necessary details via methods.

  • Inheritance: Objects can inherit properties and methods from other objects through prototypes, allowing reuse of code.

  • Polymorphism: Objects can share a method name but implement it differently, allowing for different behaviors based on the object type.

Implementation

OOP in JavaScript has various implementation techniques, with the same "under the hood" behavior:

  • Object.create()

  • Constructor Functions

  • ES6 Classes

Prototypal inheritance

Object.create()

Object.create() takes advantage of the prototypal nature of JavaScript. It takes an optional object argument which will be the prototype of the new object, without storing the same methods twice.

//Object.create()
const elfFunctions = {
  attack: function() {
    return 'atack with ' + this.weapon
  }
}
function createElf(name, weapon) {
  newElf = Object.create(elfFunctions) //it adds a hidden property __proto__
  // to the empty newElf object, pointing to the methods of elfFunctions
  newElf.name = name;
  newElf.weapon = weapon
  return newElf
}


const sam = createElf('Sam', 'bow');
const peter = createElf('Peter', 'bow');
sam.attack()
sam.hasOwnProperty('name') //returns true
sam.hasOwnProperty('attack') //returns false

Constructor Functions

Constructor functions achieve the same thing, but the use of the new keyword "automates" some operations such as removing the need for the return statement.

//Constructor Functions
function Elf(name, weapon) {
  this.name = name;
  this.weapon = weapon;
  // if called with "new" it returns the object automatically
}

Elf.prototype.attack = function() { //this adds the "attack" method as prototype
                                    // of Elf
  return 'atack with ' + this.weapon
}
const sam = new Elf('Sam', 'bow');
const peter = new Elf('Peter', 'bow');
sam.attack()

//it is a common practice to differentiate Constructor functions
//from regular functions by starting their name with a capital letter

Classes

Classes in JavaScript achieve the same thing as above but have been mainly introduced as syntactic sugar as they are easier to read and write.

//ES6 classes
class Elf {
  constructor(name, weapon) {
    this.name = name;
    this.weapon = weapon;
  }
  attack() {
    return 'atack with ' + this.weapon
    //this basically adds "attack" as a prototype method of any objects
    //created with the constructor
  }
}

const sam = new Elf('Sam', 'bow');
const peter = new Elf('Peter', 'bow');
//even is called multiple times, the "attack" method is stored only once in memory
sam.attack()
console.log(sam instanceof Elf) // returns true
sam.hasOwnProperty('name') //returns true
sam.hasOwnProperty('attack') //returns false

In this example of ES6 classes, the Elf class defines a constructor for setting up instances with name and weapon properties. The attack() method is defined on the prototype, meaning it’s shared among all instances (like sam and peter), saving memory because the method is stored only once. The attack() method can be called by any Elf instance, but it’s not an instance property, which is why hasOwnProperty('attack') returns false. This demonstrates how JavaScript's class-based inheritance manages memory efficiently.

JavaScript introduced private fields in ES2022, providing true privacy in classes. These private fields are prefixed with a # symbol and are only accessible within the class they are declared in. For example:

class Example {
  #privateField = 42;
  
  getPrivateField() {
    return this.#privateField;
  }
}

Here, #privateField is only accessible within the Example class and cannot be accessed or modified directly from outside. This allows for proper encapsulation and data hiding in JavaScript OOP.

Inheritance and polymorphism

//inheritance and polymorphism
class Character {
  constructor(name, weapon) {
    this.name = name;
    this.weapon = weapon;
  }
  attack() {
    return 'atack with ' + this.weapon
  }
}

class Elf extends Character { 
  constructor(name, weapon, type) {
    // console.log('what am i?', this); this gives an error
    super(name, weapon) //calls the prototype object's constructor
    //while also inheriting its methods
    console.log('what am i?', this);
    this.type = type;
  }
}

class Ogre extends Character {
  constructor(name, weapon, color) {
    super(name, weapon);
    this.color = color;
  }
  makeFort() { // this is like extending our prototype.
    return 'strongest fort in the world made'
  }
  attack() { //this overwrites the prototype "attack" method
    return 'aaargh'
  }
}

const houseElf = new Elf('Dolby', 'cloth', 'house')
//houseElf.makeFort() // error
houseElf.hasOwnProperty('type') //true
houseElf.hasOwnProperty('name') //still true
houseElf.hasOwnProperty('attack') //false
houseElf.attack() //"atack with cloth"


const shrek = new Ogre('Shrek', 'club', 'green')
shrek.makeFort() //"strongest fort in the world made"

In this example, inheritance is demonstrated by the Elf and Ogre classes extending the Character class, allowing them to inherit properties (name, weapon) and methods like attack() from Character. The super() call in their constructors initializes the parent class.

Polymorphism is shown by the Ogre class overriding the attack() method with its own version (aaargh), allowing the Ogre class to behave differently when the attack() method is called. The Elf class, on the other hand, uses the inherited attack() method as-is.

Last updated

Was this helpful?