Setting & Shadowing Properties in Javascript
#javascript, #objects, #shadowing, #getters and setters
Mar 10, 202515 min read
When you retrieve or set a property, JavaScript follows internal operations called [[Get]]
and [[Set]]
, which determine whether the property is accessed directly from the object or inherited from its prototype.
Setting properties on an object is more nuanced than simply adding a new property or changing an existing one. The process varies depending on whether the property already exists and whether it resides on the object itself or its prototype. If the property exists on the prototype, an assignment could unintentionally shadow the prototype property, causing the object's own property to override it.
If not carefully managed, property shadowing can introduce subtle bugs by replacing prototype properties with unintended values. In this article, we’ll break down how JavaScript handles property lookups, modifications, and shadowing behind the scenes.
Property Descriptors
Understanding how shadowing works starts with understanding property descriptors. Property descriptors provide fine-grained control over object properties and determine how they behave. They define attributes like whether a property can be modified, enumerated, or deleted.
Each object property is associated with a descriptor object that defines characteristics such as writability, enumerability, and configurability.
const series = {
name: "billions",
seasons: 5,
character: "bobby axelrod"
};
Object.getOwnPropertyDescriptor(series, 'name');
{
value: 'billions',
writable: true,
enumerable: true,
configurable: true
}
There are two main types of property descriptors: data descriptors and accessor descriptors. A data descriptor is a property with a value that can be writable or not. An accessor descriptor uses a getter-setter pair of functions. An object property is either a data property or an accessor property, but it cannot simultaneously be both.
I’ve covered property descriptors in depth in another article, but here’s a quick recap:
writable
– Controls whether the property’s value can be changed.enumerable
– Determines if the property appears in loops likefor...in
.configurable
– Decides if the property can be deleted or modified.
These attributes directly affect how JavaScript sets and retrieves properties, influencing [[Get]]
, [[Set]]
, and property shadowing.
[[Get]]
Internal properties—denoted by double square brackets—are used by the specification to influence Javascript’s behaviour but cannot be accessed directly in code.
[[Get]]
is an internal method that JavaScript uses to find and retrieve properties from an object. By default, it looks for the property on the object itself and if not found, follows the prototype chain. However, [[Get]]
may also invoke a getter function, which can be used to customize the property retrieval. We'll discuss getters in a later section.
Consider this example.
var song = {
title : 'Blind Eyes Red'
}
song.title //'Blind Eyes Red'
The song.title
is a property access, but it doesn’t just look in song
for a property of the name title
, as it might seem. Behind the scenes, Javascript performs a [[Get]]
operation (kinda like a function call: [Get]()
) on song
. The default built-in [[Get]]
operation for an object first inspects the object for a property of the requested name, and if it finds it, it will return the value accordingly.
What if we tried to access song.artist
, a property that does not exist on the object? The [[Get]]
algorithm defines other important behaviours if it does not find a property of the requested name.
Traversing the [[Prototype]]
Chain
Objects in JavaScript have an internal property, denoted in the specification as [[Prototype]]
, which is simply a reference to another object. Almost all objects are given a non-null value for this property, at the time of their creation.
As stated by MDN, “prototypes are the mechanism by which JavaScript objects inherit features from one another.”
During a property access, when the requested property isn’t present on the object directly, the default [[Get]]
operation proceeds to follow the [[Prototype]]
link of the object.
Let’s update how we create the song
object.
var songInfo = {
artist: "Minnie",
album: "Her",
};
// creates an object with the [[Prototype]] linked to songInfo
var song = Object.create(songInfo);
song.title = "Blind Eyes Red";
song.artist // 'Minnie'
The Object.create()
static method creates a new object, using an existing object as the prototype of the newly created object. song
’s prototype now points to the songInfo
object. song
only has one own property, title
, however accessing song.artist
succeeds, as [[Get]]
examined the object’s prototype to retrieve the property’s value.
But, if artist
weren’t found on songInfo
either, its [[Prototype]]
chain, if nonempty, is again consulted and followed.
This process continues until either a matching property name is found, or the [[Prototype]]
chain ends. If no matching property is ever found by the end of the chain, the return result from the [[Get]]
operation is undefined
.
// searching for a non-existent property yields undefined
song.releaseDate // undefined
So, the [[Prototype]]
chain is consulted, one link at a time, when you perform property lookups. The lookup stops once the property is found or the chain ends.
Now that we understand how JavaScript retrieves properties, let’s explore what happens when we try to set them.
[[Set]]
At first glance, it may seem like assigning a value to a property simply sets or creates that property on the object. However, JavaScript follows a more intricate process when handling property assignments.
When invoking [[Set]]
, how it behaves depends greatly on whether the property is already present on the object or not.
If the property is present, the [[Set]]
algorithm will roughly check:
- Is the property an accessor descriptor (i.e., does it have a getter/setter)? If so, call the setter, if any, instead of assigning a value directly.
- Is the property a data descriptor with
writable
offalse
(read-only)? If so, silently fail in non-strict mode, or throwTypeError
in strict mode. - Otherwise, set the value to the existing property as normal.
If the property is not found on the object itself, [[Set]]
does not immediately create it. Instead, it traverses the [[Prototype]]
chain, checking whether the property exists and whether any existing property affects assignment.
If the property isn't found anywhere in the chain, and nothing prevents assignment, it is added directly to the object as an own property. Own properties belong specifically to the instance, meaning they are stored directly on the object rather than inherited from its prototype. All operations on the property must be performed through that object.
But what happens if the property does exist somewhere in the [[Prototype]]
chain? Can we still assign to it? This is where things get tricky—leading us to shadowing.
Setting and Shadowing Properties
Earlier, we acknowledged that setting properties on an object is more nuanced than just adding a new property to the object or changing an existing property’s value.
// think of anime as an object
anime.title = 'Sakamoto Days';
If the anime
object already has a normal data accessor property called title
directly present on it, the assignment is as simple as changing the value of the existing property.
If title
is not already present directly on the anime
object, JavaScript will check the [[Prototype]]
chain to see if title
exists and whether it affects assignment (e.g., if it's a setter or read-only). If no such property is found, a new title
property is simply added to anime
with the specified value.
If title
was already present in the chain, the assignment of anime.title = ‘Sakamoto Days’
is a bit more subtle.
What is shadowing?
If title
ended up on both anime
and was found somewhere in the [[Prototype]]
chain that started at the anime
object, then this is called shadowing. The title
property directly on the anime
object shadows any title
property in its [[Prototype]]
chain because anime.title
will always resolve to the first occurrence of title
found in the lookup process.
Sounds simple right? However, shadowing is not as straightforward as it may seem. Lets look at a few scenarios for the anime.title = ‘Sakamoto Days’
when title
is not present on the anime
object directly but is at a higher level of the anime
object’s [[Prototype]]
chain.
- If a normal data accessor property named
title
is found anywhere higher on the[[Prototype]]
chain, and it’s not marked as read-only (thereforewritable:true
), then a new property calledtitle
is added directly toanime
, resulting in a shadowed property. - If a
title
is found higher on the[[Prototype]]
chain, but it’s marked as read-only (thereforewritable:false
), then both the setting of that existing property as well as the creation of the shadowed property onanime
are disallowed. If the code is running in strict mode, an error will be thrown. Otherwise, the setting of the property value will silently be ignored. Either way, no shadowing occurs. - If a
title
is found higher on the[[Prototype]]
chain and it’s a setter (hidden function), then the setter will always be called. Notitle
will be added to (aka shadowed on)anime
, nor will thetitle
setter be redefined.
Now we know that the assignment of a property does not always result in shadowing. This is only the case when the property exists higher in the [[Prototype]]
chain and is not read-only.
If you want to shadow title
in cases 2 and 3, you cannot use =
assignment, but must instead use Object.defineProperty(..)
. We can use Object.defineProperty(..)
to add a new property, or modify an existing one (if it’s configurable!), with the desired characteristics.
Disadvantages of Shadowing
Shadowing can lead to unexpected property overwrites and inconsistent behaviour in objects that inherit from a prototype. If a property is shadowed unintentionally, updates to the prototype property will no longer reflect in the shadowing object.
Let’s demonstrate.
var songInfo = {
artist: "Minnie",
album: "Her",
length: 7,
};
var song = Object.create(songInfo);
song.length; // 7
songInfo.length; // 7
song.hasOwnProperty("length"); // false
songInfo.hasOwnProperty("length"); // true
song.length++;
song.length; // 8
songInfo.length; // 7
Though it may appear that song.length++
should look up and just increment the songInfo.length
property itself in place, the ++
operation actually creates song.length
and increments it.
Let’s understand what happens.
song.length
triggers a [[Get]]
operation. Since length
is not found directly on song
, JavaScript follows the [[Prototype]]
chain and finds length
on songInfo
with a value of 7
. However, song.length++
is actually equivalent to song.length = song.length + 1
. This triggers a [[Set]]
operation, which creates a new length
property on song
instead of modifying songInfo.length
. The result is that song.length
becomes 8
, but songInfo.length
remains 7
.
Oops!
This is why we need to be very careful when dealing with delegated properties that you modify. If you wanted to increment songInfo.length
, the only proper way to do it is songInfo.length++
.
Getters and Setters
The default [[Get]]
and [[Set]]
operation for objects control how values are retrieved from existing properties or set to existing or new properties, respectively. Javascript introduced a way to override part of these default operations not on an object level but a per-property level, through the use of getters and setters.
Getters are properties that actually call a hidden function to retrieve a value. Setters are properties that actually call a hidden function to set a value.
[[Get]]
holds the getter, a function that is called when a property is read. That function computes the result of the read access, but [[Get]]
itself is not a getter—it’s an internal mechanism that looks up properties (which may invoke a getter). The same may be said for [[Set]]
. [[Set]]
holds the setter, a function that is called when a property is set to a value. The function receives that value as a parameter.
When you define a property to have either a getter or a setter or both, its definition becomes an “accessor descriptor”, as opposed to a “data descriptor”. For accessor descriptors, the value
and writable
characteristics of the descriptor are ignored, and instead Javascript considers the set
and get
characteristics of the property, as well as configurable
and enumerable
.
[[Get]]
as a getter
You can define a getter for a property either using object-literal syntax with get() { ... }
or through explicit definition with Object.defineProperty(..)
.
Take a look.
var album = {
// define a getter for name
get name() {
return "Serpentina";
},
};
// creating a getter manually via a descriptor
Object.defineProperty(album, "titleTrack", {
get: function () {
return `The title track for ${this.name} is Misunderstood`;
},
enumerable: true,
});
album.name; // 'Serpentina'
album.titleTrack; // ''The title track for Serpentina is Misunderstood'
Both album.name
and album.titleTrack
invoke a hidden function, a getter, we used to define custom behaviour and override the default behaviour.
// attempt to update the title track
album.titleTrack = 'Holding Back';
album.titleTrack; // 'The title track for Serpentina is Misunderstood'
Since we only defined a getter for titleTrack
, any attempt to set the value of titleTrack
later won’t throw an error. Instead, it will silently ignore the assignment. Even if there was a valid setter, our custom getter is hardcoded to return only 'The title track for Serpentina is Misunderstood'
, so the set operation would be ignored.
You will almost certainly want to always declare both getter and setter. Having only one or the other often leads to unexpected/surprising behaviour.
[[Set]]
as a Setter
When you define a getter for a property, you should also define a setter if you want to allow modifications. Setters override the default [[Set]]
operation.
var album = {
get name() {
return this._name_;
},
set name(val) {
this._name_ = val;
},
};
album.name = "Serpentina";
album.name; // 'Serpentina'
We updated album
to use a getter and setter. album.name = "Serpentina";
sets the name
property. You’ll notice we used this._name_
this time around. Developers often use this naming convention to indicate that a property is intended to be private or internal. However, JavaScript does not enforce this—it remains a normal property and implies nothing special about its behaviour.
You’ll notice our setter does not return anything. Setters in JavaScript don’t return anything because their purpose is just to store a value, not compute and return one. If you try to return something, JavaScript will ignore it.
Object.defineProperty(album, "titleTrack", {
get: function () {
return `The title track for ${this._name_} is Misunderstood`;
},
enumerable: true,
});
Object.defineProperty(album, "titleTrack", {
set(val) {
this._titleTrack_ = val;
},
});
We updated the titleTrack
property to use both a getter and setter. However, if you tried to run this, you’d get TypeError: Cannot redefine property: titleTrack
. The issue is that once you define a property using Object.defineProperty
with only a getter, you cannot later redefine it to add a setter using Object.defineProperty
again—at least not without explicitly making the property configurable.
When you define a property using Object.defineProperty
, if you don’t specify configurable: true
, it’s set to false
, which means you cannot redefine the property later, hence the TypeError
error when trying to add a setter.
So how do we fix this? Let’s see.
Object.defineProperty(album, "titleTrack", {
get: function () {
return `The title track for ${this._name_} is ${this._titleTrack_}`;
},
set(val) {
this._titleTrack_ = val;
},
enumerable: true,
configurable: true, // Allows redefinition later
});
album.titleTrack = "Misunderstood";
album.titleTrack; // 'The title track for Serpentina is Misunderstood'
album.name = "The Altar";
album.name; // 'The Altar'
album.titleTrack = "Meteorite";
album.titleTrack; // 'The title track for The Altar is Meteorite'
Earlier we defined the getter and setter separately in two different Object.defineProperty
calls. While this wouldn’t throw an error (if we had set configurable: true
in the getter to allows redefinition), it's still unnecessary. We should define both the getter and setter at the same time for clarity and best practice as shown in the above snippet.
Now we know how to customize the object retrieval and setting process via getters/setters. 🚀
Conclusion
We’ve explored how JavaScript handles property access and assignment behind the scenes through [[Get]]
and [[Set]]
.
Whenever a property is accessed, the engine actually invokes the internal default [[Get]]
operation, which means property access is not always as straightforward as it seems. Setting properties on an object is more than just adding or updating a property, as [[Set]]
does not immediately create the given property but may instead traverse the [[Prototype]]
chain if it isn't found.
Setting a property that exists on a prototype can unintentionally shadow the prototype property, causing the object's own property to override it. Unaware of this, you could accidentally introduce subtle errors that are difficult to debug.
Properties don’t always need to hold values—they can be accessor properties with custom getters and setters. These internal mechanisms let us define custom behaviour for property access and updates, providing flexibility in how we handle our data.
Understanding the inner workings of [[Get]]
and [[Set]]
helps you write more predictable and maintainable JavaScript. By grasping how property lookups, modifications, and shadowing operate, you can avoid unintended side effects and control how your objects interact with their prototypes.
Further Reading
Here are resources I found really helpful while doing research for this article:
https://nostarch.com/download/samples/oojs_ch03.pdf
https://2ality.com/2012/10/javascript-properties.html
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set
https://web.dev/learn/javascript/objects/property-descriptors
Back to Blogs