JavaScript Objects
Last updated
Was this helpful?
Last updated
Was this helpful?
Objects are the foundation of JavaScript and permeate its every aspect. Almost everything in JavaScript is an object. In fact, only six things are not objects. They are — null, undefined
,strings
, strings, numbers, Boolean, and symbols. These are called primitive values or primitive types.
Anything that is not a primitive value is an Object. That includes arrays, functions, constructors, and objects themselves. Yes! Functions and arrays are objects too.
Just like other primitive values, objects are values too and they serve the same purpose: Representing information or data. When we create a variable and assign a value to it, we are holding information in it for later retrieval and processing. For example —
In this case, firstName
holds and represents the first name of a person. In a sense, strings are called primitive values because they can represent only one type of information. In this case, a collection of characters or a string.
What happens when we want to represent a person as a whole? That is, I want to hold on to first name (String), last name (String), date of birth (Date), and her friends (Collection of Strings). If I create four variables for each datum, representing three persons would require 4 * 3 = 12 variable names. That’s not good!
In some languages, this question would segue into a discussion about user-defined data types or custom data types. Constructs such as classes, , unions types etc. can be used to achieve that. However, JavaScript does not have classes (even though it has a class
keyword), or structures. It only has objects for creating custom data types and associating behavior.
In JavaScript, an object can be thought of as a distinct entity that can have properties associated with it. These properties define the object’s characteristics and behavior. In our person example, an object can have 4 properties describing it — first name, last name, DOB, and friends.
Here’s how ECMA specification defines an object —
An Object is logically a collection of properties.
We are going to examine objects by the kind of operation we can perform on them. The operations are —
Creation — Creating a new object.
Addition — Adding properties to an object.
Reading/Retrieving — Reading the value associated with a key.
Existence — Whether a property exists in an object.
Updation — Updating the values of existing properties.
Deletion — Deleting existing properties.
Iteration (Enumeration) — Looping over an object’s keys and values.
Comparison — Comparing two different objects.
Copying — Making a copy of an existing object
Properties of an object are just like variables. Let’s see how can we create objects and properties —
A literal is a notation for representing a fixed value in source code. For example, 5
or "foo"
. They literally represent the value. An object is represented as —
The curly braces { }
create a new object.
You can also specify properties to be inserted at the time of creation.
The property names are treated as strings. That is, firstName
and 'firstName'
are same. An object property name can be any valid JavaScript string, or anything that can be converted to a string, including the empty string.
Note that the values can be any value type. That is, both primitive types and an object can be used as values.
The nestedObj
is a key that holds another object. The nesting can be arbitrary and cyclic too!
You can also use an expression that will be evaluated to a string using the new computed property syntax. As an example —
Note the variable propertyName
. In obj
, we wrap the expression propertyName.toUpperCase()
in [ ]
to make it a computed property. As expected, in the resulting output, the stringfirstName
is turned into uppercase.
In an object, when we have a function as a value to a key, then the function is called a method. This is one of the most important features of objects. The methods have access to the key-value pairs of the object. They can specify behavior in an object.
After we have created our object, we can add properties to it. There are two distinct ways of adding properties —
The object and the property name is separated by a .
(dot). If a property with the same name exists, its value will be overwritten. Otherwise, a new property with property name address
will be added to the object.
An object can only contain a single key with one value. We can’t have a single key having two different values.
Caveat: Using the dot-notation, you can’t use property names that are numbers, or string that contain spaces, or special character, or dynamic properties. For example, these will throw a SyntaxError
.
If you don’t want to be limited by the above caveats, you may consider using bracket notation.
Here we are adding address
property to the obj
object.
A few things to note —
The part between the brackets is an expression. That means we can use variables there. See the example below.
Executing obj.propName
adds a property propName
whereas obj[propName]
adds a property after evaluating the variable propName
. The content of propName
is address
, so a property address
with the value Earth
is added to obj
.
Also, these are now valid —
Retrieving a property is by far the most complex to understand in JavaScript. Let’s see some simple examples —
Like addition, we can use the dot-notation or bracket notation get the value of a property. If the property exists, we get its value. If the property does not exist, we get undefined
.
Objects are not just containers of key-value pairs. They have one more very interesting property. They can have a parent. That is, they store a link to another object. This parent object is also consulted when a property is read.
And not just that. That parent can also have a parent. And that grandparent object is consulted too when a property is read. This repeats until we reach an object that does not have any parent i.e the parent is null
.
Here’s the rough algorithm used when we retrieve a property —
Goal: Read middleName
from the object obj
. (Note that middleName
does not exist in obj
)
(Rough) Algorithm —
Let CurrentObject = obj
. Let CurrentKeyName = middleName
.
If CurrentObject has CurrentKeyName, return associated value.
Else, let CurrentObject = parent of CurrentObject.
Check for CurrentKeyName again in CurrentObject. If found, return value.
If CurrentObject has no parent and does not have CurrentKeyName, return undefined
.
In the last step, CurrentObject would contain the final parent-less object. By this time, we have checked every object in the chain and none of them have the property that we are looking for. If the property is found at any level, its value is returned.
This chain is called the “prototype” chain. By default, all objects have their parent as Object.prototype
. There is no parent to Object.prototype
. That’s where the chain ends, normally.
The [[Prototype]]
property keeps this reference to the parent object. It’s an internal property which means that we can’t access it directly. We can use Object.getPrototypeOf
function to get the parent (aka “prototype”) of an object.
An important point to note is that the prototype chain is consulted only while reading a value. It does not affect addition, updating or deletion of properties on parent objects. One way to look at this is — You have read-only access to all things your parent (and their parents) have.
If the property does not exist in an object(or its parents), we get undefined
. What happens when we intentionally set a property’s value to undefined
?
In this case, we have the property but its value is purposely not defined. When we retrieve the value, we’ll get undefined
. But how can we be sure that its value in intentionally undefined
or the property does not exist on the object?To ascertain that we need to show the existence of a property.
Sometimes we don’t care what value a property has. We only want to know whether the given object has the given property or not. There are 2 ways to know that —
The syntax is String (or Symbol) in
object. It will evaluate to true
or false
depending on whether the property exists.
Attentive readers may be surprised to see the last example ( 'isPrototypeOf
'
in obj
) evaluate to true
. This is the gotcha of in
operator.
isPrototypeOf
is a property of the parent of obj
. Just like Reading a property, the entire prototype chain is consulted before true
or false
is returned. If the property is found at any level, true
is returned.
What if we want our own properties? Properties that exist only on the current object in question irrespective of whether that same property exists on parents or not.
It’s a function that can be accessed through an object because of prototypes. It takes string key name ( or symbol) as an argument.
Updating is simple. It’s just like addition. You can use dot-notation or bracket-notation. Here’s an example —
It has the side effect of creating a new property firstName
and setting the value Timothy
if the property did not exist. If you don’t want that, you should pair your code with existence checks.
Also, updating a property always affects the object on which it is performed. It does not modify any object in the prototype chain.
Deletion is performed using thedelete
operator. Again, we can use both the notations.
The return value of delete
operator is true
if the property was successfully deleted. Else, it will be false
. We can make properties un-deletable. So, it is important to check the output of delete
.
Also, merely setting a property’s value to undefined
won’t delete the said property.
An object is a collection of key-value pairs. Often times you want to process the set of all values or keys and create another object with transformed values or key names. Unlike arrays, you can’t simply iterate an object. Here are a few ways to iterate objects —
for-in
loop is just like a normal for
loop except that it iterates on an object. You get the properties one by one. Here’s an example using obj
object —
Note —
The order of appearance of properties is not fixed in an object.
for-in
loop will return all string properties. It won’t give Symbol properties.
The prototype chain is consulted, and all enumerable properties of parents are also returned. (In short, enumerable properties are those properties which are visible to looping constructs.)
Because the prototype chain is consulted, we may get unwanted properties in a for-in
loop. We can put an existence check using hasOwnProperty
to only get own properties, but we have better ways to do the same thing now.
Object.keys()
is similar to for-in
loop except that it only returns the object’s own keys in an array. That is, the prototype chain is not consulted. We can then iterate on this array using a for-of
or a for
loop. Here’s an example —
Attentive readers will see that we have to manually read the value inside the loop using const value = obj[property];
. What if we only want the values?
Object.values()
has similar restrictions as Object.keys()
, but it returns an array of values instead of the keys.
What if we want both values and properties at the same time?
Same as .keys()
and .values()
except that we get an array of [key, value]
pairs.
One restriction the above methods pose is that they only return string properties and not symbols in an object. What if we want both?
ownKeys
return both string-based and symbol properties. Note that we are using Reflect
module, and not Object
module.
There are no good in-built of ways of comparing two objects. When you use the ==
or ===
operators, they only compare the references of the objects. References can be understood as “memory addresses” of the objects. Only primitives types are compared by values.
Now, two objects can have the same key-values pairs but they can’t occupy the same location in memory or reference. The object is only equal to itself as the example shows us —
See exercises below for a solution to this.
This is another hard aspect of objects. When we use the =
operator on an object, we merely copy its reference. Let’s see this by an example —
There are multiple ways to do a deep copy —
We can use ...
operator to a top-level deep copy. Here’s an example —
However, this is only effective for the top-level primitive properties. In this case, obj2
gets its own name
property. However, nestedObj
is still copied shallowly.
Also, if the value of a property is a function then we can’t create a copy of function reliably in JavaScript. Hence, the functions/methods have to be shared.
We take a JSON representation of an object and parse it again to create another object with the same key-value pairs. This has the nice property that nested objects are also deeply copied.
The assign
function takes a source and a target object. It copies all the enumerable, own key-value pairs in the target from the source.
In this case, the source is obj1
and the target is an empty object { }
.
Just like object spread operator, assign
does not copy deeply on all levels and both String and Symbol properties are copied.
Arrays are objects too. The only difference is that the keys are predetermined to be whole numbers (+0 ≤ i < 2³²–1). They are 0
, 1
, 2
… etc. by default. Also, while creating an array, the keys are automatically set. If we check using typeof []
, we will get object
.
Arrays have length
property denoting the number of items in the array that is managed by JavaScript. From the example, we can see that Object.entries
return the key-value pairs of the arr
object.
They are objects too. Specifically, they are objects with internal (not accessible to us) [[Call]] method. Let’s see —
All functions have certain properties on them by default. name
is one of them which stores the name of the function. In this case, name
gives us func1
which was the original name of the declared function.
We can also add our own properties. In the example, address
was added to func1
and later read like a normal property.
We have discussed quite a few things about objects. However, we have barely scratched the surface. There is another layer to JavaScript objects that we haven’t discussed. Hopefully, we’ll see that in a different article. In the meantime, here are a few exercises to cement your understanding of how objects work.
Given an object, make a copy of the object with all key names capitalized.
Create an equivalent of Array.map
and Array.filter
for objects. The callback will receive both key and value. See example below —
What happens when the keys are Symbol
s?
The properties are also known as key-value pairs. You may come across similar concepts in the form of , maps, and dictionaries in other languages. JavaScript objects embody those characteristics, but they also have a lot more to them as we shall see.
This effect of this code and the literal notation is the same. However, it is advised not to use this pattern. You can read more about it .
We add printFullName
method on obj1
. The method has access to the properties of the object using the this
keyword. We return the full name formatted using . In this case, the behavior is to print the full name.
The type of properties can be either astring
or a. If the property is an expression, it will be evaluated. If the property is of any other type, it will be coerced to a string value.
This gives more predictable results. However, it’s not without its own set of gotchas. Read about them .
You can use and for-of
loop to iterate entries very cleanly.
We create an object obj1
. When we do obj2 = obj1
, obj2
does not get another new object with its own key-value pairs. Rather, it starts pointing to the object pointed by obj1
. This is the core difference between and a . In deep copy, you would get another object with its own set of key-value pairs (having the same value as original ones).
One drawback is that keys whose values are undefined
, or functions or a Symbol
are skipped. It has many more rules that you can read .
We declare func1
as a function. If we do typeof func1
, we get function
instead of object
. However, that is because the so.
Write a function to two objects by value. What happens when the object’s values are functions? What about symbols?