28
头图

Commonly used data collections in JavaScript are lists (Array) and mapping tables (Plain Object). The list has already been discussed, this time we will talk about the mapping table.

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

Using [] to access object attributes is more in line with the access form of the mapping table, so when using objects as a mapping table, you usually use [] to access table elements. At this time, the content in [] is called "key", and the access operation accesses "value". Therefore, the basic structure of a map element is called a "key-value pair".

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

Keys of type number are mainly used as array indexes, and arrays can also be considered as special mapping tables whose keys are usually consecutive natural numbers. However, in the process of accessing the mapping table, the key of type number will be converted to type string for use.

Symbol type keys are rarely used, and generally use some special Symbol keys according to the specification, such as Symbol.iterator . Keys of type symbol are usually used for stricter access control. When using Object.keys() and Object.entries() to access related elements, elements whose key type is symbol type will be ignored.

1. CRUD

To create an object mapping table, use { } define Object Literal directly. Basic skills do not need to be described in detail. But it should be noted that { } is also used to encapsulate code block in JavaScript, so when using Object Literal for expressions, it is often necessary to use a pair of parentheses to wrap it, like this: ({ }) . This is especially important when using arrow function expressions to return an object directly.

The [] operator is used to add, modify, and check the elements of the mapping table.

If you want to judge whether an attribute exists, some people use !!map[key] or map[key] === undefined to judge. When using the former, pay attention to the when using the latter, pay attention to the possibility that the value itself is undefined . If you want to determine exactly whether a key exists, you should use the in operator:

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 , use the delete operator:

const a = { k1: "v1", k2: "v2", k3: "v3" };

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

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

The delete a["k2"] property of a no longer exists after the operation with k2 .

Note

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

Second, the list in the mapping table

The mapping table can be regarded as a list of key-value pairs, so the mapping table can be converted into a list of key-value pairs for processing.

key-value pair is generally called key value pair or entry in English, and KeyValuePair<TKey, TValue> is used to describe it in Java; Map.Entry<K, V> is used to describe it in C#; For example ["key", "value"] .

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

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

In addition to the entry list, the mapping table can also separate keys and values to obtain a separate key list or value list. To get a list of keys for an object, use the static method Object.keys(obj) ; correspondingly, to get a list of values, use the static method Object.values(obj) .

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

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

3. Traverse the mapping table

Since the mapping table can be regarded as a list of key-value pairs, or a list of keys or values can be obtained individually, there are many ways to traverse the mapping table.

The most basic way is to use for loop. However, it should be noted that since the mapping table usually does not have a sequence number (index number), it cannot be traversed through the ordinary for(;;) loop, but needs to be traversed using for each. But what's interesting is that for...in can be used to traverse all the keys in the mapping table; but using for...of on the mapping table will cause an error because the object "is not iterable" (not iterable, or not traversable).

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 will be more flexible in the processing of traversal. But usually we use keys and values at the same time, so in actual use, it is more commonly used to traverse all entries of the mapping table:

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

Fourth, from the list to the mapping table

The first two subsections are all about how to convert the mapping table into a list. Conversely, what about generating a mapping table from a list?

To generate a map from a list, the most basic operation is to generate an empty map, then traverse the list, get the "key" and "value" from each element, and add them to the map, such as the following example :

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 normal operation. Note that Object also provides a static method of fromEntries() . As long as we prepare a list of key-value pairs, we can quickly get the corresponding object using Object.fromEntries() :

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

Five, a small application case

In the process of data processing, it is often necessary to convert between lists and mapping tables to achieve more readable code or better performance. Two key methods of conversion have been covered earlier in this article:

  • Object.entries() converts the mapping table into a list of key-value pairs
  • Object.fromEntries() Generate mapping table from list of key-value pairs

In what situations might these transformations be used? There are many application scenarios. For example, here is a relatively classic case.

submit questions:

All nodes of a tree are obtained from the backend, and the parent relationship between nodes is described by the parentId field. Now what if you want to build it into a tree structure? Sample data:

[
 { "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 (virtual root), then read the node list in order, and each time a node is read, find the correct parent node (or root node) from the tree and insert it. This idea is not complicated, but in practice there will be two problems

  1. Finding a node in the generated tree itself is a complex process. Whether it is to search through depth traversal using recursion or through breadth traversal using queues, it is necessary to write a relatively complex algorithm, which is also time-consuming;
  2. For the order of all nodes in the list, if the child node cannot be guaranteed to be after the parent node, the processing complexity will be greatly increased.

It is not difficult to solve the above two problems, just traverse all the nodes and generate a mapping table of [id => node] . Assuming that these data are referenced by the variable nodes after they are obtained, the following code can be used to generate the mapping table:

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

The specific process will not be described in detail. Interested readers can read: Tree from List (JavaScript/TypeScript)

6. Splitting of the mapping table

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 to achieve the purpose of splitting. This process is Object.entries()filter()Object.fromEntries() . For example, you want to remove all underscore-prefixed properties in a configuration object:

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, using delete is more straightforward when it is very clear which elements to clear.

Here is another example:

submit questions:

A project is undergoing a technical upgrade. The original asynchronous request is to pass success and fail in the parameters. The callback is asynchronous. The new interface is changed to Promise style, and success and fail are no longer required in the parameters. The problem now is: a lot of code that uses this asynchronous operation needs a certain amount of time to complete the migration, and during this time, it is still necessary to ensure that the old interface can be implemented correctly.

For compatibility during migration, this code needs to take out success and fail in the parameter object, remove them from the original parameter object, and then hand over the processed parameter object to the new business processing logic. Here, the operation of removing the two entries of success and fail can be done with delete .

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 the norm, and it took 4 lines of code to handle the two special entries. The first two sentences of which it is easy to think that destructuring can be used to simplify:

const { success, fail } = options;

But have you found that the last two sentences can also be combined? look--

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

The opts here is not the option table that excludes the two entries of success and fail !

Going a step further, we can use the destructuring parameter syntax to move the destructuring process into the parameter list. Here is the modified asyncDoIt :

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

Using destructuring to split the mapping table makes the code look very concise, and this function definition method can be copied to arrow functions as processing functions in the chained data processing process. In this way, splitting data is easily solved when defining parameters, and the overall code looks very concise and clear.

Seven, merge mapping table

To merge the mapping table, the basic operation must be cyclic addition, which is not recommended.

Since the new features of JavaScript provide more convenient methods, why not! There are basically two new features:

  • Object.assign()
  • spread operator

The syntax and interface description can be seen on MDN. Here is the case:

submit questions

The parameter of one function is an option list. For the convenience of use, the caller does not need to provide all options. The options not provided all use the default option values. But it is too complicated to judge one by one. Is there a simpler way?

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

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

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

This question may be raised because I don't know Object.assign() . Once I know it, I will find that it is still very simple to use. But simple is simple, there are still pits.

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

In order to avoid accidental modification of defaultOptions , it can be "frozen":

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

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

Alternatively, using the spread operator can also be achieved:

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

The greater advantage of using the spread operator is that it is also very convenient to add a single entry, unlike Object.assign() , which must encapsulate the entry into a mapping table.

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

After talking for a long time, there is still a big hole in the above merger process. I wonder if you found it? - The above has been talking about merging mapping table , not merging objects. Although the mapping table is an object, the entry of the mapping table is a simple key-value pair relationship; while the object is different, the attributes of the object have levels and depths.

for example,

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 the shallow merge, which merges the entries of the mapping table; the latter is the result of the deep merge, which merges the multi-layer attributes of the object.

The workload of handwritten deep merging is not small, but Lodash provides _.merge() methods, you may as well use ready-made ones. _.merge() may not meet expectations when merging the array . In this case, use _.mergeWith() to merge the custom processing arrays. There are ready-made examples in the documentation.

Eight, Map class

JavaScript also provides a professional Map class , which allows any type of "key", not limited to strings, compared to Plain Object.

The various operations mentioned above have corresponding methods in Map . No need to go into details, just a brief introduction:

  • Add/Modify, use set() method;
  • By key value, use get() method;
  • Delete according to the key, use the delete() method, and a clear() to directly clear the mapping table;
  • has() access is used to determine whether a key-value pair exists;
  • size attribute can get the entry number, unlike the Plain Object, which needs to be obtained with Object.entries(map).length ;
  • entries() , keys() and values() methods are used to get a list of entry, key and value, but the result is not an array, but an Iterator;
  • There is also a forEach() method that is directly used for traversal. The processing function does not receive the entire entry (ie ([k, v]) ), but a separate (value, key, map) .

summary

Are you using objects or maps in JavaScript? It's not easy to tell the truth. As a mapping table, the various methods mentioned above are sufficient, but as an object, JavaScript also provides more tool methods, you can check Object API and Reflect API for understanding.

Mastering the operation methods of lists and mapping tables can basically solve various JavaScript data processing problems encountered in daily life. Like what data conversion, data grouping, grouping expansion, tree data... are not a problem. In general, the JavaScript native API is sufficient, but if you encounter a complex situation (such as grouping), you may wish to check the Lodash API, after all, it is a professional data processing tool.

Don't forget to read the previous article: JavaScript Data Processing - List


边城
59.8k 声望29.6k 粉丝

一路从后端走来,终于走在了前端!