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 kindbigint
- numbers of large lengthstring
- set of characters wrapped within quotes (" " or ' ') or backticks (` `)null
- unknown valuesundefined
- unassigned valuesboolean
- true or false valuessymbol
- for unique identifiersobject
- 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
Object Construtor Syntax
let person = new Person()
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
- First letter of function should be capital
- 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.