Object & Prototypal Inheritance
Objects
In JavaScript, any value that is not a String
, a Number
, a Symbol
or true
, false
, null
or undefined
is an Object. Objects penetrate almost every aspect of this language, thus it's really important to learn the objects first before going anywhere else in-depth. Even though Strings
, Number
and Boolan
are not objects, but they behave like immutable objects.
An object is an unordered collection of properties, each of which has a name and a value.
A JavaScript object also inherits the properties of another object, known as its "Prototype". The methods of an object are typically inherited properties, and this "Prototype inheritance" is a key feature of JavaScript.
It is sometimes important to be able to distinguish between properties defined directly on an object and those that are inherited from a Prototype object. JavaScript uses the term own property to refer to non-inherited properties.
In addition to its name and value, each property has three property attributes:
- The
writable
attribute specifies whether the value of the property can be set. - The
enumerable
attribute specifies whether the property name is returned by afor...in
loop. - The
configurable
attribute specifies whether the property can be deleted and whether its attribute can be altered.
Creating objects
There are three ways we can create an object in JavaScript, with object literals, with new
keyword and with the Object.create()
method.
Creating objects with object literals
The easiest way we can create an object is by including object literal. It is a comma-separated list of colon-separated name:value pairs, enclosed within curly braces.
Let's take an example:
const user = { name: "John Doe", address: "localhost" };
// An object `user` with two string properties
Creating objects with new
keyword
The new
operator creates and initializes a new object. The new
keyword must be followed by a function invocation. A function used in this way is called a constructor and initialize the newly created object.
Let's take an example:
function User(name, address) {
this.name = name;
this.address = address;
}
const john = new User("John Doe", "localhost:3000");
// An object `john` is created with two string propertes.
// Or
class Emloyee {
constructor(name, address) {
this.name = name;
this.address = address;
}
}
const bob = new Employee("Bob", "localhost:8080");
// An object `bob` is created with two string properties.
Prototype
Before approaching the 3rd process of creating objects with the Object.create()
function, we have to pause for a moment to understand what are __proto__
and "Prototype" in JavaScript? You might not have noticed, but in JavaScript, all objects have a hidden [[prototype]] property that is either an object or null
.
As you can see the john
object has an object __proto__
and also the User()
function has a "Prototype" object, these are automatically generated by the JavaScript engine.
So almost every JavaScript object has a second JavaScript object associated with it. This second object is known as [[prototype]] and the first object inherits properties from the prototype.
We already know that which is not primitive are objects in JavaScript. So, the functions are objects and objects are the object. And every object has a second object known as [[prototype]].
Do not confuse yourself with __proto__
with [[prototype]]. __proto__
is the getter/setter for the [[prototype]] object.
const cat = {
canEat: true,
canSleep: true,
};
const mew = {
canSing: true,
__proto__: cat, // setiing cat as the prototype of mew.
};
console.log(mew.canEat); // true
console.log(mew.canSing); // true
The capability of extending objects was there from the very beginning of the language. That's why JavaScript is known as the prototype-based functional object-oriented programming language.
Let's keep aside the concepts of object-oriented programming, and let's look at the above example.
console.log(cat.isPrototypeOf(mew)); // true
console.log(mew.__proto__ === cat); // true
console.log(cat.__proto__ === Object.prototype); // true
console.log(mew.__proto__.__proto__ === Object.prototype); // true
console.log(mew.__proto__.__proto__.__proto__ === null); // true
The above example creates a prototype chain that goes up to Object.prototype
. It is also an object but it didn't have its own [[prototype]], the prototype chain ends here. We learned that every object in JavaScript has a second object known as [[prototype]] but it is an exception for Object.prototype
it didn't have the second object it points to null
.
So, in short [[prototype]] is nothing but a mechanism to extend objects. Traditionally we extend object using classes, in ES6 JavaScript do support classes but under the hood, it is the [[prototype]] object that extends the child object. That's why it is important to know what is the prototype in JavaScript, it is a key feature of the language.
More on Objects
For all objects in addition to its name and value, each property has three property attributes:
writable
enumerable
, andconfiguable
When we create objects either with object literals or with a new
operator, the JavaScript engine sets true
default values to these attributes for each property. In Object.prototype
there is a method to check the values of these property attributes that is Object.getOwnPropertyDescriptors()
it returns an object containing all own property descriptors of an object. These three attributes are known as property descriptors.
const user = {
name: "John Doe",
address: "California",
};
console.log(Object.getOwnPropertyDescriptors(user));
{
name: {
value: 'John Doe',
writable: true,
enumerable: true,
configurable: true
},
address: {
value: 'California',
writable: true,
enumerable: true,
configurable: true
}
}
Creating objects with Object.create()
When creating objects with the Object.create()
method, we have the freedom to set property descriptors according to our needs. The function receives two arguments:
- [[prototype]] - sets the prototype of the newly created object.
- property descriptors - sets the property descriptors for the newly created object.
const human = Object.create(Object.prototype, {
eye: {
value: 2,
writable: false, // property cannot be set
enumerable: true, // property name can be returned by for..in loop
configurable: false, // property attributes cannot be altered
},
nose: {
value: 1,
writable: false, // property cannot be set
enumerable: true, // property name can be returned by for..in loop
configurable: false, // property attributes cannot be altered
},
leg: {
value: 2,
writable: true, // property can be set
enumerable: true, // property name can be returned by for..in loop
configurable: true, // property attributes cannot be altered
},
name: {
value: "Homo sapiens",
writable: false, // property cannot be set
enumerable: false, // property name cannot be returned by for..in loop
configurable: false, // property attributes cannot be altered
},
});
console.log(human.eye);
The above example creates an object human
with the following properties eye
, nose
, leg
, name
in such a manner that the value of eye
cannot be updated, name
cannot be returned in for..in loop. We have created the object according to our needs further modification is not allowed except for the leg
property. Since all JavaScript objects has a second object [[prototype]] the human
object [[prototype]] set to Object.prototype
object.
Let's try to modify human
object.
huamn.eye = 3; //Cannot assign to read only property 'eye' of object
console.log(human.eye);
With the help of the writable:false
property descriptor, we turned the eye
property of the human
object into a read-only property.
In the following example, we have created another object john
that inherit the human
object. The modern way of setting [[prototype]] is using the Object.setPrototypeOf()
method. The old way of setting [[prototype]] using __proto__
is deprecated on the browser side. It is available on the server-side (nodejs) for just historical reasons.
The below example lists all the properties of the john
object has. We have declared only one property of the john
object rest four of them are inherited from the human
object.
const john = {
canCode: true,
};
Object.setPrototypeOf(john, human);
for (const key in john) {
console.log(`${key} -> ${john[key]}`);
}
canCode -> true
eye -> 2
nose -> 1
leg -> 2
john
object has 5 properties but it only shows up 4, why? Because we set the enumerable
flag to false at the parent object human
. Thus it did not return in the for..in loop.
Configuring object
Property descriptors work at the level of individual properties. There are also methods available at the Object.prototype
object that limit access to the whole object.
Object.preventExtension(obj)
- Forbids the adding of new properties to the object.
Object.seal(obj)
- Forbids adding/removing/customizing of the properies
- flag
cofigurable: false
to all existing properties
Object.freeze(obj)
- Forbids adding/removing/customizing of the properies
- flag
writable: false
to all existing properties - flag
cofigurable: false
to all existing properties
For testing object configuration there are also methods available at the Object.prototype
object:
Object.isExtensible(obj)
- Return
true
, if adding properties is forbiden, else returnfalse
otherwisetrue
.
- Return
Object.isSealed(obj)
- Returns
true
if theconfigurable: false
flag set tofalse
, for all properties i.e. if adding/removing/customizing of the properies are forbiden else returnfalse
otherwisetrue
.
- Returns
Object.isFrozen(obj)
- Return
true
if thewritable: false
andconfigurable: false
flags are set tofalse
, for all properties if adding/removing/customizing of the properies are forbiden else returnfalse
otherwisetrue
.
- Return
Resources
References
- ๐ Object.create() - MDN
- ๐ Object.getOwnPropertyDescriptors() - MDN
- ๐ Object.setPrototypeOf() - MDN
- ๐ Object.setPrototypeOf() - MDN
Articles
- ๐ Prototypal Inheritance in JavaScript - Kevin Ennis
- ๐ What Makes JavaScript JavaScript? Prototypal Inheritance - Dmitri Pavlutin
- ๐ Understanding Prototypes and Inheritance in JavaScript - Tania Rascia