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 propertiesperson.age = 29
//Methods (could be also defined on the object with firstName, etc. )person.isAdult = function() { return this.age >= 18 }
let person = {}
is the same as let person = new Object()
.
Listing members
//for...infor (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 objectlet person1 = new Person("John", "Wick", 29)person1.isAdult()
Object.create()
Both object literals and constructor functions use Object.create()
underneath.
It’s called “pure prototypal inheritance”.
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};
Another usage:
let person = { firstName: "Default", lastName: "Default", age: 0};
let jack = Object.create(person);jack.firstName = "Jack";
This creates an empty jack
object and sets person
as its prototype. The
default values stay on prototype, jack
can set its own values for them:
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 oneslet 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 ‘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
andconfigurable
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 Wickperson.fullName = "Saul Goodman"console.log(person.name.first) //Saulconsole.log(person.name.last) //Goodmanconsole.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. - “normal” objects do have a prototype, but they do not have
protoype
property (undefined
). It is available atperson.__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. This object
instance contains the methods such as bind
, call
, apply
. This is how
functions inherit these methods.
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;Person.prototype.getFullName = function() { return this.firstName + " " + this.lastName;}
console.log(Person.prototype) // { age: 29 }
let jim = new Person("Jim", "Smith")console.log(jim.age) // 29
jim.age = 19console.log(jim.age) // 19console.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 noage
- looks under
jim.__proto__
- there isage
and it is used
When we change the value of jim.age
only this is changed. The prototype’s
value does not change.
We added the getFullName
on the prototype, and not in the constructor
directly, because this way the function is created just once in memory. If we
were creating it in the constructor every instance of Person
would have a
separate object for this method!
There is a Prototype Chain. Every objech has prototype. If some property/method is not found on an object, JS engine looks into the prototype of that object. If it’s not there either, it looks into prototype’s prototype, and so on.
The same behaviour is for methods. We can even add more stuff to the prototype after the objects that use this prototype have been created. These objects will get the new functionality!
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 = 18console.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 prototypeStudent.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 = 18Student.fromPerson = function(person) { return new Student(person.firstName, person.lastName, person.age)}
console.log(Person.adultAge) //18let student = Student.fromPerson(somePerson)
Classes
It is just syntactic sugar for the previous approach. Classes in JS are objects!
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(", ")}.` }}
Under the hood, it just sets Person
as a prototype (__proto__
) of Student
.
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)