What Removing Object Properties Tells Us About JavaScript
A group of contestants are asked to complete the following task:
Makeobject1
similar toobject2
.
let object1 = {
a: "hello",
b: "world",
c: "!!!",
};
let object2 = {
a: "hello",
b: "world",
};
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
.
const movie = {
name: "Up",
};
console.log(movie.premiere); // 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:
const movie = {
name: "Up",
premiere: 2009,
};
movie.premiere = undefined;
console.log(movie);
Here is the output we get back:
{name: 'up', premiere: undefined}
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:
const propertyExists = movie.hasOwnProperty("premiere");
console.log(propertyExists); // true
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?
console.log(iDontExist);
// Uncaught ReferenceError: iDontExist is not defined
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.
const dog = {
breed: "bulldog",
fur: "white",
};
delete dog.fur;
console.log(dog); // {breed: 'bulldog'}
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.
const movies = ["Interstellar", "Top Gun", "The Martian", "Speed"];
delete movies[2];
console.log(movies); // ['Interstellar', 'Top Gun', empty, 'Speed']
console.log(movies.length); // 4
Secondly, let’s imagine the following nested object:
const user = {
name: "John",
birthday: {day: 14, month: 2},
};
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:
let movie = "Home Alone";
let bestSeller = movie;
In this case, each variable has an independent space in memory. We can see this behavior if we try to reassign one of them:
movie = "Terminator";
console.log(movie); // "Terminator"
console.log(bestSeller); // "Home Alone"
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.
let movie = {title: "Home Alone"};
let bestSeller = movie;
bestSeller.title = "Terminator";
console.log(movie); // {title: "Terminator"}
console.log(bestSeller); // {title: "Terminator"}
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.
const cat = {
breed: "siamese",
age: 3,
};
const handler = {
get(target, property) {
return `cat's ${property} is ${target[property]}`;
},
};
const catProxy = new Proxy(cat, handler);
console.log(catProxy.breed); // cat's breed is siamese
console.log(catProxy.age); // cat's age is 3
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.
const product = {
name: "vase",
price: 10,
};
const handler = {
deleteProperty(target, property) {
console.log(`Deleting property: ${property}`);
},
};
const productProxy = new Proxy(product, handler);
delete productProxy.name; // Deleting property: name
The name of the property is logged in the console but throws an error in the process:
Uncaught TypeError: 'deleteProperty' on proxy: trap returned falsish for property 'name'
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:
// ...
const handler = {
deleteProperty(target, property) {
console.log(`Deleting property: ${property}`);
return true;
},
};
const productProxy = new Proxy(product, handler);
delete productProxy.name; // Deleting property: name
console.log(productProxy); // {name: 'vase', price: 10}
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.
const product = {
name: "vase",
price: 10,
};
const handler = {
deleteProperty(target, property) {
console.log(`Deleting property: ${property}`);
return Reflect.deleteProperty(target, property);
},
};
const productProxy = new Proxy(product, handler);
delete productProxy.name; // Deleting property: name
console.log(product); // {price: 10}
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.
"use strict";
delete Math.PI;
Output:
Uncaught TypeError: Cannot delete property 'PI' of #<Object>
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.
const movie = {
title: "Avatar",
genre: "science fiction",
};
const {title, genre} = movie;
console.log(title); // Avatar
console.log(genre); // science fiction
It also works with arrays using square brackets ([]
):
const animals = ["dog", "cat", "snake", "elephant"];
const [a, b] = animals;
console.log(a); // dog
console.log(b); // cat
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:
const car = {
type: "truck",
color: "black",
doors: 4
};
const {color, ...newCar} = car;
console.log(newCar); // {type: 'truck', doors: 4}
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.
const find = (product, category) => {
const options = {
limit: 10,
product,
category,
};
console.log(options);
// Find in database...
};
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:
find("bedsheets");
And since a category
is not specified, it returns as undefined
, resulting in the following output:
{limit: 10, product: 'beds', category: undefined}
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:
const options = {
limit: 10,
product,
category,
};
…we can do this instead:
const options = {
limit: 10,
product,
...(category && {category}),
};
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:
const options = {
limit: 10,
product,
...category,
};
And if we try to destructure any falsy value inside an object, they will be destructured into nothing:
const options = {
limit: 10,
product,
};
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:
const options = {
limit: 10,
product,
...{category},
};
And since category
is defined, it is destructured into a normal property:
const options = {
limit: 10,
product,
category,
};
Put it all together, and we get the following betterFind()
function:
const betterFind = (product, category) => {
const options = {
limit: 10,
product,
...(category && {category}),
};
console.log(options);
// Find in a database...
};
betterFind("sofas");
And if we don’t specify any category
, it simply does not appear in the final options
object.
{limit: 10, product: 'sofas'}
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:
let monitor = {
size: 24,
screen: "OLED",
};
monitor.screen = undefined;
monitor = JSON.parse(JSON.stringify(monitor));
console.log(monitor); // {size: 24}
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!
Further Reading On SmashingMag
- “Discovering Primitive Objects In JavaScript (Part 1),” Kirill Myshkin
- “Primitive Objects In JavaScript: When To Use Them (Part 2),” Kirill Myshkin
- “A Re-Introduction To Destructuring Assignment,” Laurie Barth
- “Document Object Model (DOM) Geometry: A Beginner’s Introduction And Guide,” Pearl Akpan