Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies

Wednesday, January 17, 2024

What Removing Object Properties Tells Us About JavaScript

 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:

Make object1 similar to object2.
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 to undefined.”
  • 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 and JSON.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,

If A and B are true, 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:

  1. category is undefined (or falsy);
  2. 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!

No comments:

Post a Comment