Is Map object faster than if-else and switch?

JavaScript/TypeScript
Sponsored links

Map object in javascript or typescript can be used as a key-value object like a tuple. It’s almost the same as Dictionary type in C#. Let’s master how to use Map object.

The environment is as follows.

Node.js 14.13.0
Windows 10 Professional 64bit
CPU Intel Core i5-7200U @2.50GHz
RAM 8 GB

Sponsored links

Basic usage of Map object

Map type can store any data. This is basic usage.

const map = new Map<string, number>();
const one = 1;
let two = 2;
const three = 3;
map.set("one", one)
    .set("two", two)
    .set("three", three);
console.log(`size: ${map.size}`);
console.log(map);
console.log("before update: " + map.get("two"));
two = 16;
console.log("after update: " + map.get("two"));
// ---- Result ----
// size: 3
// Map(3) { 'one' => 1, 'two' => 2, 'three' => 3 }
// before update: 2
// after update: 2

map.set function returns Map object, so its function call can be chained if you like.
In the example above, variable two is updated but its change doesn’t affect the value stored in map object.

Sponsored links

Storing Object type in Map object

Map can store object type too but we should know that the value change to the original variable affects the value stored in Map object. Let’s see the example.

const hoge: Person = { name: "hoge", age: 12 };
const foo: Person = { name: "foo", age: 22 };
const map = new Map<string, Person>();
map.set("hoge", hoge);
map.set("foo", foo)

console.log(`size: ${map.size}`);
console.log(map.get("hoge"));
hoge.name = "Updated hoge";
console.log(map.get("hoge"));

// ----Result----
// size: 3
// { name: 'hoge', age: 12 }
// { name: 'Updated hoge', age: 12 }

hoge.name is updated and its change affects the value stored in Map! Object type is stored as a reference to the variable. Therefore, its value change to the original variable affects the value stored in Map object. Be careful.

If it is an object type and you don’t want to have the side effect you need to copy the original object by using the spread operator.

map.set("foo", { ...foo })
console.log(map.get("foo"));
foo.name = "updated foo";
console.log(map.get("foo"));

// ---- Result ----
// { name: 'foo', age: 22 }
// { name: 'foo', age: 22 }

It works but let’s see another example.

let hoge = { name: "hoge", age: 12, deep: { value: 99 } };
const map = new Map<string, Person>();
map.set("hoge", { ...hoge });
hoge.deep.value = 1;

// ---- Result ----
// { name: 'hoge', age: 12, deep: { value: 1 } }

The value change to hoge.deep.value affects the result because the spread operator is a shallow copy. It copies only first-level values. It copies the reference for second or deeper level.
It can’t be used for class either because it doesn’t copy getter or function. Use one of the modules that create a complete copy. cloneCopy() from lodash is one of the options for that.

Using Map object instead of switch-case or if-else

Map can be used instead of if-else and switch-case because Map object can’t have the same key.

const map = new Map([
    [0, "Initializing"],
    [1, "Idle"],
    [2, "Running"],
    [3, "Stop"],
    [4, "Error"],
]);
getStatus(0);
getStatus(3);
getStatus(5);

function getStatusBySwitch(value: number) {
    switch (value) {
        case 0: return "Initializing";
        case 1: return "Idle";
        case 2: return "Running";
        case 3: return "Stop";
        case 4: return "Error";
        default: "Undefined";
    }
}
function getStatus(value: number) {
    console.log(`${getStatusBySwitch(value)}, ${map.get(value)}`);
}

// ---- Result ----
// Initializing, Initializing
// Stop, Stop
// Undefined, undefined

Its result is the same. This is powerful way when many cases are required, for example in the following cases

  • When a function wants to create a new class depending on the input
  • When many constant variables are necessary

Using Array function (reduce, filter, map, etc…)

Map object implements iterator instead of Array. Therefore, forEach function can be used but sometimes we want to use Array functions like reduce , filter ,map. However, those functions can’t be called directly. We need to convert it to Array first if we want to use those functions.

const map = new Map([
    ["Desk", 44],
    ["Chair", 23],
    ["Light", 36],
    ["Mat", 97],
]);
map.forEach((value: number, key: string) => {
    console.log(`${key}: ${value}`);
});
const sum = Array.from(map.values())
        .reduce((acc, cur) => acc + cur);
const ave = sum / map.size;
console.log(`sum: ${sum}, ave: ${ave}`);
const joinedKeys = Array.from(map.keys()).join(",");
console.log(joinedKeys);

// ---- Result ----
// Desk: 44
// Chair: 23
// Light: 36
// Mat: 97
// sum: 200, ave: 50
// Desk,Chair,Light,Mat

Performance comparison, Map, if-else, switch

Are you interested in performance? I measured it in this simple example.

import { performance } from "perf_hooks";
{
    const statusMap = new Map([
        [0, "Initializing"],
        [1, "Idle"],
        [2, "Running"],
        [3, "Stop"],
        [4, "Error"],
    ]);
    function getStatusByMap(value: number) {
        return statusMap.get(value);
    }
    function getStatusBySwitch(value: number) {
        switch (value) {
            case 0: return "Initializing";
            case 1: return "Idle";
            case 2: return "Running";
            case 3: return "Stop";
            case 4: return "Error";
            default: return "Undefined";
        }
    }
    function getStatusByIfElse(value: number) {
        if (value === 0) {
            return "Initializing";
        } else if (value === 1) {
            return "Idle";
        } else if (value === 2) {
            return "Running";
        } else if (value === 3) {
            return "Stop";
        } else if (value === 4) {
            return "Error";
        }
    }
    function measure(cb: (value: number) => void) {
        const start = performance.now();
        for (let i = 0; i < 100000000; i++) {
            const val = i % 5;
            cb(val);
        }
        const end = performance.now();
        console.log(`${cb.name}: ${end - start}`);
    }
    measure(getStatusByMap);
    measure(getStatusByIfElse);
    measure(getStatusBySwitch);
}

The result is the following. The unit is ms.

MapIf-ElseSwitch
517.18280000053351212.45980000030251353.3245999999344
494.46399999968711207.63379999995231012.4100999999791
764.17650000005961510.58670000080021148.4190999995917
575.77499999944121263.114299999551286.8793999999762

There is no big difference between if-else and switch but Map is 2 times faster. However, this loop count is not in practice. I changed it from 100,000,000 to 100,000. The result is the following.

MapIf-ElseSwitch
3.6659999992698436.2666999995708471.847099999897182
3.8366000000387435.2934999996796251.8441000003367662
4.101200000382964.55669999960809951.888600000180304

Switch case is the fastest but This tiny difference doesn’t cause any performance problem. I think Javascript and Typescript are not used for performance-sensitive software. Let’s write in the cleanest way.

Extra test

I added the number of conditions from 5 to 100.


const statusMap = new Map([
    [0, '0'],
    [1, '1'],
    [2, '2'],
    ...
    [99, '99'],
]);
function getStatusByMap(value: number) {
    return statusMap.get(value);
}
function getStatusBySwitch(value: number) {
    switch (value) {
        case 0: return '0';
        case 1: return '1';
        case 2: return '2';
        ...
        case 99: return '99';
        default: return "Undefined";
    }
}
function getStatusByIfElse(value: number) {
    if (value === 0) { return "0"; }
    else if (value === 1) { return '1' }
    else if (value === 2) { return '2' }
    ...
    else if (value === 99) { return '99' }
}
function measure(cb: (value: number) => void) {
    const start = performance.now();
    for (let i = 0; i < 100000000; i++) {
        const val = i % 100;
        cb(val);
    }
    const end = performance.now();
    return end - start;
}

function run() {
    const a = measure(getStatusByMap);
    const b = measure(getStatusByIfElse);
    const c = measure(getStatusBySwitch);
    console.log(`${a}|${b}|${c}`);
}

for (let i = 0; i < 10; i++) {
    run();
}

100 M times

MapIf-ElseSwitch
623.36420000065123021.07820000033833108.6668999996036
1850.95039999950682985.65500000026082928.4054000005126
1734.6088000005112934.5376000003893064.6140000000596
1833.81769999954852977.36770000029362985.345900000073
1864.10460000019523062.97699999995533109.095200000331
1887.88339999970054253.6975000007084481.461799999699
4189.8727000001826373.23039999976756872.441399999894
5618.2287000007937557.4495999999346910.5358999995515
3710.8312999997295727.2734999991954694.466000000015
1982.46160000003873348.70600000023844472.591199999675

100 K times

MapIf-ElseSwitch
6.6497999997809537.8190999999642376.39780000038445
2.2372000003233552.5900000007823112.590199999511242
4.679200000129644.407499999739234.497000000439584
4.1290999995544553.2102000005543233.1229999996721745
4.56049999967217454.6663999995216733.496199999935925
3.54769999999552974.7657000003382564.807300000451505
2.35049999970942743.76630000025033953.546000000089407
3.3315000003203753.04320000018924474.181400000117719
1.72099999990314252.711000000126663.557099999859929
1.62509999983012682.7242999998852613.3985999999567866

Well… It doesn’t make a big difference. Download the source code from my repository if you want to try it yourself.

Comments

Copied title and URL