The common data collections in JavaScript are lists (Array) and mapping tables (Plain Object). This article talks about mapping tables.

Due to the dynamic nature of JavaScript, the object itself is a mapping table, and the “attribute name ⇒ attribute value” of an object is the “key ⇒ value” in the mapping table. To make it easier to use the object as a mapping table, JavaScript even allows property names that are not identifiers – any string can be used as a property name. Of course, non-identifier property names can only be accessed using [], not the . sign.

Using [] to access object properties fits better with the mapping table access form, so when using an object as a mapping table, it is common to use [] to access the table elements. In this case, the content of [] is called the “key” and the access operation is to access the “value”. Therefore, the basic structure of a mapping table element is called a “key-value pair”. In JavaScript objects, keys are allowed to have

In JavaScript objects, there are three types of keys allowed: number, string, and symbol.

Keys of type number are mainly used as array indexes, and arrays can be considered as special mapping tables, where the keys are usually consecutive natural numbers. However, during mapping table access, the number type keys are converted to string type for use.

Symbol-type keys are less frequently used, and are generally used as a specification for special Symbol keys, such as Symbol.iterator. symbol-type keys are usually used for stricter access control, and elements with the key type symbol type.

I. CRUD

Create object mapping tables directly using { } to define Object Literal on the line, basic skills, no need to elaborate. However, it should be noted that { } is also used in JavaScript to encapsulate blocks of code, so using Object Literal in an expression often requires wrapping it in a pair of parentheses, like this: ({ }). This is especially important when using arrow function expressions that return an object directly.

The [] operator is used for adding, changing, and checking mapping table elements.

If you want to determine whether an attribute exists, some people have the habit of using ! !map[key], or map[key] === undefined. If you want to determine exactly whether a key exists, you should use the in operator.

1
2
3
4
5
6
7
const a = { k1: undefined };

console.log(a["k1"] !== undefined);  // false
console.log("k1" in a);              // true

console.log(a["k2"] !== undefined);  // false
console.log("k2" in a);              // false

Similarly, to delete a key, instead of changing its value to undefined or null, the delete operator is used.

1
2
3
4
5
6
const a = { k1: "v1", k2: "v2", k3: "v3" };

a["k1"] = undefined;
delete a["k2"];

console.dir(a); // { k1: undefined, k3: 'v3' }

The k2 attribute of a no longer exists after using the delete a[“k2”] operation.

Note

In the above two examples, ESLint may report a violation of the dot-notation rule because k1, k2, and k3 are all legal identifiers. In this case, you can either turn off this rule or use . symbolic access (the team decides how to handle this).

II. Lists in mapping tables

A mapping table can be viewed as a list of key-value pairs, so a mapping table can be converted into a list of key-value pairs for processing.

A key-value pair is generally called a key value pair or entry in English, and is described by Map.Entry<K, V> in Java; KeyValuePair<TKey, TValue> in C#; JavaScript is more straightforward, using an array of only two elements to represent a key-value pair, such as [“key” , “value”].

In JavaScript, you can use Object.entries(it) to get a list of key-value pairs formed by [key, value].

1
2
3
const obj = { a: 1, b: 2, c: 3 };
console.log(Object.entries(obj));
// [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]

In addition to entry lists, mapping tables can also separate keys and values to get a separate list of keys, or a list of values. To get a list of keys for an object, use the Object.keys(obj) static method; accordingly, to get a list of values use the Object.values(obj) static method.

1
2
3
4
const obj = { a: 1, b: 2, c: 3 };

console.log(Object.keys(obj));      // [ 'a', 'b', 'c' ]
console.log(Object.values(obj));    // [ 1, 2, 3 ]

III. Iterative mapping table

Since a mapping table can be viewed as a list of key-value pairs or a list of keys or values that can be obtained individually, there are more ways to iterate through a mapping table.

The most basic way is to use a for loop. Note, however, that since mapping tables usually do not have ordinal numbers (index numbers), they cannot be traversed by a normal for(;;;) loop, but rather by using for each. Interestingly, however, for… .in can be used to iterate through all the keys of a mapping table; however, using for… .of on a mapping table will error out because the object “is not iterable”.

1
2
3
4
5
6
7
const obj = { a: 1, b: 2, c: 3 };
for (let key in obj) {
    console.log(`${key} = ${obj[key]}`);   // 拿到 key 之后通过 obj[key] 来取值
}
// a = 1
// b = 2
// c = 3

Since the mapping table can get the key set and value set separately, it is more flexible to handle the traversal. But usually we usually use both keys and values, so in practice, it is more common to iterate over all entries of the mapping table.

1
2
Object.entries(obj)
    .forEach(([key, value]) => console.log(`${key} = ${value}`));

IV. From Lists to Mapping Tables

The previous two subsections were all about how to convert a mapping table into a list. In turn, what about generating a mapping table from a list?

To generate a mapping table from a list, the most basic operation is to generate an empty mapping table, and then iterate through the list, from each element to get the “key” and “value” to add them to the mapping table, such as the following example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const items = [
    { name: "size", value: "XL" },
    { name: "color", value: "中国蓝" },
    { name: "material", value: "涤纶" }
];

function toObject(specs) {
    return specs.reduce((obj, spec) => {
        obj[spec.name] = spec.value;
        return obj;
    }, {});
}

console.log(toObject(items));
// { size: 'XL', color: '中国蓝', material: '涤纶' }

This is the usual operation. Notice that Object also provides a fromEntries() static method, so if we prepare a list of key-value pairs, we can use Object.fromEntries() to quickly get the corresponding object.

1
2
3
4
5
function toObject(specs) {
    return Object.fromEntries(
        specs.map(({ name, value }) => [name, value])
    );
}

V. A small use case

During data processing, lists and mapped tables often need to be converted to each other to achieve more readable code or better performance. Two key methods of conversion have been covered earlier in this article.

  • Object.entries() converts a mapped table into a list of key-value pairs

  • Object.fromEntries() generates a mapping table from a list of key-value pairs

In which cases might these conversions be used? There are many application scenarios, for example, here is a more classic case.

asks the question.

Got all the nodes of a tree from the backend, and the parent relationship between the nodes is described by the parentId field. Now what should I do if I want to build it into a tree structure? Sample data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[
 { "id": 1, "parentId": 0, "label": "第 1 章" },
 { "id": 2, "parentId": 1, "label": "第 1.1 节" },
 { "id": 3, "parentId": 2, "label": "第 1.2 节" },
 { "id": 4, "parentId": 0, "label": "第 2 章" },
 { "id": 5, "parentId": 4, "label": "第 2.1 节" },
 { "id": 6, "parentId": 4, "label": "第 2.2 节" },
 { "id": 7, "parentId": 5, "label": "第 2.1.1 点" },
 { "id": 8, "parentId": 5, "label": "第 2.1.2 点" }
]

The general idea is to first build an empty tree (imaginary root), then read the list of nodes in order, and for each node read, find the correct parent (or root) node from the tree and insert it. This idea is not complicated, but in practice, we encounter two problems

  1. finding a node in the generated tree itself is a complex process, whether by recursion through depth traversal or by queueing through breadth traversal, which requires writing relatively complex algorithms and is time consuming.
  2. for the list of all node order, if you can not guarantee that the child node after the parent node, the complexity of processing will greatly increase.

To solve the above two problems is not difficult, just need to first iterate through all nodes, generate a [id => node] mapping table is good to do. Assuming that the data is referenced by the variable nodes, the mapping table can be generated with the following code.

1
2
3
const nodeMap = Object.fromEntries(
    nodes.map(node => [node.id, node])
);

Without going into the details of the process, interested readers can read: Generating Trees from Lists (JavaScript/TypeScript).

VI. Mapping table splitting

The mapping table itself does not support splitting, but we can select some key-value pairs from it according to certain rules to form a new mapping table for the purpose of splitting. This process is Object.entries() ⇒ filter() ⇒ Object.fromEntries(). For example, if you want to exclude all properties with underscore prefixes from a configuration object.

1
2
3
4
5
6
const options = { _t1: 1, _t2: 2, _t3: 3, name: "James", title: "Programmer" };

const newOptions = Object.fromEntries(
    Object.entries(options).filter(([key]) => !key.startsWith("_"))
);
// { name: 'James', title: 'Programmer' }

However, it is more straightforward to use delete when you know exactly which elements you want to get rid of.

Here is another example.

Ask the question.

The problem is that a lot of code applying this asynchronous operation will take some time to complete the migration, and in the meantime, the old interface still needs to be executed correctly. During this time, the old interface still needs to be executed correctly.

For compatibility during the migration, this code needs to take the success and fail objects out of the parameter object, remove them from the original parameter object, and give the processed parameter object to the new business logic. The operation to remove the success and fail entries can be done with delete.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
async function asyncDoIt(options) {
    const success = options.success;
    const fail = options.fail;
    delete options.success;
    delete options.fail;
    try {
        const result = await callNewProcess(options);
        success?.(result);
    } catch (e) {
        fail?.(e);
    }
}

This is a moderate approach, taking 4 lines of code to handle two special entries, the first two of which can easily be simplified by using destructuring.

1
const { success, fail } = options;

But did you notice that the last two sentences can also be merged in?

1
const { success, fail, ...opts } = options;

The opts we get here are the option list excluding the success and failure entries!

Further, we can use the deconstruction argument syntax to move the deconstruction process to the argument list. Here is the modified asyncDoIt.

1
2
3
async function asyncDoIt({ success, fail, ...options } = {}) {
    // TODO try { ... } catch (e) { ... }
}

Using the deconstructed split mapping table makes the code look very clean, and such a way of defining functions can be copied to the arrow functions as processing functions during chained data processing. In this way, splitting data is solved by hand when defining parameters, and the code as a whole will look very concise and clear.

VII. Merge mapping table

To merge mapping tables, the basic operation must be to add them in a loop, which is not recommended.

Since the new features of JavaScript provide a more convenient way to do this, why not use them? There are basically only two new features.

  • Object.assign()

  • expand operator

The syntax and interface descriptions can be found on MDN, but here are some examples.

Ask the question

There is a function whose arguments are a list of options, and in order to facilitate the use of the caller does not need to provide all the options, the options not provided all use the default option values. But it is too tedious to determine one by one, is there a simpler way?

Yes, of course there is! Use Object.assign() ah.

1
2
3
4
5
6
7
8
const defaultOptions = {
    a: 1, b: 2, c: 3, d: 4
};

function doSomthing(options) {
    options = Object.assign({}, defaultOptions, options);
    // TODO 使用 options
}

Once you know it, you will find that it is still very simple to use. But simple is simple, but there are still some pitfalls.

The first argument of Object.assign() must be given an empty mapping table, otherwise defaultOptions will be modified, because Object.assign() will merge the entries of each argument into its first argument (mapping table).

To avoid accidental modification of defaultOptions, you can “freeze” it.

1
2
3
4
const defaultOptions = Object.freeze({
//                     ^^^^^^^^^^^^^^
    a: 1, b: 2, c: 3, d: 4
});

In this way, Object.assign(defaultOptions, …) will report an error.

Alternatively, this can be achieved by using the expand operator.

1
options = { ...defaultOptions, ...options };

An even bigger advantage of using the expand operator is that it is easy to add a single entry, unlike Object.assign() where you have to wrap the entry into a mapping table.

1
2
3
4
5
6
7
8
9
function fetchSomething(url, options) {
    options = {
        ...defaultOptions,
        ...options,
        url,        // 键和变量同名时可以简写
        more: "hi"  // 普通的 Object Literal 属性写法
    };
    // TODO 使用 options
}

After all the talk, there is still a big hole in the merge process above, I don’t know if you found it? – The above is talking about merging mapping tables, not merging objects. Although a mapping table is an object, the entry of a mapping table is a simple key-value pair relationship; unlike an object, which has a hierarchy and depth of properties.

For example.

1
2
3
const t1 = { a: { x: 1 } };
const t2 = { a: { y: 2 } };
const r = Object.assign({}, t1, t2);    // { a: { y: 2 } }

The result is { a: { y: 2} } instead of { a: { x: 1, y: 2 } }. The former is the result of a shallow merge, where the entries of the mapping table are merged; the latter is the result of a deep merge, where multiple layers of properties of the object are merged.

It’s a lot of work to write a deep merge by hand, but Lodash provides the _.merge() method, so you can use it off the shelf. _.merge() may not work as expected when merging arrays, in this case use _.mergeWith() to customize the array merge, there is a ready-made example in the documentation.

VIII. Map Class

JavaScript also provides a specialized Map class, which, in contrast to Plain Object, allows arbitrary types of “keys”, not limited to string.

All the operations mentioned above have corresponding methods in Map. Without going into detail, a brief description is sufficient.

  • add/modify, using the set() method.
  • get() method to get the value by key.
  • delete by key, using the delete() method, and a clear() method that clears the map table directly.
  • has() access to determine if a key-value pair exists.
  • size property to get the number of entries, unlike Plain Object which needs Object.entries(map).length to get it.
  • entries(), keys() and values() methods are used to get a list of entries, keys and values, but the result is not an array, but an Iterator.
  • There is also a forEach() method used to iterate directly, and the handler does not receive the entire entry (i.e., ([k, v])), but a separated (value, key, map).

Summary

Are you using objects or mapping tables in JavaScript? To be honest, it’s not really easy to say. As a mapping table, the methods mentioned above are sufficient, but as an object, JavaScript provides more tools and methods, so check the Object API and Reflect API.

By mastering how to manipulate lists and mapping tables, you can basically solve various JavaScript data processing problems that you encounter on a daily basis. Data conversion, data grouping, group expansion, and tree data …… are just a few of the things you can do. In general, the native JavaScript API is sufficient, but if you encounter more complex situations (such as grouping), you may want to check the Lodash API, which is a professional data processing tool after all.