Blogs / setting-and-shadowing-properties-js



Setting & Shadowing Properties in Javascript

#javascript, #objects, #shadowing, #getters and setters

Mar 10, 202515 min read



Setting & Shadowing Properties in Javascript

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.

billions

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 like for...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:

  1. 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.
  2. Is the property a data descriptor with writable of false (read-only)? If so, silently fail in non-strict mode, or throw TypeError in strict mode.
  3. 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.

  1. If a normal data accessor property named title is found anywhere higher on the [[Prototype]] chain, and it’s not marked as read-only (therefore writable:true), then a new property called title is added directly to anime, resulting in a shadowed property.
  2. If a title is found higher on the [[Prototype]] chain, but it’s marked as read-only (therefore writable:false), then both the setting of that existing property as well as the creation of the shadowed property on anime 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.
  3. If a title is found higher on the [[Prototype]] chain and it’s a setter (hidden function), then the setter will always be called. No title will be added to (aka shadowed on) anime, nor will the title 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

Thanks for reading


Back to Blogs