1console.log("hello-world);
console.log("hello-world);
.container {
width: 80%;
}
<pre><code class="language-css">
.container {
width: 80%;
}
</code></pre>
1console.log("hello-world);
console.log("hello-world);
.container {
width: 80%;
}
<pre><code class="language-css">
.container {
width: 80%;
}
</code></pre>
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! 👨💻
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.
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 like Object.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.
set()
, get()
, has()
, and delete()
, making it easier to work with the data. They also provide iterators which make looping over the map straightforward.// 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");
Lets see various techniques to convert JavaScript Object to Map.
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 of Object.entries()
is perfectly aligned with what the Map
constructor expects as input.
In following example, we first define an object obj
with three key-value pairs. We then use Object.entries(obj)
to convert this object into an array of [key, value]
pairs. Finally, we pass this array to the Map
constructor to create a new Map
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"}
for...in
and are returned by methods like Object.keys()
and Object.entries()
).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.
null
and undefined
Values: A common mistake is not accounting for null
or undefined
object properties. If your object might contain such values, you should handle them appropriately before converting. The Map
constructor won’t throw an error, but the presence of null
or undefined
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}
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 use map()
to transform each key-value pair. The keys are converted to uppercase using key.toUpperCase()
, and the values are multiplied by 2. This transformed array is then passed to the Map
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}
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 by Object.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
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 using map.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 a for...in
loop, you might unintentionally iterate over inherited properties.
In the context of Object.entries()
with a for...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. Because Object.entries()
ignores the inherited properties, the for...of
loop only iterates over the properties that are actually part of obj
itself, not those inherited from prototype
.
// 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().
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.
MyObject
is a constructor function with a prototype property inheritedProperty
.MyObject
, obj
, is created and an additional property ownProperty2
is added to it.for...in
loop to iterate over properties of obj
, the hasOwnProperty()
check ensures that only ownProperty1
and ownProperty2
are added to the map, and the inheritedProperty
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.
for...in
loop directly used, then it will loop on inherited properties too.for...in
doesn’t behave Like Object.entries()
: Note, unlike Object.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.// 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:
hasOwnProperty
, the map mapWithoutHasOwnProperty
ends up containing both the properties defined on obj
itself (ownProperty
) and those inherited from PrototypeObject
(additionalProperty
).Object.entries()
to create mapWithObjectEntries
. This method only considers the object’s own enumerable properties, so it includes only inheritedProperty
and ownProperty
, but not additionalProperty
which is an inherited property.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"}
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.
obj
has two symbol-keyed properties (symbolKey1
and symbolKey2
) along with a normal enumerable property (normalKey
).Object.getOwnPropertySymbols(obj)
method retrieves an array of the object’s symbol properties.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"}
Reflect.ownKeys()
for a Complete Property ListReflect.ownKeys()
is a method in JavaScript that returns an array of all properties found directly upon a given object.Object.keys()
or Object.getOwnPropertyNames()
, Reflect.ownKeys()
provides the most comprehensive list of an object’s properties.obj
includes a normal enumerable property, a symbol property, and a non-enumerable property.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.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"
// }
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.Lets summarise key issues which can arise from techniques above:
Object.entries()
and Map()
Constructor: Ideal for simple objects, but be mindful of null or undefined values.Array.map()
with Object.keys()
: Useful for transforming object properties during conversion, but it doesn’t account for non-enumerable properties.for...of
Loop with Object.entries()
: Offers more control, especially for adding conditional logic, but remember to check for object’s own properties.for...in
Loop with Object.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.
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:
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"}}}
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:
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"}
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:
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.Reflect.ownKeys(obj)
. For each key, it checks if the corresponding value is an object and not an array (using typeof value === 'object' && !Array.isArray(value)
). This check is important to differentiate between objects (which might need further recursive conversion) and other types of properties.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.map.set(key, value)
.Now you are an expert in converting an Object to a Map in JavaScript, with all intricacies involved.
Keep learning, and keep coding! 🚀👨💻
Feel free to reach out!