There are 3 ways of using objects:
- Object Literals
- Constructor Functions
- Classes
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 }
//for...in
for (let member in person {
...
})
//Object.keys:
let members = Object.keys(person)
For objects, the reference is compared.
Not type-safe.
Type-safe.
NaN != NaN
+0 == -0
NaN == NaN.
+0 != -0
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()
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
}
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)
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
}
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 notfirstName
itself).let person = { name: { first: "John", last = "Wick", } } Object.defineProperty(person, "name", {writable: false}) person.name.first = "Jim" //worksTo completely "lock" and object, it needs to be frozen:
Object.freeze(person.name)Controls whether object can be enumerated with
for...in
loop orObject.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 withJSON.stringify(person)
. ThefirstName
property will not be serialized.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.firstNameOnce it's done, it cannot be changed back! Only
writable
stays changeable.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
- 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 atperson.__proto__
(Object {}
)- prototype has a
constructor
property that points to a function that created itFunction'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 onPerson
's prototype. When we create some object from that constructor function, it will have the same prototype (same object reference).When we request
age
onjim
, JS:
- looks under
jim
- there is noage
- looks under
jim.__proto__
- there isage
and it is usedWhen we change the value of
jim.age
only this is changed. The prototype's value does not change.The same behaviour is for methods.
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')) //trueAll objects in JS inherit from
Object
andObject
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
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)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.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
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)