Removing properties from an object in JavaScript might not be the most exciting job, but there are many ways to achieve it, each revealing a fundamental aspect of how JavaScript works. Juan Diego Rodríguez explores each technique in this article.
A group of contestants are asked to complete the following task:
Makeobject1
similar toobject2
.
Seems easy, right? Simply delete the c
property to match object2
. Surprisingly, each person described a different solution:
- Contestant A: “I set
c
toundefined
.” - Contestant B: “I used the
delete
operator.” - Contestant C: “I deleted the property through a
Proxy
object.” - Contestant D: “I avoided mutation by using object destructuring.”
- Contestant E: “I used
JSON.stringify
andJSON.parse
.” - Contestant F: “We rely on Lodash at my company.”
An awful lot of answers were given, and they all seem to be valid options. So, who is “right”? Let’s dissect each approach.
Contestant A: “I Set c
To undefined
.”
In JavaScript, accessing a non-existing property returns undefined
.
It’s easy to think that setting a property to undefined
removes it from the object. But if we try to do that, we will observe a small but important detail:
Here is the output we get back:
As you can see, premiere
still exists inside the object even when it is undefined
. This approach doesn’t actually delete the property but rather changes its value. We can confirm that using the hasOwnProperty()
method:
But then why, in our first example, does accessing object.premiere
return undefined
if the property doesn’t exist in the object? Shouldn’t it throw an error like when accessing a non-existing variable?
The answer lies in how ReferenceError
behaves and what a reference is in the first place.
A reference is a resolved name binding that indicates where a value is stored. It consists of three components: a base value, the referenced name, and a strict reference flag.
For a user.name
reference, the base value is the object, user
, while the referenced name is the string, name
, and the strict reference flag is false
if the code isn’t in strict mode
.
Variables behave differently. They don’t have a parent object, so their base value is an environment record, i.e., a unique base value assigned each time the code is executed.
If we try to access something that doesn’t have a base value, JavaScript will throw a ReferenceError
.
However, if a base value is found, but the referenced name doesn’t
point to an existing value, JavaScript will simply assign the value undefined
.
“The Undefined type has exactly one value, called undefined. Any variable that has not been assigned a value has the value undefined.”
— ECMAScript Specification
We could spend an entire article just addressing undefined
shenanigans!
Contestant B: “I Used The delete
Operator.”
The delete
operator’s sole purpose is to remove a property from an object, returning true
if the element is successfully removed.
Some caveats come with the delete
operator that we have to take into consideration before using it. First, the delete
operator can be used to remove an element from an array. However, it
leaves an empty slot inside the array, which may cause unexpected
behavior since properties like length
aren’t updated and still count the open slot.
Secondly, let’s imagine the following nested object:
Trying to remove the birthday
property using the delete
operator will work just fine, but there is a common misconception that doing this frees up the memory allocated for the object.
In the example above, birthday
is a property holding a nested object. Objects in JavaScript behave
differently from primitive values (e.g., numbers, strings, and booleans)
as far as how they are stored in memory. They are stored and copied “by
reference,” while primitive values are copied independently as a whole
value.
Take, for example, a primitive value such as a string:
In this case, each variable has an independent space in memory. We can see this behavior if we try to reassign one of them:
In this case, reassigning movie
doesn’t affect bestSeller
since they are in two different spaces in memory. Properties or
variables holding objects (e.g., regular objects, arrays, and functions)
are references pointing to a single space in memory. If we try to copy an object, we are merely duplicating its reference.
As you can see, they are now objects, and reassigning a bestSeller
property also changes the movie
result. Under the hood, JavaScript looks at the actual object in memory
and performs the change, and both references point to the changed
object.
Knowing how objects behave “by reference,” we can now understand how using the delete
operator doesn’t free space in memory.
The process in which programming languages free memory is called garbage collection. In JavaScript, memory is freed for an object when there are no more references and it becomes unreachable. So, using the delete
operator may make the property’s space eligible for collection, but
there may be more references preventing it from being deleted from
memory.
While we’re on the topic, it’s worth noting that there is a bit of a debate around the delete
operator’s impact on performance.
You can follow the rabbit trail from the link, but I’ll go ahead and
spoil the ending for you: the difference in performance is so negligible
that it wouldn’t pose a problem in the vast majority of use cases.
Personally, I consider the operator’s idiomatic and straightforward
approach a win over a minuscule hit to performance.
That said, an argument can be made against using delete
since it mutates an object. In general, it’s a good practice to avoid
mutations since they may lead to unexpected behavior where a variable
doesn’t hold the value we assume it has.
Contestant C: “I Deleted The Property Through A Proxy
Object.”
This
contestant was definitely a show-off and used a proxy for their answer.
A proxy is a way to insert some middle logic between an object’s common
operations, like getting, setting, defining, and, yes, deleting
properties. It works through the Proxy
constructor that takes two parameters:
target
: The object from where we want to create a proxy.handler
: An object containing the middle logic for the operations.
Inside the handler
, we define methods for the different operations, called traps, because they intercept the original operation and perform a custom change. The constructor will return a Proxy
object — an object identical to the target
— but with the added middle logic.
Here, the handler
modifies the getting operation to return a custom value.
Say we want to log the property we are deleting to the console each time we use the delete
operator. We can add this custom logic through a proxy using the deleteProperty
trap.
The name of the property is logged in the console but throws an error in the process:
The error is thrown because the handler didn’t have a return
value. That means it defaults to undefined
. In strict mode, if the delete
operator returns false
, it will throw an error, and undefined
, being a falsy value, triggers this behavior.
If we try to return true
to avoid the error, we will encounter a different sort of issue:
The property isn’t deleted!
We replaced the delete
operator’s default behavior with this code, so it doesn’t remember it has to “delete” the property.
This is where Reflect
comes into play.
Reflect
is a global object with a collection of all the internal methods of an
object. Its methods can be used as normal operations anywhere, but it’s
meant to be used inside a proxy.
For example, we can solve the issue in our code by returning Reflect.deleteProperty()
(i.e., the Reflect
version of the delete
operator) inside of the handler.
It is worth calling out that certain objects, like Math
, Date
, and JSON
, have properties that cannot be deleted using the delete
operator or any other method. These are “non-configurable” object
properties, meaning that they cannot be reassigned or deleted. If we try
to use the delete
operator on a non-configurable property, it will fail silently and return false
or throw an error if we are running our code in strict mode.
Output:
If we want to avoid errors with the delete
operator and non-configurable properties, we can use the Reflect.deleteProperty()
method since it doesn’t throw an error when trying to delete a
non-configurable property — even in strict mode — because it fails
silently.
I assume, however, that you would prefer knowing when you are trying to delete a global object rather than avoiding the error.
Contestant D: “I Avoided Mutation By Using Object Destructuring.”
Object destructuring is an assignment syntax that extracts an object’s properties into individual variables. It uses a curly braces notation ({}
) on the left side of an assignment to tell which of the properties to get.
It also works with arrays using square brackets ([]
):
The spread syntax (...
)
is sort of like the opposite operation because it encapsulates several
properties into an object or an array if they are single values.
We can use object destructuring to unpack the values of our object and the spread syntax to keep only the ones we want:
This way, we avoid having to mutate our objects and the potential side effects that come with it!
Here’s an edge case with this approach: deleting a property only when it’s undefined
. Thanks to the flexibility of object destructuring, we can delete properties when they are undefined
(or falsy, to be exact).
Imagine you run an online store with a vast database of products. You have a function to find them. Of course, it will need some parameters, perhaps the product name and category.
In this example, the product name
has to be provided by the user to make the query, but the category
is optional. So, we could call the function like this:
And since a category
is not specified, it returns as undefined
, resulting in the following output:
In this case, we shouldn’t use default parameters because we aren’t looking for one specific category.
Notice how the database could incorrectly assume that we are querying products in a category called undefined
! That would lead to an empty result, which is an unintended side effect. Even though many databases will filter out the undefined
property for us, it would be better to sanitize the options before making the query. A cool way to dynamically remove an undefined
property is through object destructing along with the AND
operator (&&
).
Instead of writing options
like this:
…we can do this instead:
It
may seem like a complex expression, but after understanding each part,
it becomes a straightforward one-liner. What we are doing is taking
advantage of the &&
operator.
The AND
operator is mostly used in conditional statements to say,
IfA
andB
aretrue
, then do this.
But at its core, it evaluates two expressions from left to right, returning the expression on the left if it is falsy and the expression on the right if they are both truthy. So, in our prior example, the AND
operator has two cases:
category
isundefined
(or falsy);category
is defined.
In the first case where it is falsy, the operator returns the expression on the left, category
. If we plug category
inside the rest of the object, it evaluates this way:
And if we try to destructure any falsy value inside an object, they will be destructured into nothing:
In the second case, since the operator is truthy, it returns the expression on the right, {category}
. When plugged into the object, it evaluates this way:
And since category
is defined, it is destructured into a normal property:
Put it all together, and we get the following betterFind()
function:
And if we don’t specify any category
, it simply does not appear in the final options
object.
Contestant E: “I Used JSON.stringify
And JSON.parse
.”
Surprisingly to me, there is a way to remove a property by reassigning it to undefined
. The following code does exactly that:
I sort of lied to you since we are employing some JSON shenanigans to pull off this trick, but we can learn something useful and interesting from them.
Even though JSON takes direct inspiration from
JavaScript, it differs in that it has a strongly typed syntax. It
doesn’t allow functions or undefined
values, so using JSON.stringify()
will omit all non-valid values during conversion, resulting in JSON text without the undefined
properties. From there, we can parse the JSON text back to a JavaScript object using the JSON.parse()
method.
It’s important to know the limitations of this approach. For example, JSON.stringify()
skips functions and throws an error if either a circular reference (i.e., a property is referencing its parent object) or a BigInt
value is found.
Contestant F: “We Rely On Lodash At My Company.” #
It’s worth noting that utility libraries such as Lodash.js, Underscore.js, or Ramda also provide methods to delete — or pick()
— properties from an object. We won’t go through different examples for
each library since their documentation already does an excellent job of
that.
Conclusion #
Back to our initial scenario, which contestant is right?
The answer: All of them! Well, except for the first contestant. Setting a property to undefined
just isn’t an approach we want to consider for removing a property from
an object, given all of the other ways we have to go about it.
Like most things in development, the most “correct” approach depends on the situation. But what’s interesting is that behind each approach is a lesson about the very nature of JavaScript. Understanding all the ways to delete a property in JavaScript can teach us fundamental aspects of programming and JavaScript, such as memory management, garbage collection, proxies, JSON, and object mutation. That’s quite a bit of learning for something seemingly so boring and trivial!
No comments:
Post a Comment