In the blogpost, we’ll learn about JavaScript Objects (and its various key types), Maps and then explore various techniques to convert JavaScript objects into maps. We’ll understand how each method handles different types of keys and properties, along with tips and common errors to avoid. We’ll also provide a coding problem to practice these concepts, solidifying your understanding of converting (reaaallly 😎) complex objects into maps.
Lets dive in! 👨💻
- Refresher: Map, Object, and Object key types
- Ways to Convert an Object to Map in JavaScript
- Using Object.entries() and Map() Constructor
- Using Array.map() with Object.keys()
- Using a for…of Loop with Object.entries()
- Using a for…in Loop with Object.hasOwnProperty()
- Using Object.getOwnPropertyNames() for non enumerable properties
- Using Object.getOwnPropertySymbols() for Symbol Keyed Properties
- Using Reflect.ownKeys() for a Complete Property List
- Tips & errors while converting Object to Map in JavaScript
- 🧪Practice Coding Problem: Converting Complex Object to Map
Refresher: Map, Object, and Object key types
Before diving into the various techniques for converting objects to maps in JavaScript, let’s refresh our understanding of Maps, Objects, and the different types of keys objects can have.
Understanding JavaScript Objects
- Basics: Objects in JavaScript are collections of properties, where each property is a key-value pair. The keys are always strings or symbols.
- Properties: Properties can be enumerable or non-enumerable. Enumerable properties show up in
for...in
loops and methods likeObject.keys()
, while non-enumerable properties do not. - Inheritance: Objects can inherit properties from their prototype, leading to a mix of own properties and inherited properties.
Understanding JavaScript Maps
- Characteristics: Maps are a collection of key-value pairs where keys can be of any type, not just strings or symbols. Maps maintain the insertion order of elements, which is not guaranteed in objects.
- Methods and Iteration: Maps come with built-in methods like
set()
,get()
,has()
, anddelete()
, making it easier to work with the data. They also provide iterators which make looping over the map straightforward.
Object Key Types
- String and Symbol Keys: While object keys are typically strings, they can also be symbols, which are unique and immutable.
- Non-String Keys: When dealing with non-string keys in objects, remember that these keys are converted to strings, which can lead to unexpected behavior. Maps, on the other hand, can handle any key type.
// JavaScript Object
const obj = {
stringKey: "value1",
[Symbol("symbolKey")]: "value2",
};
Object.defineProperty(obj, "nonEnumerableKey", {
value: "value3",
enumerable: false,
});
// JavaScript Map
const map = new Map();
map.set("stringKey", "value1");
map.set(Symbol("symbolKey"), "value2");
Ways to Convert an Object to Map in JavaScript
Lets see various techniques to convert JavaScript Object to Map.
Using Object.entries() and Map() Constructor
Object.entries()
takes an object as an input and returns an array of its own enumerable string-keyed property[key, value]
pairs. This includes properties found directly upon the object. The output format ofObject.entries()
is perfectly aligned with what theMap
constructor expects as input.
- In following example, we first define an object
obj
with three key-value pairs. We then useObject.entries(obj)
to convert this object into an array of[key, value]
pairs. Finally, we pass this array to theMap
constructor to create a newMap
instance.
const obj = {
key1: 'value1',
key2: 'value2',
key3: 'value3'
};
const map = new Map(Object.entries(obj));
console.log(map);
// Output:
// Map(3) {"key1" => "value1", "key2" => "value2", "key3" => "value3"}
- Use for Simple Objects: This method is ideal for objects with simple (key-value like object), enumerable properties ( those that show up in loops like
for...in
and are returned by methods likeObject.keys()
andObject.entries()
). - Check for Object Structure: Works best with flat objects (key-value like Object). Can work on Nested Objects too, but note that if a property value is itself an object or an array (i.e., a nested structure),
Object.entries()
does not break down this nested structure into its own key-value pairs. Instead, it treats the entire nested object or array as a single value.
const nestedObject = {
key1: "value1",
key2: {
subKey1: "subValue1",
subKey2: 456
},
key3: [1, 2, 3]
};
const map = new Map(Object.entries(nestedObject));
console.log(map);
// Map(3) {
// "key1" => "value1",
// "key2" => { subKey1: "subValue1", subKey2: 456 },
// "key3" => [1, 2, 3]
// }
// 1️⃣ The second entry is "key2" => { subKey1: "subValue1", subKey2: 456 }.
// Here, the entire object { subKey1: "subValue1", subKey2: 456 } is treated as a single value associated with the key key2.
// 2️⃣ Similarly, for key3, the entire array [1, 2, 3] is a single value in the map.
- ⚠Handling
null
andundefined
Values: A common mistake is not accounting fornull
orundefined
object properties. If your object might contain such values, you should handle them appropriately before converting. TheMap
constructor won’t throw an error, but the presence ofnull
orundefined
can lead to unexpected behavior in your application logic.
const obj = {
key1: 'value1',
key2: null,
key3: undefined
};
const map = new Map(Object.entries(obj));
console.log(map);
// Output:
// Map(3) {"key1" => "value1", "key2" => null, "key3" => undefined}
Using Array.map() with Object.keys()
Object.keys(obj)
returns an array of a given object’s own enumerable property names, iterated in the same order that a normal loop would.- The
map()
method then creates a new array with the results of calling a provided function on every element in this array. - Useful when you need to modify the keys or values (or both) while converting the object to a map.
- In following example, we first use
Object.keys(obj)
to get an array of keys['a', 'b', 'c']
. We then usemap()
to transform each key-value pair. The keys are converted to uppercase usingkey.toUpperCase()
, and the values are multiplied by 2. This transformed array is then passed to theMap
constructor.
const obj = {
a: 1,
b: 2,
c: 3,
};
// Capitalise Key and double the value, and convert to map.
const map = new Map(
Object.keys(obj).map((key) => [
key.toUpperCase(),
obj[key] * 2,
]),
);
console.log(map);
// Output: Map(3) {"A" => 2, "B" => 4, "C" => 6}
- ⚠Ignoring Non-Enumerable Properties: A key limitation of using
Object.keys()
is that it only considers the object’s own enumerable properties. This means that non-enumerable properties, which might be present due to property descriptors or inherited through prototypes, will not be included in the conversion. If an object has a non-enumerable property, it will not appear in the array returned byObject.keys()
, and therefore, it will not be included in the final Map. See example below.
const obj = Object.create(
{},
{
a: { value: 1, enumerable: true },
// b as Non-enumerable property
b: { value: 2, enumerable: false },
},
);
const map = new Map(
Object.keys(obj).map((key) => [key, obj[key]]),
);
console.log(map);
// Output: Map(1) {"a" => 1}
// Note: Property 'b' is not included in the map
Using a for...of
Loop with Object.entries()
This technique combines the Object.entries()
method with a for...of
loop to convert an object into a Map in JavaScript.
Object.entries(obj)
returns an array of[key, value]
pairs of the object’s own enumerable properties.- The
for...of
loop then iterates over these pairs, allowing for greater control and flexibility during the iteration process. - Useful when you need to add conditional logic or perform more complex operations during the conversion to a Map.
- In following example,
for...of
loop iterates over these pairs, and if the value is not null then it transforms the value to uppercase and then add the key-value pair to the map usingmap.set()
.
const obj = {
key1: "value1",
key2: "value2",
key3: "value3",
key4: null,
};
const map = new Map();
for (const [key, value] of Object.entries(obj)) {
// Adding conditional logic
if (value !== null) {
// Example of a complex operation
map.set(key, value.toUpperCase());
}
}
console.log(map);
// Output:
// Map(3) {"key1" => "VALUE1", "key2" => "VALUE2", "key3" => "VALUE3"}
Note: Object.entries() only shows enumerable, self owned properties (not inherited properties).
- ⚠ Forgetting to Check for Object’s Own Properties: A common mistake when using this technique is not checking whether the properties belong to the object itself and are not inherited from its prototype chain.
- In JavaScript, an object can inherit properties from another object. When using
Object.entries()
, these inherited properties are not included, but when using other methods like afor...in
loop, you might unintentionally iterate over inherited properties. - In the context of
Object.entries()
with afor...of
loop, this is less of a concern, but it’s a good practice to be aware of the object’s property ownership, especially when adapting or expanding on this basic technique. - In following example,
Object.entries(obj)
only returns the object’s own enumerable property[key, value]
pairs. BecauseObject.entries()
ignores the inherited properties, thefor...of
loop only iterates over the properties that are actually part ofobj
itself, not those inherited fromprototype
.
// Define a prototype with a property
const prototype = {
inheritedProperty: 'inheritedValue'
};
// Create a new object that inherits from the prototype
const obj = Object.create(prototype);
obj.ownProperty = 'ownValue';
// Iterating both own and inherited properties.
for (const key in obj) {
console.log(key);
// This will log both 'ownProperty' and 'inheritedProperty'
}
// Iterating only own properties.
for (const [key, value] of Object.entries(obj)) {
console.log(`${key}: ${value}`);
// This will only log 'ownProperty: ownValue'
}
Could there be simpler way to just use Object’s own property? Yes, Object.hasOwnProperty().
Using a for…in Loop with Object.hasOwnProperty()
The for...in
loop iterates over all enumerable properties of an object, including those inherited from the prototype chain. To ensure that only the object’s own properties (and not any inherited properties) are added to the Map, the Object.hasOwnProperty()
method can be used.
Object.hasOwnProperty(prop)
is a method that checks whether the object has the specified property as its own property (as opposed to inheriting it). Its useful in situations where you don’t want to include inherited properties in your Map.
- In this example,
MyObject
is a constructor function with a prototype propertyinheritedProperty
. - An instance of
MyObject
,obj
, is created and an additional propertyownProperty2
is added to it. - When we use the
for...in
loop to iterate over properties ofobj
, thehasOwnProperty()
check ensures that onlyownProperty1
andownProperty2
are added to the map, and theinheritedProperty
from the prototype is excluded.
function MyObject() {
this.ownProperty1 = "value1";
}
MyObject.prototype.inheritedProperty = "value2";
const obj = new MyObject();
obj.ownProperty2 = "value3";
const map = new Map();
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
map.set(key, obj[key]);
}
}
console.log(map);
// Output:
// Map(2) {"ownProperty1" => "value1", "ownProperty2" => "value3"}
Useful method if Object has inheritance, and need granular control while creating Map using Object’s own properties only.
- ⚠ Forgetting to use Object.hasOwnProperty(): If
for...in
loop directly used, then it will loop on inherited properties too. - ⚠
for...in
doesn’t behave LikeObject.entries()
: Note, unlikeObject.entries()
, which only considers an object’s own enumerable properties,for...in
iterates over all enumerable properties, including those inherited. Not recognizing this difference can lead to incorrect assumptions about the properties being iterated over. - See below code example and explanation:
// Define a prototype with an inherited property
function PrototypeObject() {
this.inheritedProperty = "inheritedValue";
}
// Create an object that inherits from PrototypeObject
PrototypeObject.prototype.additionalProperty =
"additionalValue";
const obj = new PrototypeObject();
obj.ownProperty = "ownValue";
// 1️⃣ Create a Map without using hasOwnProperty
const mapWithoutHasOwnProperty = new Map();
for (const key in obj) {
mapWithoutHasOwnProperty.set(key, obj[key]);
// This includes both own and inherited properties
}
console.log(
"Map without hasOwnProperty:",
mapWithoutHasOwnProperty,
);
// Output:
// Map without hasOwnProperty: Map(3) {
// 'inheritedProperty' => 'inheritedValue',
// 'ownProperty' => 'ownValue',
// 'additionalProperty' => 'additionalValue'
// }
// 2️⃣ Using Object.entries() for comparison
const mapWithObjectEntries = new Map(Object.entries(obj));
console.log(
"Map with Object.entries():",
mapWithObjectEntries,
);
// Output:
// Map with Object.entries(): Map(2) {
// 'inheritedProperty' => 'inheritedValue',
// 'ownProperty' => 'ownValue'
// }
Explanation:
- In the first part, where we don’t use
hasOwnProperty
, the mapmapWithoutHasOwnProperty
ends up containing both the properties defined onobj
itself (ownProperty
) and those inherited fromPrototypeObject
(additionalProperty
). - In the second part, we use
Object.entries()
to createmapWithObjectEntries
. This method only considers the object’s own enumerable properties, so it includes onlyinheritedProperty
andownProperty
, but notadditionalProperty
which is an inherited property.
Using Object.getOwnPropertyNames() for non enumerable properties
Non-enumerable properties are those that are not listed during standard object property enumeration processes, such as a for...in
loop or methods like Object.keys()
. They are often used for properties that are meant to be hidden from typical object property iterations.
The method Object.getOwnPropertyNames()
returns an array of all properties (both enumerable and non-enumerable) found directly upon a given object. This differs from Object.keys()
or Object.entries()
, which only return enumerable properties. This technique is essential when you need to work with all properties of an object, including those that are not enumerable.
In following example, myObject
has both a visible (enumerable) and a hidden (non-enumerable) property. Object.getOwnPropertyNames(myObject)
returns an array of both these property names, which we then use to populate a new Map
. This map includes both the enumerable and non-enumerable properties.
const myObject = {
visibleProperty: "I am visible",
};
Object.defineProperty(myObject, "hiddenProperty", {
value: "I am hidden",
enumerable: false,
});
const map = new Map();
// Using Object.getOwnPropertyNames() to include non-enumerable properties
Object.getOwnPropertyNames(myObject).forEach((prop) => {
map.set(prop, myObject[prop]);
});
console.log(map);
// Output: Map(2) {"visibleProperty" => "I am visible", "hiddenProperty" => "I am hidden"}
- Ideal when you need to consider every property of an object, especially in cases like deep cloning, property serialization, or when dealing with objects with hidden properties.
- But be aware, that non-enumerable properties might be set for specific reasons, such as hiding internal properties that are not meant to be exposed or iterated over.
Using Object.getOwnPropertySymbols() for Symbol Keyed Properties
In JavaScript, symbols are used as unique property keys for objects. These symbol-keyed properties are special because they are not enumerable through standard methods like for...in
loops or Object.keys()
.
To access these symbol properties, Object.getOwnPropertySymbols()
is used. This method retrieves an array of all symbol properties found directly on a given object. It’s crucial for handling objects that use symbols as property keys, ensuring that no such properties are overlooked.
- In following example,
obj
has two symbol-keyed properties (symbolKey1
andsymbolKey2
) along with a normal enumerable property (normalKey
). - The
Object.getOwnPropertySymbols(obj)
method retrieves an array of the object’s symbol properties. - The
forEach
loop is then used to iterate over these symbol properties, adding each one to the map along with its corresponding value.
const symbolKey1 = Symbol('key1');
const symbolKey2 = Symbol('key2');
const obj = {
[symbolKey1]: 'value1',
[symbolKey2]: 'value2',
normalKey: 'value3'
};
const map = new Map();
// Retrieving and adding symbol-keyed properties to the map
Object.getOwnPropertySymbols(obj).forEach(symbolKey => {
map.set(symbolKey, obj[symbolKey]);
});
console.log(map);
// Output: Map(2) {Symbol(key1) => "value1", Symbol(key2) => "value2"}
Using Reflect.ownKeys()
for a Complete Property List
Reflect.ownKeys()
is a method in JavaScript that returns an array of all properties found directly upon a given object.- This includes all enumerable and non-enumerable properties as well as symbol properties.
- Unlike
Object.keys()
orObject.getOwnPropertyNames()
,Reflect.ownKeys()
provides the most comprehensive list of an object’s properties. - This method is particularly useful for a thorough conversion of an object’s properties to a Map, ensuring that no properties, regardless of their nature, are omitted.
- In following example,
obj
includes a normal enumerable property, a symbol property, and a non-enumerable property. - Using
Reflect.ownKeys(obj)
, we obtain all these property keys, including the symbol and non-enumerable ones, and then iterate over them to create a new Map. - This results in a map that represents every property of
obj
, regardless of their enumerability or type.
const symbolKey = Symbol("symbolKey");
const obj = {
normalKey: "value1",
[symbolKey]: "value2",
};
Object.defineProperty(obj, "nonEnumerableKey", {
value: "value3",
enumerable: false,
});
const map = new Map();
Reflect.ownKeys(obj).forEach((key) => {
map.set(key, obj[key]);
});
console.log(map);
// Output:
// Map(3) {
// "normalKey" => "value1",
// Symbol(symbolKey) => "value2",
// "nonEnumerableKey" => "value3"
// }
- ⚠ Avoid for Simple Scenarios: While
Reflect.ownKeys()
is powerful, it’s important not to overuse it, especially in simple scenarios where such detailed property enumeration is not necessary. Overusing this method can lead to unnecessarily complex code and potential performance implications. - ⚠ Misunderstanding Symbol Properties: Symbols are often used for specific purposes, such as representing unique keys that won’t clash with other properties. Misusing symbol properties or not handling them correctly can lead to issues in code logic, especially in property access and manipulation.
Tips & errors while converting Object to Map in JavaScript
Lets summarise key issues which can arise from techniques above:
Object.entries()
andMap()
Constructor: Ideal for simple objects, but be mindful of null or undefined values.Array.map()
withObject.keys()
: Useful for transforming object properties during conversion, but it doesn’t account for non-enumerable properties.for...of
Loop withObject.entries()
: Offers more control, especially for adding conditional logic, but remember to check for object’s own properties.for...in
Loop withObject.hasOwnProperty()
: Ensures only the object’s own properties are processed, filtering out inherited ones.Object.getOwnPropertyNames()
: Retrieves all properties, including non-enumerable ones, for a complete overview.Object.getOwnPropertySymbols()
: Specifically targets symbol-keyed properties, ensuring they are included in the conversion.Reflect.ownKeys()
: Provides the most comprehensive list of properties, including non-enumerable and symbol properties, but should be used judiciously.
Further lets explore some more possible issues below.
Handling Nested Objects
Converting objects with nested structures to maps can be challenging because standard conversion techniques (like Object.entries()
or a for...in
loop) do not automatically convert nested structures into their own map entries. Instead, these nested objects or arrays are treated as single entities/values. This might not be ideal if you want to maintain the nested structure’s granularity within the map.
Possible solutions:
- Recursive Techniques: Implement a recursive function to handle nested objects. This function would iterate through each property and, if a property is an object itself, recursively apply the same conversion logic. Find below code sample for the same.
- Flattening the Structure: Flatten the nested object before conversion. This involves transforming the nested structure into a single-level object with compound keys. However, this might lose the original structure’s hierarchy.
const nestedObject = {
level1: {
level2: {
key: "value",
},
},
};
// 1️⃣ Using Object.entries and Map constructor will not recurseively convert nested objects to Maps, but will still keep entire value as object and place agains Map key.
console.log(new Map(Object.entries(nestedObject)));
// Output: Map(1) { 'level1' => { level2: { key: 'value' } } }
// 2️⃣ But if want to convert nested Objects also to Maps, then need recursion as follows:
function convertToMap(obj) {
const map = new Map();
for (const key in obj) {
if (typeof obj[key] === "object") {
// Recursive call for nested objects
map.set(key, convertToMap(obj[key]));
} else {
map.set(key, obj[key]);
}
}
return map;
}
const map = convertToMap(nestedObject);
console.log(map);
// Output: Map(1) {"level1" => Map(1) {"level2" => Map(1) {"key" => "value"}}}
Dealing with Non-String Keys
JavaScript objects typically use strings as keys, but Maps can use almost any value, including objects, for keys. When converting objects to Maps, you might encounter non-string keys, especially when dealing with complex data structures or integrating with other systems.
Possible solutions:
- Key Transformation: Transform non-string keys into string representations if maintaining the key type is not crucial. This could involve serialization or applying a hashing function.
- Using Maps for Complex Keys: Utilize Maps directly when dealing with complex keys, as Maps naturally handle non-string keys, including objects and symbols, allowing for more flexible data structures.
const objWithNonStringKeys = {
[Symbol('symbolKey')]: 'symbolValue',
[123]: 'numericValue'
};
const map = new Map(Object.entries(objWithNonStringKeys).map(([key, value]) => {
// Transforming non-string object keys (like symbols) to string
const transformedKey = typeof key === 'symbol' ? key.toString() : key;
return [transformedKey, value];
}));
console.log(map);
// Output: Map(2) {"Symbol(symbolKey)" => "symbolValue", "123" => "numericValue"}
🧪Practice Coding Problem: Converting Complex Object to Map
In the spirit of Test Driven Learning ( 😁), lets test our understanding by solving a problem.
You have a complex object representing a library system. This object contains nested structures, symbol-keyed properties, and a mix of enumerable and non-enumerable properties. Your task is to write a function that converts this complex object into a Map, ensuring all types of properties (including nested, symbol-keyed, and non-enumerable) are appropriately converted.
Problem (JavaScript)function convertLibraryObjectToMap(obj) { // > > > 👉 Write code here 👈 < < < } const librarySymbol = Symbol('library'); const library = { books: { 'Moby Dick': { author: 'Herman Melville', year: 1851 }, '1984': { author: 'George Orwell', year: 1949 }, nestedSection: { 'The Hobbit': { author: 'J.R.R. Tolkien', year: 1937 } } }, [librarySymbol]: 'Main Library', }; Object.defineProperty(library, 'location', { value: '123 Library St.', enumerable: false }); // Driver Code: const libraryMap = convertLibraryObjectToMap(library); console.log(libraryMap); /* Output should be a Map containing keys like 'books', 'librarySymbol', 'location', and within 'books', another Map for nestedSection. */
Please attempt before seeing the Answer:
function convertLibraryObjectToMap(obj) {
const map = new Map();
const keys = Reflect.ownKeys(obj); // Get all property keys
keys.forEach(key => {
const value = obj[key];
if (value && typeof value === 'object' && !Array.isArray(value)) {
// Recursive call for nested objects
map.set(key, convertLibraryObjectToMap(value));
} else {
map.set(key, value);
}
});
return map;
}
const libraryMap = convertLibraryObjectToMap(library);
console.log(libraryMap);
Explanation:
- Recursive Function Approach: function calls itself when it encounters a nested object. This ensures that even deeply nested structures are correctly converted into Maps at each level.
- Use of
Reflect.ownKeys()
: to retrieve all keys of the object, including non-enumerable and symbol properties. Ensures that all types of properties are considered in the conversion, not just the enumerable ones. - Iterating Over All Properties: The function iterates over each key retrieved from
Reflect.ownKeys(obj)
. For each key, it checks if the corresponding value is an object and not an array (usingtypeof value === 'object' && !Array.isArray(value)
). This check is important to differentiate between objects (which might need further recursive conversion) and other types of properties. - Handling Nested Objects: If the value is a nested object, the function makes a recursive call:
convertLibraryObjectToMap(value)
. This call converts the nested object into a Map. The recursive nature of the function ensures that no matter how deeply an object is nested, it gets converted into a nested Map. - Setting Values in the Map: For each key, the corresponding value (or the nested Map if the value is an object) is added to the current Map using
map.set(key, value)
. - Returning the Map: Finally, after all keys have been processed, the function returns the Map that represents the complete structure of the original object.
Now you are an expert in converting an Object to a Map in JavaScript, with all intricacies involved.
Keep learning, and keep coding! 🚀👨💻