JavaScript: Array.map()

Array.prototype.map()

map()
Calls one same provided callback function once for each element in original array and constructs a new array from the results.
In other word: The new array consists of the callback function result on each of the element.
Just think the new array is a map image of the original.
map(...)
Syntax
// Callback function
map(callbackFn)
map(callbackFn, thisArg)


// Inline callback function
map(function (element) {...})
map(function (element, index) {...})
map(function (element, index, array) {...})
map(function (element, index, array) {...}, thisArg)

// Arrow function
map((element)  => {...})
map((element, index)  => {...})
map((element, index, array)  => {...})
Parameters (2):
  • callbackFn: The callback function
  • thisArg (optional) : Value to use as this when running callback function. undefined is assigned if not provided.
Return value:
New array with each element being the result of the callback function.
Original array:
In general: not changed.
Unless manipulated during the map-callback function run, through access to the original array using array and index parameters. This is generally avoided, except in some special cases.
Callback function
function callbackFn(element, index, array) {
    ...
    return new element value;
}
// Element is pushed to the new array. It will occupy the same index.
Parameters (3):
  • element : Current element being process
  • index (optional): index of current element being process
  • array (optional): The original array
Return values:
  • New element value for the new array.
Map function is invoked only for cells with assigned values, including undefined.
It is not called for missing elements of the array.
  • Indexes that have never been set
  • Indexes which have been deleted
empty cell?
It's included in the new array, but map function is not called. I guess, it could have been merely "nothing is put in that cell" and the next element will go to the subsequent index.
const array = [1, "a", , undefined, null, NaN, true, false];

const map1 = array.map(el => el);
const map2 = array.map(el => !el);
const map3 = array.map(el => "cat");

array;  // (8) [1, 'a', empty, undefined, null, NaN, true, false]
map1;   // (8) [1, 'a', empty, undefined, null, NaN, true, false]
map2;   // (8) [false, false, empty, true, true, true, false, true]
map3;   // (8) ['cat', 'cat', empty, 'cat', 'cat', 'cat', 'cat', 'cat']

let counter = 0;
const map4 = array.map(el => {counter++; return el});
counter;    // 7
map4;       // (8) ['z', 'z', empty, 'z', 'z', 'z', 'z', 'z']
See that empty is included in the result list, but map function is not called. Evidence: empty spot, and counter is only 7.
Let's compare with similar operations in filter.
const array = [1, "a", , undefined, null, NaN, true, false];

const filter1 = array.filter(el => el);
const filter2 = array.filter(el => !el);
const filter3 = array.filter(el => el || !el);

array;  // (8) [1, 'a', empty, undefined, null, NaN, true, false]
filter1;   // (3) [1, 'a', true]
filter2;   // (4) [undefined, null, NaN, false]
filter3;   // (7) [1, 'a', undefined, null, NaN, true, false]

let counter = 0;
const filter4 = array.filter(el => {counter++; return el || !el});
counter;    // 7
filter4;    // (7) [1, 'a', undefined, null, NaN, true, false]
See that the function is not called for empty cell either. Counter is also only 7. Also in the result array, at most we get 7 elements.
When to not use map()?
When we are not going to use the new array
Example of good use:
  • When the new array is needed as an input for other function/request with certain value/format.
  • When array is to be sent out to a third party and they don't need / should not see the complete information.
What actually happen when map() is called
  • Very similar to filter() except it creates new array of new values according to callback function.
  • When map is called, it sees the cells that are there. Only to those cells, the callback function will be called.
    • New future cells created during the callback function run will be ignored
    • If content is shifted or changed, then whatever new value populate the cell will be used.
  • It doesn't call the callback function when the cell is empty or not existing. But even when the callback function is not called, the cell with the same index is already assigned in the new array. They will be left empty as well.
    • Cells that are empty because the value is never assigned. This cells marked as empty in the array.
      • Remember, undefined is an assigned value.
      • As well as null and NaN. Well, NaN's type is actually number
    • Cells that are empty upon deletion (like with delete operator).
    • Cells that are removed after map() is called, for example by pop() method from callback function.

    So these empty cells are mapped empty too in the new array.
  • For each cell, callback function is called. Current cell value of the original array is used. This could be different from original array value when map() is initiated, if previous cell run changed the value of the consequent one.Callback function results in a value. This value is added to the new array cell with the same index.
  • New array has the same length as the original array. Each cell value is transformed from the original cell value of the same index. Empty stays empty.
  • In the end, we have 2 array. Original and map result.

Example: Simple map

Map numbers to their square roots
Given: array of numbers
const numberArray = [1, 2, 3, 4, 5];
Map to the square root
const rootArray = numberArray.map(num => Math.sqrt(num));
Results
numberArray;    // (5) [1, 2, 3, 4, 5]
rootArray;      // (5) [1, 1.4142135623730951, 1.7320508075688772, 2, 2.23606797749979]

Example: Reformat array

Common map use is to create array with a certain format. In this example, remove the property names.
Given: an object array
const keyData = [
    { key: 1, value: 10 },
    { key: 2, value: 20 },
    { key: 3, value: 30 }
];
Map to plain object array without property names
const keyReady = keyData.map(({ key, value }) => ({ [key]: value }));
Results
keyReady;
/* [{ 1: 10 },
    { 2: 20 },
    { 3: 30 }]; */

// keyData remains unchanged

Mapping non-array collections

With Array.prototype.map.call()

Mapping for non-array collections
map() is an array prototype method and it works only for array. However it can be adapted to array like object like a string or a nodelist by calling the Array.prototype.map().
Non-array collection examples:
  • string: a collection of characters
  • nodelist: a collection of nodes, usually returned by properties such as Node.childNodes or methods such as document.querySelectorAll()
Syntax
Array.prototype.map.call(theCollection, elFunction);
Array.prototype.map.call(theCollection, el => ...el...);
Breakdown
call() syntax
function elFunction(arg1-n)
elFunction.call(theObject, arg1-n)
Parameters of call():
  • First: theObject aka thisArg. The object to which the function is being called. Keyword this inside the function will refer to this object.
  • Rest (optional): arg1-n. Collection of parameters required by original function.
For example:
function Person(name, age, location) {...}
Person.call(dad, name, age, location)
map() syntax for array
theArray.map(elFunction)
theArray.map(el => ...el...)
Required parameter: elFunction
  • Function for each element.
  • Often written as an anonymous arrow function when short.
Return: []
  • Return from each of the elFunction call to each element, assembled into an array.
The prototype:
Array.prototype.map(elFunction)
Array.prototype.map.call()
map() adapted for non-array collection is modified from its prototype form. By as simple as calling it with call()
Array.prototype.map.call(theCollection, elFunction);
The parameters are the calling parameters:
  • First parameter is theCollection
  • The rest of parameters are the mapping parameters:
    • First and mandatory is elFunction
    • Rest are optional
Map prototype as a function constant
It can also be broken down into 2 lines. Unnecessary though, I think.
const collectionMap = Array.prototype.map;
collectionMap.call(theCollection, elFunction);

Array.from()

Creating array from another array or any collection

Array.from()
There's actually an already built-in method for creating an array, from any collection, whether it is an array or a non-array collection.
Array.from(theCollection);
Array.from(theCollection, elFunction);
Array.from(theCollection, el => ...el...);
Syntax is similar, but element function can be omitted. Include element function when there's element manipulation.

String mapping

With Array.prototype.map.call()

String mapping
String is a collection of characters.
Task
Create array from string. With and without character manipulation.
Using Array.from()
Code:
plainArray    = Array.from("Hello Kitty!");
copyArray     = Array.from("Hello Kitty!", char => char);
capitalArray  = Array.from("Hello Kitty!", char => char.toUpperCase());
codeArray     = Array.from("Hello Kitty!", char => char.charCodeAt(0));
Using Array.prototype.map.call()
Code:
copyArray     = Array.prototype.map.call("Hello Kitty!", char => char);
capitalArray  = Array.prototype.map.call("Hello Kitty!", char => char.toUpperCase());
codeArray     = Array.prototype.map.call("Hello Kitty!", char => char.charCodeAt(0));
Results are the same when element function is given
plainArray; 
// (12) ['H', 'e', 'l', 'l', 'o', ' ', 'K', 'i', 't', 't', 'y', '!']

copyArray; 
// (12) ['H', 'e', 'l', 'l', 'o', ' ', 'K', 'i', 't', 't', 'y', '!']

capitalArray; 
// (12) ['H', 'E', 'L', 'L', 'O', ' ', 'K', 'I', 'T', 'T', 'Y', '!']

codeArray; 
// (12) [72, 101, 108, 108, 111, 32, 75, 105, 116, 116, 121, 33]
Note: Calling map must have element function. Without it, it will be a typeError
  • plainArray = Array.prototype.map.call("Hello Kitty!"); // typeError

NodeList mapping

With Array.prototype.map.call()

NodeList mapping
NodeList is a collection of nodes.
NodeList is depicted in array format. Superficially it looks like an array, but it's not actually an array. Check the prototypes it has. It's limited, not that many, while array's prototype are many. One of the array prototype a nodelist doesn't have is mapping.
Task
  • Create array of NodeList of article elements.
  • Create array of just the ids.
Using Array.from()
const theNodeList = document.querySelectorAll("article");
const nodeArray = Array.from(theNodeList);
const idArray = Array.from(theNodeList, el => el.id);
Using Array.prototype.map()
const theNodeList = document.querySelectorAll("article");
const nodeArray = Array.prototype.map.call(theNodeList, el => el);
const idArray = Array.prototype.map.call(theNodeList, el => el.id);
Results are the same:
theNodeList; 
// NodeList(7) [article#map-method, article#simpel-map, article#reformat-array, article#array.prototype.map.call, article#string-mapping, article#nodelist-mapping, article#references]

nodeArray; 
// (7) [article#map-method, article#simpel-map, article#reformat-array, article#array.prototype.map.call, article#string-mapping, article#nodelist-mapping, article#references]

idArray; 
// (7) ['map-method', 'simpel-map', 'reformat-array', 'array.prototype.map.call', 'string-mapping', 'nodelist-mapping', 'references']

Tricky mapping example: parseInt

parseInt()
parseInt(numStr, radix)
2 parameters:
  • numStr: The number in the radix system
  • radix (optional): The radix/base
Return:integer-radix10 value or NaN (type number)
Read more: CodeyLuwak: parseInt()
Task
Map an array of integer strings into integer numbers
Using parseInt() with caution
parseInt() is one of common way used to change data type of number strings into integer numbers
  • parseInt("5"); // 5
parseInt() can take 1 or 2 arguments. The first and mandatory argument is the number value. In map(), such values are the elements of an array, written at front.
  • parseInt(el)
  • [el1, el2, el3].map()
It's tempting to write it as
  • [el1, el2, el3].map(parseInt)
Give the array real values:
  • ["1", "2", "3"].map(parseInt)
We are expecting [1, 2, 3] as result, but we will get [1, NaN, NaN] instead.
The problem is, parseInt(), while often only written with 1 argument, actually takes 2. The second one being the radix to the callback function. Now going back to the map(), it takes 3 arguments: the element, the index, and the array. The first argument is already in tune. parseInt will ignore the third argument. But it will take the second argument, the index, as the radix. The code above is read as:
  • ["1", "2", "3"].map(parseInt(el,index))
    • Element 1: parseInt("1", 0); // 1 because radix 0 operates as if 10.
    • Element 2: parseInt("2", 1); // NaN because radix can't be 1.
    • Element 3: parseInt("3", 2); // NaN because 3 isn ot a valid number for radix 2.
Solution? Rather than calling function parseInt directly, use it as return for map's callback function.
  • ["1", "2", "3"].map(el => parseInt(el));
    // (3) [1, 2, 3]
  • ["1", "2", "3"].map(el => parseInt(el, 10));
    // (3) [1, 2, 3]
  • ["1.1", "2.2", "3.3"].map(el => parseInt(el));
    // (3) [1, 2, 3]
If we want floats and exponential notation stay the same, we can use Number(value)
  • ['1', '2', '3'].map(Number);
    // (3) [1.1, 2.2, 3.3]
  • ["1.1", "2.2", "3.3"].map(Number);
    // (3) [1.1, 2.2, 3.3]

Wesbos Javascript30: 04 Array Cardio Practice

Task: 2. Give us an array of the inventors first and last names
Given
const inventors = [
    { first: 'Albert', last: 'Einstein', year: 1879, passed: 1955 },
    { first: 'Isaac', last: 'Newton', year: 1643, passed: 1727 },
    { first: 'Galileo', last: 'Galilei', year: 1564, passed: 1642 },
    { first: 'Marie', last: 'Curie', year: 1867, passed: 1934 },
    { first: 'Johannes', last: 'Kepler', year: 1571, passed: 1630 },
    { first: 'Nicolaus', last: 'Copernicus', year: 1473, passed: 1543 },
    { first: 'Max', last: 'Planck', year: 1858, passed: 1947 },
    { first: 'Katherine', last: 'Blodgett', year: 1898, passed: 1979 },
    { first: 'Ada', last: 'Lovelace', year: 1815, passed: 1852 },
    { first: 'Sarah E.', last: 'Goode', year: 1855, passed: 1905 },
    { first: 'Lise', last: 'Meitner', year: 1878, passed: 1968 },
    { first: 'Hanna', last: 'Hammarström', year: 1829, passed: 1909 }
];
Solution
const invName = inventors.map(inv => `${inv.first} ${inv.last}`);
Result
invName;

// (12) ['Albert Einstein', 'Isaac Newton', 'Galileo Galilei', 'Marie Curie', 'Johannes Kepler', 'Nicolaus Copernicus', 'Max Planck', 'Katherine Blodgett', 'Ada Lovelace', 'Sarah E. Goode', 'Lise Meitner', 'Hanna Hammarström']

References