Internal Properties in JavaScript
The EcmaScript specification defines internal properties for every object. These indicate their standard behavior and largely define how JavaScript works.
As we know, object properties in JavaScript can be of three types:
- Data properties: Regular properties that contain data.
- Accessor properties: Properties that change the standard behavior of
[[get]]and[[put]]. - Internal properties: The language’s internal properties, such as
[[prototype]],[[get]], or[[put]], among others.
Today we’ll look at the third type of properties. But let’s take it step by step.
What Are They?
Internal properties are properties set by the language to define the internal behavior of objects. These properties don’t have a name and are usually not directly accessible through code.
Every object has internal properties, and they’re denoted enclosed in
[[...]]
What Are They?
[[get]]-> Returns the value of a named property.[[put]]-> Sets the value of a named property.[[prototype]]-> The object’s prototype or link, used for inheritance.[[extensible]]-> Defines whether new properties can be added.[[delete]]-> Removes the value associated with a property name.[[class]]-> Defines the type of object.
There are a few more, which you can check out in the EcmaScript documentation.
Today I’d like to dive deeper into three of them: [[prototype]], [[get]], and [[put]].
[[prototype]]
Creates a link from one object to another. This method is responsible for enabling prototypal inheritance in JavaScript.
[[prototype]] creates a reference to another object. Most objects have a prototype chain linking to another object.
Object.prototype is the top of the [[prototype]] prototype chain. All objects normally link to it.
This object has a set of properties that are common across all JS objects, thanks to the prototype chain.
Some methods like .toString(), valueOf(), etc. — do those ring a bell?
[[get]]
Defines how property values on an object are accessed.
The standard behavior is to inspect the object for the requested property and return its value.
It also has another important behavior when the property is not found.
If the property isn’t found on the object being queried, the search continues through the objects linked via [[prototype]].
var otroObjeto = {
a: 2
};
// Object.create, permite crear un objeto,
// fijando de manera explicita elvinculo por [[prototype]]
var miObjeto = Object.create( otroObjeto );
miObjeto.a; // 2
The property lookup traverses the entire prototype chain, all the way up to Object.prototype.
If the property isn’t found at any level of the prototype chain, undefined is returned.
var miObjeto = {
a: undefined
};
miObjeto.a; // undefined
miObjeto.b; // undefined
The returned value is the same, but the operation to get the value of miObjeto.b requires more work since it needs to traverse the entire prototype chain.
[[put]]
Defines how a property value is set on an object.
You might think that setting the value of a property is straightforward, but there are some things to consider.
[[put]] has an algorithm that analyzes several variables before setting a property.
Let’s look at some considerations:
- If the property is not found in the prototype chain, the property is set normally.
- If the property already exists and is an accessor property, the property’s setter is called.
- If the property has a writable: false attribute, it fails silently unless strict mode is used.
- If the property exists on the queried object and a property with the same name exists higher up in the prototype chain, the property being set takes precedence. This is called shadowing.
Let’s look at this last case in more detail.
If we wanted to set myObj.bar, but this property already exists on an object higher up in the prototype chain, one of the following cases could happen:
- If the bar property is a normal property found at a higher level of the chain, and is NOT marked as writable: false, bar is added to myObj, and the other property is hidden (shadowing).
var eseObjeto = {
bar: 2
};
var esteObjeto = Object.create( eseObjeto );
esteObjeto.bar++;
console.log(esteObjeto.bar); // 3 'shadowing'
console.log(eseObjeto.bar); // 2
- If bar is found at a higher level in the prototype chain and IS marked as writable: false, both properties — the existing one and the new one — are rejected. If strict mode is used, an error is thrown.
var eseObjeto = {};
Object.defineProperty(eseObjeto, 'bar', {
writable: false
});
var esteObjeto = Object.create( eseObjeto );
esteObjeto.bar++;
console.log(esteObjeto.bar); //undefined
- If bar is found at a higher level of the prototype chain and is a setter, the setter will be the one that gets used.
var eseObjeto = {
get bar() {
return this.__bar__;
},
set bar(value) {
this.__bar__ = value * 2;
},
};
var esteObjeto = Object.create( eseObjeto );
esteObjeto.bar = 2;
console.log(esteObjeto.bar); //4
To avoid any unexpected behavior, it’s recommended to use Object.defineProperty.
The shadowing behavior is dangerous since it can lead to unexpected property behavior. Use it with great care.