Inheritance and Encapsulation in JS land
Proto_types in JS for season developers
A prototype is an object to which the search for a particular property can be delegated. Prototypes are a convenient means of defining properties and functionality that will be automatically accessible to other objects. Prototypes serve a similar purpose to that of classes in classical object-oriented languages. Indeed, the main use of prototypes in JavaScript is in producing code written in an object-oriented way, similar to, but not exactly like, code in more conventional, class-based languages such as Java or C#.
More importantly, we'll discover why it's important to having prototypes in JS land
Generate Object using functions
In JavaScript, objects are collections of named properties with values. For example, we can easily create new objects with object-literal notation
const obj = {
prop1 : 'a',
prop2 : ()=> {},
prop3 : {}
}
When developing software, we strive not to reinvent the wheel, so we want to reuse as much code as possible
function userCreation(name,score) {
const newUser = {};
newUser.name = name;
newUser.score = score;
newUser.increment = ()=> {
newUser.score++;
}
return newUser;
}
The problem is that every time the user calls this function to create a JS object, it will create the same objects that take up a computer's memory to store. Technically,only the data is dynamic, but the function will remain the same no matter how many times we create an object. Can we do it better?
What if I told you that we could save up some memories by putting all functions on the upper chain and sharing them all with the newly created object?
The power of Object.create
Object.create
is used for creating objects with a specified prototype. This allows you to create objects that inherit properties and methods from another object (the prototype).
const functionStore = {
increment:function() { this.score++; },
login:function() { console.log("you log in"); }
}
function userCreation(name,score) {
const newUser = Object.create(functionStore);
newUser.name = name;
newUser.score = score;
return newUser;
}
Let's have a look at this keyword again.
// [[Prototype]]:Object
const functionStore = {
increment:function() { this.score++; },
login:function() { console.log("you log in"); }
}
This keyword is a very flexible word in JS that depends on the object calling a function, In our case, we group our store function object in higher up the chain and we might not know what the value of the object that executes our function is, but with this, it replies on the object calling our function
newUser.increment;
// It will increase property score of the newUser to 1;
This,call,apply and bind with fns in JS
You might have noticed that putting the share common function in the upper chain is a great way to cut down on the computer's memory resource, but you should also think about how it knows the context of the value is calling to the function. Obviously, there could be a thousand of objects sharing the same function together so this keyword is the way JS offers us to solve this issue
Understand the context and this
const myTruck = {
speed:50
}
function drive() {
console.log(`Driving at ${this.speed}`);
}
Binding the current implementation of a function to the current context invokes function
//Bind first and execute later
const boundDrive = drive.bind(myTruck);
boundDrive();
// Driving at 50
drive.call(myTruck);
// Driving at 50;
drive.apply(myTruck);
// Driving at 50
Difference between bind,call and apply
bind creates a new function with a specified "this" value
call invokes a function immediately with a specified "this" value and individual arguments
apply invokes a function immediately with "this" value and an array of arguments
const person = {
name: "John",
sayHello: function(greeting) {
console.log(`${greeting}, ${this.name}!`);
}
};
const anotherPerson = {
name: "Jane"
};
// Using bind
const greetJohn = person.sayHello.bind(person, "Hello");
greetJohn(); // Outputs: Hello, John!
// Using call
person.sayHello.call(anotherPerson, "Hi"); // Outputs: Hi, Jane!
// Using apply
person.sayHello.apply(anotherPerson, ["Hola"]); // Outputs: Hola, Jane!
Borrow Array.prototype.map to use with string
Array.prototype.map = function(callback, thisArg) {
const newArray = [];
//Whoever calls me must have a length property
for (let i = 0; i < this.length; i++) {
if (this.hasOwnProperty(i)) {
newArray[i] = callback.call(thisArg, this[i], i, this);
}
}
return newArray;
};
// Example to use myMap with a string
function splitter(string) {
return Array.prototype.map.call(string, (x) => x);
}
console.log(splitter('Hello World'));
Why can we call the bind,call, apply on function?
It turns out that everything in JS is an object. Keep in mind that when we create a function in JS, it has a special power that lets us do something significant.
Prototype in JS
We can simplify the Object.create function with prototype in JS and it will work the same
function UserCreator(name,score) {
this.name = name;
this.score = score;
}
// we create an empty object and assign it value with name and score
UserCreator.prototype.increment = function () {
this.score++;
}
UserCreator.prototype.login = function () {
console.log("login");
}
const user1 = new UserCreator("Vince",100);
Note that the prototype property also exists in an array. Think back every time we use array methods like push(), join(), etc. All these built-in array-related functions are actually stored inside an array at Array.prototype
The new keyword
If you notice that we don't have to manually create an object and return an object. The new keyword when calling a function will automatically create an object and return an object for us
The class 'syntatic sugar'
With that solution using the prototype keyword, it's very implicit, but it exposes so many low-level details to the users. We want some magic to somehow replicate the traditional language like Java and C++ do. So welcome to the syntatic sugar class keyword of javascript
class UserCreator {
constructor(name,score) {
this.name = name;
this.score = score;
}
increment() {
this.score++;
}
login() {
console.log("login");
}
}
The static keyword
In the previous examples, you saw how to define object methods (prototype methods), accessible to all object instances. In addition to such methods, classical object-oriented languages such as Java and C# use static methods, which are defined at the class level. Check out the following example:
class Ninja{
constructor(name, level){
this.name = name;
this.level = level;
}
swingSword() {
return true;
}
static compare(ninja1, ninja2){
return ninja1.level - ninja2.level;
}
}
// How to use it
Ninja.compare(new Ninja("Vince",100), new Ninja("MA",1000) );
// Static method is used at the class level
Behind the scene, it's also another syntatic sugar for class-based behaviors
function Ninja(name,level) { };
Ninja.compare = (ninja1,ninja2)=> { }
There're a lot of example out there using static methods (utils for one-time use)
Object.keys, Object.values, Object.freeze, Object.defineProperty,etc
Array.isArray,Array.from
Inheritance in JS
class Person {
constructor(name) {
this.name=name;
}
dance() {
return true;
}
}
class Ninja extends Person {
constructor(name,weapon) {
super(name); // Person.call(this,name);
this.weapon=weapon;
}
wieldWeapon() {
return true;
}
}
const ninja = new Ninja("Bob","knife");
//Let me prove to you classes is just a prototype in JS
Person.prototype.isPrototypeOf(ninja);
Behind the scene
// Equivalent ES5 code
function Person(name) {
this.name = name;
}
Person.prototype.dance = function () {
return true;
};
//Assign properties
function Ninja(name, weapon) {
Person.call(this, name);
this.weapon = weapon;
}
//Link Ninja prototype to Person prototype, otherwise it doesn't work
Ninja.prototype = Object.create(Person.prototype);
Ninja.prototype.constructor = Ninja;
Ninja.prototype.wieldWeapon = function () {
return true;
};
Encapsulation
class Mammal {
constructor(sound) {
this._sound=sound;
}
talk() {
return this._sound;
}
}
class Dog extends Malmal {
constructor(sound) {
super(sound);
}
}
const funky = new Dog("WOOO");
funky._sound; // we can still access this
TS stories
Declare a variable without constructor
class Vehicle {
color:string="red";
}
// equivelant to
class Vehicle {
constructor() {
this.color = "red";
}
}
Typescript adds public, protected and private field modifiers that seem to provide some enforcement
class Diary {
private secret = 'cheated on my English test';
}
const diary = new Diary();
diary.secret;
// Property secret is private and only accessible within class 'Diary'
Don't reply on private to hide information since it's only there to prevent users from accessing it but users can choose not to obey the rules
(diary as any).secret;
Short-way for declaring variable and initialize it
And assign modifier to the variable inside class
public Piece {
constructor(private readonly color:Color,
public autoAssign:string,
private file:File,
private rank:Rank) {
this.position = new Position(file,rank);
}
}
Auto assign the parameter to this
public Piece {
constructor(color,autoAssign,file,rank) {
this.position = new Position(file,rank);
this.color = color;
this.file = file;
this.rank = rank;
this.autoAssign = autoAssign
}
}
Abstract class
We've defined a class Piece but we don't want users to instantiate a new Piece but rather extends from it
abstract class Animal {
// Abstract method (does not have an implementation)
abstract makeSound(): void;
// Regular method
move(): void {
console.log("Moving along...");
}
}
class Dog extends Animal {
// Providing the implementation for the abstract method
makeSound(): void {
console.log("Woof! Woof!");
}
}
class Cat extends Animal {
// Providing the implementation for the abstract method
makeSound(): void {
console.log("Meow! Meow!");
}
}
super
- method calls, like super .take to override parent's method implementation
Constructor calls, which have the super to call constructor function of parent's constructor
Builder pattern
Builder pattern is a way to seperate the construction of an object from the way that object is actually implemented (JQuery, ES6 data structure Map and Set, the style of API looks really the same)
new RequestBuilder()
.setURL("/users");
.setMethod("get");
.setData({firstName:"Anna"})
.send();
// We can guess that it has some default variables
// and we can set via it publics methods
class RequestBuilder() {
constructor() {
this.data = null;
this.method = "get";
this.url = null;
}
}
Implementation
class RequestBuilder {
private data: object | null = null;
private method: "get" | "post" | null = null;
private url: string | null = null;
setMethod(method:"get" | "post"):this {
this.method = method;
return this;
}
setData(data:object):this {
this.data = data;
return this;
}
setURL(url:string):this {
this.url = url;
return this;
}
}
Building sorter class
First attempt
class Sorter {
constructor(public collection:number[]) {}
sort():void{
const {length} = this.collection;
for(let i=0; i<length;i++) {
for(let j=0; j<length-i-1;j++) {
if(this.collection[j] > this.collection[j+1]) {
const leftHand = this.collection[j];
this.collection[j] = this.collection[j+1];
this.collection[j+1] = leftHand;
}
}
}
}
Second attempt
With union operator
class Sorter {
constructor(public collection:number[] | string) {}
sort():void{
const {length} = this.collection;
if(Array.isArray(this.collection){
for(let i=0; i<length;i++) {
for(let j=0; j<length-i-1;j++) {
if(this.collection[j] > this.collection[j+1]) {
const leftHand = this.collection[j];
this.collection[j] = this.collection[j+1];
this.collection[j+1] = leftHand;
}
}
}
}
if(typeof this.collection === "string") {...}
}
Third-attempt
With inversion of control
class NumbersCollection {
constructor(public data:number[]) {}
get length():number {
return this.data.length;
}
compare(leftIdx:number,rightIdx:number):boolean {
return this.data[leftIdx] > this.data[rightIdx];
}
swap(leftIdx:number,rightIdx:number):void {
const leftHand = this.data[lefxIdx];
this.data[leftIdx] = this.data[rightIdx];
this.data[rightIdx] = leftHand;
}
}
class Sorter {
constructor(public collection: NumbersCollection) {}
sort():void {
for(let i=0; i<length;i++) {
for(let j=0; j<length-i-1;j++) {
if(this.collection.compare(j,j+1)){
this.collection.swap(j,j+1);
}
}
}
}
Forth attempt
Giving instructions to other classes on how to do compare and sort(interface)
interface Sortable {
length:number;
compare(leftIdx:number,rightIdx:number):boolean;
sort(leftIdx:number,rightIdx:number):void;
}
Fifth attempt
Building sorting for string
class CharacterCollection extends Sortable {
constructor(public data:string) { }
get length() {
return this.data.length;
}
compare(leftIdx:number,rightIdx:number) {
return this.data[leftIdx].toLowerCase() > this.data[rightIdx].toLowerCase();
}
swap(leftIdx:number,rightIdx:number) {
const characters = this.data.split(/\s+/);
const leftHand = this.characters[leftIdx];
this.characters[leftIdx] = this.characters[rightIdx];
this.characters[rightIdx] = leftHand;
this.data = this.characters.join(" ");
}
}
Sixth attempt
The solution above is fine but it's too verbose
const numberCollections = new NumbersCollection(1,2,3);
const sortNumber = new Sort(numberCollections);
sortNumber.sort();
Passing down the sort methods to all of the collection class would be a better choice
interface Sortable { }
class Sorter {
sort():void {
for(let i=0; i<length;i++) {
for(let j=0; j<length-i-1;j++) {
// Type error => there's no definition of this in parent class
// You can't reference to the methods of its children in parent
if(this.compare(j,j+1)){
this.swap(j,j+1);
}
}
}
}
class NumberCollections extends Sorter{
constructor(numbers:number[]) {
super();
}
}
Abstract class can handle this situation
Can't be used to create an object directly
Only used as a parent class
The implementation methods can refer to other methods don't actually exist yet
Can contain real implementation for some methods
Can make children classes promise to implement some other methods
abstract class Sorter {
// This will be implemented in the future
abstract compare(leftIdx:number,rightIdx:number):boolean;
abstract swap(leftIdx:number,rightIdx:number):void;
abstract length:number;
sort():void {
for(let i=0; i<length;i++) {
for(let j=0; j<length-i-1;j++) {
if(this.compare(j,j+1)){
this.swap(j,j+1);
}
}
}
}
}
class NumbersCollection extends Sorter {};
class CharactersCollection extends Sorter {};
Use-case
Implements streaming from node
var stream = require('stream');
function StatStream(limit) {
stream.Readable.call(this);
this.limit = limit;
}
StatStream.prototype = Object.create(stream.Readable.prototype,{
constructor: {value:StatStream};
})
StatStream.prototype._read = function (size) {
// Must implement it (abstract methods)
}
Interfaces vs Abstract classes
Inheritance in real-life
At this time, we already know the value of inheritance system, so let's inherit some common methods that we don't have to write from scratch
Array methods
Object methods
EventTarget Interface
Event interface
And so on. Even we can build our own class with our business logic and let others inherit from it
DOM with the inhertance concept?
Keep in mind that these are the properties values
Its prototype
Event with inheritance concept?
Its prototype
Inheritance vs composition
//Instead of building from prototype chain , we seperate our
//common functions
const barker = (state)=> ({
//Return a new object with bark method
bark:()=> console.log("Woof, I'm" + state.name);
})
const driver = (state)=> ({
//Return a new object with drive method
drive:()=> state.position = state.position + state.speed
})
//Instead of forcing users to inherit these fns we give them
//the ability to compose it
const muderRobotDog = ()=>{
let state = {
name,
speed,
position:0
};
return Object.assign({} , barker(state) , driver(state) );
}
const murderRobot = murderRobotDog();
murderRobot.bark();
murderRobot.drive();