Secrets of the JavaScript Object API

The “everything is an object” maxim clearly describes just how important objects are in JavaScript. These structures form the basis of the entire language! With that said, I think it’s easy to assume that the JS Object API doesn’t get the attention it deserves.

Think about the ways you usually interact with objects. And I don’t mean some complex, dedicated ones like functions or arrays (which are still objects after all), but simple structures that you use to organize your data. Certainly, you use dot or bracket notation to access the object's properties, and maybe even Object.assign() or the spread operator if you write more modern code. But what else do you use? I bet it's not that much.

Because of the fundamental nature of objects, they and their syntax are meant to be simple. But you might find yourself in a situation where you require some more advanced functionality that you don't know, but the Object API can provide. So, in this blog post, I’d like to walk you through some of these lesser-known functionalities, that might actually be really useful!

Object manipulation

Aside from everything I've just mentioned, Object API provides you with additional methods to interact with your objects. The two I'd like to tell you about here are Object.defineProperty() and Object.defineProperties().

Object.defineProperty()

The most obvious way to set an object's property is by doing it right when declaring the object or later on with the dot or bracket notation. Thus, having the Object.defineProperty() method might feel a bit repetitive and unnecessary. And in most cases it is, but it also provides some functionalities that you can't get anywhere else!

Object.defineProperty() not only defines/sets/overrides the property's value but its whole descriptor - something like metadata of the property. Take a look:

let obj = {};
let firstProperty = 10;

Object.defineProperty(obj, "firstProperty", {
  configurable: true,
  enumerable: true,
  get: () => {
    console.log(`Retrieving the property "firstProperty".`);

    return firstProperty;
  },
  set: newValue => {
    console.log(`Setting the property "firstProperty" to ${newValue}.`);

    firstProperty = newValue;
  }
});
Object.defineProperty(obj, "secondProperty", {
  configurable: false,
  enumerable: false,
  writable: false,
  value: "value"
});

obj.firstProperty; // Retrieving the property "firstProperty". 10
obj.secondProperty; // "value"
obj.firstProperty = 20; // Setting the property "firstProperty" to 20.
obj.secondProperty = "secondValue";
obj.firstProperty; // Retrieving the property "firstProperty". 20
obj.secondProperty; // "value"

Above I use Object.defineProperty() to set two properties on the object obj. The first argument that the method takes is the object that the property will be set on. It'll be returned later on from the Object.defineProperty() call. After that comes the second argument, which is the property's name and the last one, which is the property's descriptor.

I used two properties on purpose - to showcase the two flavors of descriptors - the data and the accessor ones. They share two properties - configurable and enumerable. The first one defines whether the property's descriptor type can be changed (e.g. by calling Object.defineProperty() the second time) or the property deleted (with the delete keyword). while the second one controls whether the property appears in the for... in loop or when used with some methods we'll discuss later on. Both properties default to false, which sets apart the most basic Object.defineProperty() call from the usual syntax.

Now, data descriptors allow you to set two other properties - value and writable. While the meaning of the first one is obvious, the second one refers to the possibility of changing (aka writing to) the property. Mind you that it's not the same as the configurable property, but like the one mentioned, defaults to false.

The second kind of descriptors - accessor descriptors, also provide you with two additional properties, but this time they're called get and set. These should have a form of individual functions that are called accordingly when the property is retrieved and set. They're the same setters and getters that you might have seen before, with the difference being that they're defined after the object is created. Just to remind you:

let firstProperty = 10;
let obj = {
  get firstProperty() {
    console.log(`Retrieving the property "firstProperty".`);

    return firstProperty;
  },
  set firstProperty(newValue) {
    console.log(`Setting the property "firstProperty" to ${newValue}.`);

    firstProperty = newValue;
  }
};

Properties that have setters and/or getters cannot have values of their own. Instead, they either calculate them from the other properties or use different variables.

Object.defineProperties()

So, if you want to use Object.defineProperty() to define multiple properties, you'll be better off using Object.defineProperties() instead. Here's how it looks:

let obj = {};
let firstProperty = 10;

Object.defineProperties(obj, {
  firstProperty: {
    configurable: true,
    enumerable: true,
    get: () => {
      console.log(`Retrieving the property "firstProperty".`);

      return firstProperty;
    },
    set: newValue => {
      console.log(`Setting the property "firstProperty" to ${newValue}.`);

      firstProperty = newValue;
    }
  },
  secondProperty: {
    configurable: false,
    enumerable: false,
    writable: false,
    value: "value"
  }
});

Basically, you just swap out the string argument for an object with property name - descriptor key-value structure, that's easier to use, read and manage when multiple properties are involved.

Immutability

The introduction of the const keyword in ES6 as a new way of declaring "variables" spread a little controversy as to what exactly is constant. As it turns out - it's not the value (as usual), but the variable itself. So, if you e.g. assign an object to such a variable, you won't be able to change the variable's value, but you'll be able to freely change the properties of the assigned object.

const obj = {};
obj.property = 1;
obj.property; // 1
obj = {}; // ERROR

This might be OK for most, but not so for the ones striving for immutability. const doesn't guarantee your variable's value to stay the same unless it's a primitive (i.e. number, string or boolean). And that's where the Object API comes into play, with a set of methods that allow you to define the mutation rules of not only a single property (like with Object.defineProperty()) but the entire objects!

Object.preventExtensions()

Starting with the most "loose" of the methods, Object.preventExtensions() simply prevents any new properties from being added to an object (aka extending it). Once you call it with your object as an argument, no new property will be allowed to be defined (even with the use of Object.defineProperty()).

const obj = Object.preventExtensions({
  firstProperty: 10,
  secondProperty: 20
});

obj.firstProperty = 100;
delete obj.secondProperty; // true
obj.thirdProperty = 30; // nothing or ERROR
obj.firstProperty; // 100
obj.secondProperty; // undefined
obj.thirdProperty; // undefined

Object.preventExtensions(), as well as all the other "locking" methods of Object API, return the passed object, making for a nice, immutability-friendly syntax you see above.

Again, after calling the method, you can do pretty much everything but define new properties. This includes deleting and changing the already present property values and descriptors. An attempt to set new property will either be left silent or throw an error (e.g. when you're in strict mode).

You can check whether the object can be extended with the Object.isExtensible() method.

const firstObject = { property: 10 };
const secondObject = Object.preventExtensions({ property: 20 });

Object.isExtensible(firstObject); // true
Object.isExtensible(secondObject); // false

Object.seal()

If you want to go a bit further than Object.preventExtensions(), you can use Object.seal() to not only disallow any new properties to be set, but also make all exiting properties non-configurable. Remember the configurable property from the Object.defineProperty()? Object.seal() is like combining Object.preventExtensions() with Object.defineProperties() where you override all existing properties to be non-configurable. Your properties are still writable (unless you've previously set them not to), so you can easily change their values. However, you cannot delete a property or change the descriptor type (from data to accessor or vice versa).

const obj = Object.seal({
  firstProperty: 10,
  secondProperty: 20
});

obj.firstProperty = 100;
delete obj.secondProperty; // false
obj.thirdProperty = 30; // nothing or ERROR
obj.firstProperty; // 100
obj.secondProperty; // 20
obj.thirdProperty; // undefined

If you want to check whether the given object has already been sealed, you can use the Object.isSealed() method. Also useful might be the previously-discussed Object.isExtensible() method, which, when the object is sealed, will return false.

const obj = Object.seal({ property: 20 });

Object.isSealed(obj); // true
Object.isExtensible(obj); // false

Object.freeze()

Lastly, if you want to take the immutability of your objects to another level, Object.freeze() is at your disposal. As the name indicates, it not only makes your object non-extensible and non-configurable but also completely unchangeable. You can only access your previously-defined properties and that's it! Any attempt to change anything won't work and will either be left silent or throw an error.

const obj = Object.freeze({
  firstProperty: 10,
  secondProperty: 20
});

obj.firstProperty = 100; // nothing or ERROR
delete obj.secondProperty; // false
obj.thirdProperty = 30; // nothing or ERROR
obj.firstProperty; // 10
obj.secondProperty; // 20
obj.thirdProperty; // undefined

To check if an object is "frozen", you'll have to use the Object.isFrozen() method, but keep in mind that both Object.isExtensible() and Object.isSealed() still apply.

const obj = Object.freeze({ property: 20 });

Object.isFrozen(obj);
Object.isSealed(obj); // true
Object.isExtensible(obj); // false

Now, just to remind you that as "everything is an object", the same "locking" methods can be applied to all the other objects that are present in JS. Examples of such include custom classes, functions, and most importantly - arrays. This is especially great when you're going for full-blown immutability and functional programming in pure JS.

const arr = Object.freeze([1, 2, 3]);

arr.push(4); // ERROR
arr.pop(); // ERROR
arr[0] = 0; // nothing or ERROR

Iteration

As we're on the topic of arrays, let's talk about iteration. Looping through arrays is normal, but what about objects? There's certainly less freedom in that department.

There's a for...in loop that lets you iterate through enumerable properties (remember the descriptors we've talked about before) of an object and read their key names.

const obj = {
  firstProperty: 10,
  secondProperty: 20
};

for (const key in obj) {
  const value = obj[key];
}

However, this method is pretty limiting. You only get access to the property keys and you have to use that to access the value if you need that. That's one additional (and possibly unnecessary) Line Of Code (LOC) to be added to your codebase - one that could have been avoided.

Basically, you have a lot less flexibility with simple objects than with arrays and their API. So, how about converting objects to arrays and looping through that instead? Well, that's exactly what some of the Object API methods allow you to do!

Object.keys()

Let's start with the simplest of methods - Object.keys(). Like the name implies it returns all the keys of the passed object in a form of an array of strings. When your data is organized in such a way, you can use e.g. the .forEach() method from Array API to loop through all of the retrieved property keys.

const obj = {
  firstProperty: 10,
  secondProperty: 20
};
const keys = Object.keys(obj); // ["firstProperty", "secondProperty"]

keys.forEach(key => {
  const value = obj[key];
});

Still, Object.keys() isn't that of a compelling option. It pretty much gives you the same result as the for...in loop at the loss of some performance. However, if you consider this syntax better or cleaner you shouldn't care about such small performance benefits.

Object.keys() also stands out from the rest of related Object API methods, with better support for older browsers. Most notably it supports up (or rather down) to IE 9, while the next two methods don't support this particular browser at all! Still, if the support of old browsers matters to you and you don't want to use any polyfills, you'll be better served by the for...in loop, which supports even IE 6!

Object.values()

As for the "need only the values" case we've discussed before, Object.values() will serve this purpose just fine. Instead of keys, it returns an array of object's property values.

const obj = {
  firstProperty: 10,
  secondProperty: 20
};
const values = Object.values(obj); // [10, 20]

values.forEach(value => {
  // do something with value
});

Object.entries()

Finally, Object.entries() is a method that gives you access both to the object's keys as well as its values. It returns them in the form of an array of key-value pairs (arrays).

const obj = {
  firstProperty: 10,
  secondProperty: 20
};
const entries = Object.entries(obj); // [["firstProperty", 10], ["secondProperty", 20]]

entries.forEach(([key, value]) => {
  // do something with the key and the value
});

Object.entries() feels especially good when used with the destructuring syntax like in the example above.

Object.fromEntries()

While Object.fromEntries() isn't a method meant for iterating through objects, it does basically the opposite of what the Object.entries() method does. Namely, it converts an array of key-value pairs (Object.entries() output) to an object. Just a fun fact!

const obj = {
  firstProperty: 10,
  secondProperty: 20
};
const entries = Object.entries(obj); // [["firstProperty", 10], ["secondProperty", 20]]
const objCopy = Object.fromEntries(entries);

Maps are better?

In comparison to the for...in loop, any of these methods don't take into consideration properties from the object's prototype. To achieve the same (usually desired) effect with the for...in loop, you'll have to use the .hasOwnProperty() method to check whether the property is the object's own.

You should also remember that both for...in loop and Object API methods ignore the non-enumerable properties (like I've said before), and the ones that use Symbols as their keys.

In reality, though, all that is kind-of "low-level" (as far as the JS goes) stuff, and you're unlikely to have to deal with any of such issues in real-world projects. What's more important, however, is the fact that any of the ways of object iterations we've just covered don't guarantee the order of iterated keys, values, entries or whatever. It usually follows the order in which the properties were defined, but it's not a good practice to follow such an assumption.

If you're going for something that's like an array and object combined, you might be interested in Maps. These are structures that organize data in a key-value fashion and allow for iteration while maintaining the correct order of the key-value pairs. They also have decent cross-browser support and other unique properties known from both arrays and objects. I've covered them already in one of my previous posts, so go check it out if you're interested!

Final words

That’s it! Hope you enjoyed the article and learned something new. The amount of possibilities JS API can offer is truly impressive! Here, we've barely scratched the surface! From the Object API itself, we've missed some more complex, prototype-related methods. I think they're not as useful as the ones listed (especially when writing modern JS) but I encourage you to explore them on your own to strengthen your JS knowledge!

So, if you like the article, consider sharing it with others and following me on Twitter, Facebook or through my weekly newsletter for more up-to-date content. You can also check out my YouTube channel and drop a like or a sub there. As always, thanks for reading and have a great day!