Skip to content

Instantly share code, notes, and snippets.

@marcinjahn
Last active August 17, 2020 18:29
Show Gist options
  • Save marcinjahn/9187c0bfa146e5dc6876fc5db9ba8014 to your computer and use it in GitHub Desktop.
Save marcinjahn/9187c0bfa146e5dc6876fc5db9ba8014 to your computer and use it in GitHub Desktop.

OOP

There are 3 ways of using objects:

  • Object Literals
  • Constructor Functions
  • Classes

Object Literals

let person = {
    firstName: "John",
    lastName = "Wick",
    //short method syntax, specific to object literals:
    sayHello() { console.log("hello") }
} 

//Dynamically adding properties
person.age = 29

//Methods (could be also defined on the object with firstName, etc. )
person.isAdult = function() { return this.age >= 18 }

Listing members

//for...in
for (let member in person {
    ...
})

//Object.keys:
let members = Object.keys(person)

Equality

For objects, the reference is compared.

==

Not type-safe.

===

Type-safe.

NaN != NaN

+0 == -0

Object.is(obj1, obj2)

NaN == NaN.

+0 != -0

Constructor Functions

function Person(firstName, lastName, age) {
    this.firstName = firstName
    this.lastName = lastName
    this.isAdult = function() { return this.age >= 18 }
}

//"new" keyword creates a new object, invokes Person function,
//and sets the context of "this" to that new object
let person1 = new Person("John", "Wick", 29)
person1.isAdult()

Object.create()

Both object literals and constructor functions use Object.create() underneath.

let person = Object.create(
    Object.prototype,
    {
        firstName: {value: 'John', enumerable: true, writable: true, configurable: true},
        lastName: {value: 'Wick', enumerable: true, writable: true, configurable: true},
        age: {value: 29, enumerable: true, writable: true, configurable: true}
    }
)

It's not used often, the other ways are prefered. The above is the same as:

let person = {
    firstName: "John",
    lastName = "Wick",
    age: 29
} 

Merging Objects

let person = {
    firstName: "John",
    lastName = "Wick",
    sayHello() { console.log("hello") }
}

let person2 = {
    age: 20
}

Object.assign(person2, person1)
//Now person2 contains all properties and methods of person1 and age.

//New merged object without modifying existing ones
let merged = Object.assign({}, person1, person2)

Object Properties

Property Descriptor

Every property fo an object has a descriptor:

let descriptor = Object.getOwnPropertyDescriptor(person, "firstName")

It returns an object:

Object {
    value: "John",
    writable: true, //the value can be changed from its initial value
    enumerable: true,
    configurable: true
}

Writable

We can modify the descriptor:

Object.defineProperty(person, "firstName", {writable: false})

Now, the property becomes read only and if we try to change it we get an error:

TypeError: Cannot assign to read only property 'firstName' of object '#'

However, if firstName was an object, it would be still possible to change its properties (but not firstName itself).

let person = {
    name: {
        first: "John",
        last = "Wick",
    }
}

Object.defineProperty(person, "name", {writable: false})
person.name.first = "Jim" //works

To completely "lock" and object, it needs to be frozen:

Object.freeze(person.name)

Enumerable

Controls whether object can be enumerated with for...in loop or Object.keys.

We can disable enumeration for some proeprty:

Object.defineProperty(person, "firstName", {enumerable: false})

Now, firstName will not show up in results of enumeration. It also affects JSON serialization with JSON.stringify(person). The firstName property will not be serialized.

Configurable

It controls whether:

  • the property descriptor's enumerable and configurable cannot be changed
  • the property can be deleted from the object or not
Object.defineProperty(person, 'firstName', {configurable: false})

//The lines below will throw errors:
Object.defineProperty(person, "firstName", {enumerable: false})
Object.defineProperty(person, "firstName", {configurable: true})
delete person.firstName

Once it's done, it cannot be changed back! Only writable stays changeable.

Getters and Setters

let person = {
    name: {
        first: "John",
        last: "Wick",
    }
}

Object.defineProperty(person, "fullName",
    {
        get: function() {
            return this.name.first + " " + this.name.last
        },
        set: function(value) {
            let nameParts = value.split(' ')
            this.name.first = nameParts[0]
            this.name.last = nameParts[1]
        }
    })
    
console.log(person.fullName) //John Wick
person.fullName = "Saul Goodman"
console.log(person.name.first) //Saul
console.log(person.name.last) //Goodman
console.log(person.fullName) //Saul Goodman

Prototypes

  • it is an object that exists on every function ({}). When a function is created, there is an Object created in memory, with the same name as the function. This object is a prototype of the function.
  • objects do have a prototype, but they do not have protoype property (undefined). It is available at person.__proto__ (Object {})
  • prototype has a constructor property that points to a function that created it

Function's prototype is an Object instance that is given as a prototype object for every object created from that function as a constructor.

Object's prototype is the same object that the constructor function had (they refer to the same object in memory).

Example:

function Person(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
}
Person.prototype.age = 29

console.log(Person.prototype) //{ age: 29 }

let jim = new Person("Jim", "Smith")
console.log(jim.age) //29

jim.age = 19
console.log(jim.age) //19
console.log(jim.__proto__.age) //29

age property exists on Person's prototype. When we create some object from that constructor function, it will have the same prototype (same object reference).

When we request age on jim, JS:

  • looks under jim - there is no age
  • looks under jim.__proto__ - there is age and it is used

When we change the value of jim.age only this is changed. The prototype's value does not change.

The same behaviour is for methods.

hasOwnProperty

function Person(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
}
Person.prototype.age = 29

let jim = new Person("Jim", "Smith")
console.log(jim.hasOwnProperty('age')) //false

jim.age = 18
console.log(jim.hasOwnProperty('age')) //true

Inheritance

All objects in JS inherit from Object and Object has no prototype (null).

function Person(firstName, lastName, age) {
    this.firstName = firstName
    this.lastName = lastName
    this.age = age
    this.isAdult = function() {
        return this.age >= 18
    }
    Object.defineProperty(person, "fullName",
    {
        get: function() {
            return this.firstName + " " + this.lastName
        },
        enumerable: true
    })
}

function Student(firstName, lastName, age) {
    //Calling Person constructor function in a context of the new Student object
    //Thanks to it this new object gets the firstName, 
    //lastName, age and isAdult()
    Person.call(this, firstName, lastName, age)
    
    this.enrolledCourses = []
    
    this.enroll = function(courseId) {
        this.enrolledCourses.push(courseId)
    }
    
    this.getCourses = function() {
        return `${this.fullName}'s courses: ${this.enrolledCourses.join(", ")}.`
    }
}
Student.prototype = Object.create(Person.prototype) //we create a new prototype object for Student, however its own prototype is set to Person's prototype
Student.prototype.constructor = Student //the above line causes the constructor to be set to Person. We change it back to Student

//Alternatively, the 2 lines above could be just:
Object.setPrototypeOf(Student.prototype, Person.prototype)

The 3 things are really the key in defining inheritance:

  • calling base function in a constructor of a new type
  • creating a new prototype based on base's prototype
  • setting the prorotype's constructor back to the correct one

Static members

Person.adultAge = 18
Student.fromPerson = function(person) {
    return new Student(person.firstName, person.lastName, person.age)
}

console.log(Person.adultAge) //18
let student = Student.fromPerson(somePerson)

Classes

It is just syntactic sugar for the previous approach.

class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }
    
    get fullName() {
        return this.firstName + " " + this.lastName
    }
    
    set fullName(value) {
        let nameParts = value.split(' ')
        this.name.first = nameParts[0]
        this.name.last = nameParts[1]        
    }
    
    isAdult() {
        return this.age >= 18
    }
}

let jim = new Person("Jim", "Smith")

Getters and setters are set as enumerable: false by default. To change that a property descriptor needs to be modified. Getters and setters are defined on prototype, while other properties and methods are defined on the instances directly.

Object.defineProperty(Person.prototype, 'fullName', {enumerable: true})

Now, fullName is enumerable.

Inheritance

class Student extends Person {
    constructor(firstName, lastName, age) {
        super(firstName, lastName, age)
        this.enrolledCourses = []
    }
    
    enroll(courseId) {
        this.enrolledCourses.push(courseId)
    }
    
    getCourses() {
        return `${this.fullName}'s courses: ${this.enrolledCourses.join(", ")}.`
    }
}

Static members

static keyword is for defining static members.

class Student {
    ...
    
    static fromPerson(person) {
        return new Student(person.firstName, person.lastName, person.age)
    }
    
    static adultAge = 18
}

let person = new Person("John", "Wick", 29)
let student = Student.fromPerson(person)
console.log(Student.adultAge)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment