Introduction to Objects in Javascript

Introduction to Objects in Javascript

I am currently interviewing for Frontend Developer roles and I failed one of the interviews because I was not good at Object Oriented Javascript. Writing this article to serve me as a goto article for any interview preparation in future and for others who want to learn. I have compiled all the resources from MDN Docs, FreeCodeCamp and Javascript.info.

Lets dive into the discussion without wasting time.

Primitive and Non-primitive data-types

There are 8 data types in Javascript

  • number - numbers of any kind
  • bigint - numbers of large length
  • string - set of characters wrapped within quotes (" " or ' ') or backticks (` `)
  • null - unknown values
  • undefined - unassigned values
  • boolean - true or false values
  • symbol - for unique identifiers
  • object - for complex data structures

Refer here for more information.

Except object all others are primitive datatypes. Objects are used to store collections of data in the form of key-value pairs.

Introduction to Objects

A object is as simple as {}

typeof {} //"object"

Object creation

Objects can be created in two ways

  1. Object Construtor Syntax

     let person = new Person()
    
  2. Object Literal Syntax

     let person = {}
    

Objects usually have properties and methods. We can add properties into a object in the form key:value. Lets add two properties in person object

let person = {
  name: "john",
  age: "24"
}

name and age are the properties and "john" and "24" are their respective values.

Accessing properties

Using Dot (.) notation

We can access the properties in the format <objectName>.<propertyName>

let person = {
  name: "john",
  age: "24"
}

// Accessing name
console.log(person.name) // "john"
console.log(person.age) // 24

Using Square ([]) brackets

The square brackets come in handy when we have multi-word key i.e., object key with more than one word and has a space (' ') in between the words.

let person = {
  name: "john",
  age: "24",
  "favourite language": "javascript" // note the special way in which key is defined
}

console.log(person["name"]) // "john"
console.log(person["favourite language"]) // "javascript"

If we use the dot (.) notation to access multiword properties, JavaScript will throw an error. Javascript searches for property favourite in the object person. and then gives a syntax error when comes across unexpected language.

console.log(person.favorite language)

The square bracket notation is also used to evaluate an expression and find a property of that result

let key = "favourite language"
console.log(person[key]) // javascript

Adding and Deleting properties to objects

We can add new properties to object using the dot notation after object has been created. To the same person object from above, we can add new property called email as below

person.email = "test@xyz.com"

console.log(person.email) // test@xyz.com

You can delete a property by object using delete followed by <objectName>.<propertyName>

delete person.age // deletes age property

References and copying

Most questions in the interviews around the object, if you are beginner, will be around this topic

The key difference between and primitive data types and objects is that Objects are stored and copied by reference

Let us understand the above statement with below example

// consider a variable of primitive type 
let a = 4;

// suppose you want to create a copy of the above variable
let b = a;

console.log(a) // 4
console.log(b) // 4

// at some point later in your code you change the value of a to be something else
a = 5;

console.log(a) // 5
console.log(b) // 4

In the case of primitive data types the values are copied as a whole i.e., each variable has a separate location in the memory and contains its own copy of the value. The above example stands true for all primitive data types.

Now lets check similar scenario with Objects

// consider a object person with name and email properties

let person = {
  name:"john",
  email : "john@xyz.com"
}

// you want to make a copy of this object

let newPerson = person

// you want to change the value of newPerson to be something else

newPerson.name = "doe"

console.log(person.name) // "doe"
console.log(newPerson.name) // "doe"

This happens because Objects are stored as references. There is a single object with parameters name and email in memory and refernced by two varaibles person and newPerson. We are making change to same object, hence those changes are reflected in both the variables.

Copying Objects

As we have seen above, normal copy just creates a refernce to the same object. If we want to create a separate clone of the object, we can loop through the entire object using for...in loop and copy each property.

Using Object.assign()

The other way we can do it is using Object.assign() method. According to MDN documentation, it can be used to copy its own enumerable properties. It cannot be used to copy nested objects. The nested object is still copied as a reference.

let person = {
  name:"john",
  email : "john@xyz.com"
}

let newPerson = {}
Object.assign(newPerson, person)

newPerson.name = "doe"

console.log(person.name) // "john"
console.log(newPerson.name) // "doe"
}

Using Spread Operator (...)

Spread operator was added in ES6. It is used to spread or expand an iterable such as array or object. It can be used to copy objects. This copies without reference.

const user = { name: "john", age: "24" }
const newUser = {...user}
console.log(newUser) // { name: "john", age: "24" }

newUser.name = "lewis"
console.log(newUser.name) // "lewis"
console.log(user.name) // "john"

Deep Copying

Copying using Object.assign() and Spread Operator (...) does not copy the nested object - object which contains object in it. The nested part of object is still copied as a reference.

We can do a Deep Copy of an object using JSON.stringify() and JSON.parse()

const user = {
  name: "john", 
  age: "24", 
  address: {
    state: "Texas",
    country: "USA"
  }
}

const newUser = JSON.parse(JSON.stringify(user))
newUser.address.state="California"

console.log(user) 
// { name: "john", age: "24", address:{ state: "Texas", country: "USA" }}
console.log(newUser) 
// { name: "john", age: "24", address:{ state: "California", country: "USA" }}

Object Methods

Objects along with properties have methods. Lets add a method sayHello() to our userobject.

const user = {
  name:"john",
  age: "24",
  sayHello: function() {
    console.log(`Hello ${user.name}`);
  }
}

user.sayHello() // "Hello john"

The idea behind having methods in objects is to allow only the methods to manipulate the object. The above code would work perfectly fine. But if the object is copied to another object and we call the sayHello() method, it will cause an error as it will try to access the method from another object.

let user = {
  name: "John",
  age: 30,
  sayHi() {
    console.log( user.name ); // leads to an error
  }
};

let admin = {...user};
user = null; // overwrite to make things obvious
admin.sayHi();

To overcome this, we need to use this keyword while refering any property of the object inside its method. this inside the method of an object refers to the enclosing object (the object in which the method is present).

The this behaves differently in JavaScript. Its value is evaluated at the run time depending upon the context. Please do give this article a read. Arrow functions don't have their own this. They derive it from their enclosing Lexical Context

Constructors

So far we have seen how to create objects. What if we need to create multiple objects of the same type for example a list of users, list of todo items, we need to use Constructors along with new keyword to create objects.

Constructors are functions that create new objects - they define both the properties and methods that can act upon these properties. It serves as a blue print to the create objects.

Constructors are functions with just two conventions

  1. First letter of function should be capital
  2. Constructors should be called only with new keyword
function User(name, age) {
  this.name = name,
  this.age = age
}

The above snippet creates a Constructor with name User. Constructors use this keyword to set the properties of the new object. Here, this refers to the new object being created. The above Constructor defines two properties name and age and sets their values to the parameters in Constructor definition.

const user = new User("john", "24")
console.log(user.name) // "john"

We can create a new user as above. The new operator tells JavaScript to create a new instance of User object. Pass the values for name and age as arguments to the constructor call. We can have the methods in the constuctor function in the same way we have the methods in normal objects.

function User(name, age) {
  this.name = name,
  this.age = age,
  this.sayHi = function() {
    console.log(`Hi ${this.name}`)
  }
}

const user = new User("john", "24")
user.sayHi() // "Hi john"

Instance of

When we create a new object using constructor, the new object is said to be instance of the constructor. This can be verified using instance of operator. It returns true if it is instance of the constructor

function User(name, age) {
  this.name = name,
  this.age = age,
  this.sayHi = function() {
    console.log(`Hi ${this.name}`)
  }
}

const user = new User("john", "24")
console.log(user instanceof User) // true

The instanceof operator can be used to distinguish between normal objects and objects created normally.

Call, apply and bind

The this keyword's value is calculated at the runtime. Inside a event listener or when the Object's method is passed as a callback to setTimeout(), this context is lost. The value of this will be the element that recieved the event in event listener function. I

To overcome this JavaScript provides, with bind() method. Along with bind() it is good to have idea about call() and apply() methods.

call() and apply()

This is a function borrowing method in Javascript. The call() method calls a function with a given this context and arguments provided induvidually.

const user1 = {
  fname: "john",
  lname: "doe",
  getFullName: function() {
    console.log(`Hello ${this.lname}, ${this.fname}`) 
  }
}

user1.getFullName() // "Hello doe, john"

Consider there exists a object user2 which does not have the getFullName implementation. We can call getFullName and provide user2 as the context.

const user2  = { 
  fname: "Alfred", 
  lname: "Marshall" 
}

user1.getFullName.call(user2) // "Hello Marshall, Alfred"

It is a good practice to have these functions outside. We can pass additional arguments to the call function induvidually. Apply works the same way as call but it takes the additional arguments in an array

const user1 = {
  fname: "John",
  lname: "Doe",
}

const greetNewJoiner = function(teamName, location) {
  console.log(`Hello ${this.lname}, ${this.fname}`) 
  console.log(`We are happy to welcome you to our ${location} office and into ${teamName} team`)
}

const user2  = { 
  fname: "Alfred", 
  lname: "Marshall" 
}

greetNewJoiner.call(user1, "Frontend" , "California")
// "Hello Doe, John"
// "We are happy to welcome you to our California office and into Frontend team"
greetNewJoiner.call(user2, "Devops", "California") 
// "Hello Marshall, Alfred"
// "We are happy to welcome you to our California office and into Devops team"

// Apply example
greetNewJoiner.apply(user2, ["Devops", "California"]) 
// "Hello Marshall, Alfred"
// "We are happy to welcome you to our California office and into Devops team"

bind()

Bind creates a new function and returns the function with newly bound this context.

const user = {
  fName: "Kevin",
  sName: "Edwards",
}

const greetUser =  function() {
  console.log(`Welcome ${this.sName}, ${this.fName}`)
}

const admin = {
  fName: "John",
  sName: "Doe",
}

const greetAdmin = greetUser.bind(admin)

greetAdmin()
// "Welcome Doe, John"

Most of the questions in interview will be around these three methods.

That's all for this article. Please let me know if I need to include anything else.

Thanks for reading.