!DOCTYPE html> filter() | JS CodeyLuwak

JavaScript: Array.filter()

Array.filter()

filter()
The filter() method creates a new array with all elements that pass the filter test function.
filter(callbackFn)
Syntax
// Callback function
filter(callbackFn)
filter(callbackFn, thisArg)


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

// Arrow function
filter((element) => {...})
filter((element, index) => {...})
filter((element, index, array) => {...})
Parameters (2):
  • Callback function
  • thisArg (optional) : Value to use as this when running callback function. undefined is assigned if not provided.
Return value:
New array with elements that pass the test. If no elements pass the test, an empty array is returned.
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 boolean;
}
// Element is pushed to the new array if return is true.
Parameters (3):
  • element : Current element being process
  • index (optional): index of current element being process
  • array (optional): The original array
Return values: boolean
  • true : include element
  • false : exclude element

Example: Simple filter

Find numbers above 50
Given: array with numbers 1-100
const numbers = [5, 14, 85, 52, 70, 25];
Using callback function
function isBig (value) {
    return value > 50;
}
const largeNumbers = numbers.filter(isBig);
Using inline callback function
const largeNumbers = numbers.filter(function (value) {return value > 50});
Using arrow function
const largeNumbers = numbers.filter(value => value > 50);
All resulting in:
// largeNumbers = [85, 52, 70]
// numbers = [5, 14, 85, 52, 70, 25]; (original array not changed)
Find all prime numbers in an array
Given: array with numbers -3 to 13
const numbers = [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
Using callback function
function isPrime (num) {
    for (let i = 2; num > i; i++) {
        if (num % i == 0) {
            return false;
        }
    }
    return num > 1;
}
const primeNum = numbers.filter(isPrime);
Result:
// primeNum = [2, 3, 5, 7, 11, 13]

Example: Removing invalid entries in JSON

Filtering out and counting invalid entries from JSON
Given: id array
const array = [
    { id: 15 },
    { id: -1 },
    { id: 0 },
    { id: 3 },
    { id: 12.2 },
    { },
    { id: null },
    { id: NaN },
    { id: 'undefined' }  
];
Using callback function
let invalidEntries = 0;
function isValid (item) {
    if (Number.isFinite(item.id) && item.id !== 0) {
        return true;
    }
    invalidEntries += 1;
    return false;
}
const validArray = array.filter(isValid);
Result:
// validArray = [{ id: 15 }, { id: -1 }, { id: 3 }, { id: 12.2 }]
// invalidEntries = 5

Parameter: array and index

Modifying, appending, and deleting elements of original array while filtering

Parameters array and index of callback function
Used to access and modify the original array and elements while filtering.
// Callback function
filter(callbackFn)
function callbackFn(element, index, array) {...}

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

// Arrow function
filter((element, index, array) => {...})
Some examples. These codes can be added to the body of the callback function.
  • Appending new elements: array.push(newEl);
  • Deleting elements: array.pop();
  • Modifying current elements: array[index] += string
  • Modifying next elements: array[index+1] += string
Important: In using this note, remember to always re-entering the original array. Since we are playing with modification of original array.
Modifying element while filtering: toLowerCase
Given: array of animals
const animals = ['VELOCIRAPTOR', 'DODO', 'GREAT AUK', 'OX', 'THYLACINE', 'DOG'];
Task:
  • Filter short name animals (max 6 characters).
  • Lowercase the content as we go.
const filterLC = animals.filter((animal, idx, arr) => {
    arr[idx] = arr[idx].toLowerCase();
    return animal.length <= 6;
});

// Also works: arr[idx] = animal.toLowerCase();
// Won't work: animal = animal.toLowerCase();
Results:
filterLC; // (3) ['DODO', 'OX', 'DOG']
animals;  // (6) ['velociraptor', 'dodo', 'great auk', 'ox', 'thylacine', 'dog']
Note: For each test to the array's cell, the array's cell value is recorded first to be passed to the new array in the case test returns true. This happen even before the codeblock is run. Modification didn't affect this value.
Modifying element while filtering: add text
Given: array of animals
const animals = ['velociraptor', 'dodo', 'great auk', 'ox', 'thylacine', 'dog'];
Task:
  • Filter short name animals (max 6 characters).
  • Add "My " in the beginning of each element.
const filterMy = animals.filter((animal, idx, arr) => {
    arr[idx] = "My " + animal;
    return animal.length <= 6;
});
Results:
filterMy; // (3) ['dodo', 'ox', 'dog']
animals;  // (6) ['My velociraptor', 'My dodo', 'My great auk', 'My ox', 'My thylacine', 'My dog']
Note: Even though the value spitted out to the new array is taken in the beginning of each cell test, the value itself can be processed and taken account into the filter condition.
const filterMy2 = animals.filter((animal, idx, arr) => {
    arr[idx] = "My " + animal;
    return arr[idx].length <= 6;
});
Results:
filterMy2; // (3) ['ox', 'dog']
animals;   // (6) ['My velociraptor', 'My dodo', 'My great auk', 'My ox', 'My thylacine', 'My dog']
// 'dodo' is eliminated because 'My dodo' length is over 6.
Modifying "the next" element while filtering: add text
Given: array of animals
const animals = ['velociraptor', 'dodo', 'great auk', 'ox', 'thylacine', 'dog'];
Task:
  • Filter short name animals (max 6 characters).
  • Add "My " in the beginning of "next" element.
const filterNext = animals.filter((animal, idx, arr) => {
    arr[idx+1] = "My " + arr[idx+1];
    return animal.length <= 6;
});
Results:
filterNext; // (2) ['My ox', 'My dog']
animals;  // (7) ['velociraptor', 'My dodo', 'My great auk', 'My ox', 'My thylacine', 'My dog', 'My undefined']
Note:
  • Remember: Cell initial value is taken and this value is to be included in the new result array in the case test is positive.
  • So for the first cell, the value will be the original value.
  • However the value of second cell is changed during the first cell test run. Therefore, by the time the second cell test is started, the cell value has changed. Therefore this new value is what going to be included in the new array in the case test result is true.
  • Filter function counting the length of array and/or creating the pointer to each cell to later be tested in the beginning of the run. New cells created after that point will not even get tested.
const filterNext2 = animals.filter((animal, idx, arr) => {
    arr[idx+1] = "My " + arr[idx+1];
    return animal.length > 6;
});
Results:
filterNext2; // (4) ['velociraptor', 'My dodo', 'My great auk', 'My thylacine']
animals;  // (7) ['velociraptor', 'My dodo', 'My great auk', 'My ox', 'My thylacine', 'My dog', 'My undefined']
See that the newly generated element cell doesn't get tested.
Appending elements
Given: array of animals
const animals = ['velociraptor', 'dodo', 'great auk', 'ox', 'thylacine', 'dog'];
Task:
  • Filter short name animals (max 6 characters).
  • Add new elements as we go
const filterAppend = animals.filter((animal, idx, arr) => {
    arr.push("new");
    return animal.length <= 6;
});
Results:
filterAppend; // (3) ['dodo', 'ox', 'dog']
animals;  // (12) ['velociraptor', 'dodo', 'great auk', 'ox', 'thylacine', 'dog', 'new', 'new', 'new', 'new', 'new', 'new']
Note: Newly created element cells don't get tested.
Appending elements in between
Given: array of animals
const animals = ['velociraptor', 'dodo', 'great auk', 'ox', 'thylacine', 'dog'];
Task:
  • Filter short name animals (max 6 characters).
  • Add new elements "in next cell" as we go
const filterAppend = animals.filter((animal, idx, arr) => {
    arr.splice(idx + 1, 0, "new");
    return animal.length <= 6;
});
Results:
filterAppend; // (5) ['new', 'new', 'new', 'new', 'new']
animals;      // (12) ['velociraptor', 'new', 'new', 'new', 'new', 'new', 'new', 'dodo', 'great auk', 'ox', 'thylacine', 'dog']
Note: See the originally existing cells get tested. That it's "new", only in our mind it is new element. What actually happens in array is, the new one is the cell. Contents are merely assigned, reassigned, or deleted.
Let's see similar but asking for more than 6 characters
const filterAppend = animals.filter((animal, idx, arr) => {
    arr.splice(idx + 1, 0, "new");
    return animal.length > 6;
});
Results:
filterAppend; // ['velociraptor']
animals;      // (12) ['velociraptor', 'new', 'new', 'new', 'new', 'new', 'new', 'dodo', 'great auk', 'ox', 'thylacine', 'dog']
Removing elements
Given: array of animals
const animals = ['velociraptor', 'dodo', 'great auk', 'ox', 'thylacine', 'dog'];
Task:
  • Filter short name animals (max 6 characters).
  • Remove elements as we go
const filterRemove = animals.filter((animal, idx, arr) => {
    arr.pop();
    return animal.length <= 6;
});
Results:
filterRemove; // ['dodo']
animals;      // (3) ['velociraptor', 'dodo', 'great auk']
It never reached ox, let alone dog. Because by the time the test of cell 3 is done, cell 4, 5, and 6 are already gone.
Deleting next element as filter go
Given: array of animals
const animals = ['velociraptor', 'dodo', 'great auk', 'ox', 'dog', 'thylacine'];
Task:
  • Filter short name animals (max 6 characters).
  • Delete next elements as we go. Deletion works differently from popping. Deletion only delete the content, it doesn't remove the cell. When a test reaching an empty cell, it skipped it entirely. Not merely failed, the code is not run at all.
const filterDel = animals.filter((animal, idx, arr) => {
    delete arr[idx + 1];
    return animal.length <= 6;
});
Results:
filterDel; // ['dog']
animals;      // (6) ['velociraptor', empty, 'great auk', empty, 'dog', empty]
Cell 2, 4, and 6 is not even run. Evident is, "greak auk" and "dog" didn't get erased. The whole test for those cells are skipped.

Empty vs undefined

empty vs undefined
Even though value undefined is assigned to an empty cell, they are essentially different and filter() method behave differently to them.
Examples of empty element:
  • cell never filled
  • cell content deleted by delete operator
Examples of undefined value:
  • cell value listed as undefined
When they are the same:
array = ["a", undefined, , "b", "c", "d"];
array;              // (6) ['a', undefined, empty, 'b', 'c', 'd']

delete array[4];    // true
array;              // (6) ['a', undefined, empty, 'b', empty, 'd']

array[1];           // undefined
array[2];           // undefined
array[4];           // undefined

"Hi " + array[1];       // 'Hi undefined'
"Hi " + array[2];       // 'Hi undefined'
"Hi " + array[4];       // 'Hi undefined'

array[1] === array[2];   // true
array[1] !== array[2];   // false

array[1] === array[4];   // true
array[1] !== array[4];   // false
Filter test function is called for undefined. It skips empty.
empty
Callback function skips empty entirely. The function is not even called. Test is not initiated.
undefined
Callback function runs for undefined values. Test is conducted. And of course return value can even be true and element is included in the new result array.
array;  // (6) ['a', undefined, empty, 'b', empty, 'd']
let counter = 0;
const validArray = array.filter(el => {
    counter ++;
    return el;
});

counter;    // 4
validArray; // (3) ['a', 'b', 'd']
array;  // (6) ['a', undefined, empty, 'b', empty, 'd']
let counter = 0;
const undefArray = array.filter(el => {
    counter ++;
    return el === undefined;
});

counter;            // 4
undefArray;         // [undefined]
undefArray.length;  // 1
Don't get confused with return value formulation and code return undefined instead.
array;  // (6) ['a', undefined, empty, 'b', empty, 'd']
let counter = 0;
const undefArray2 = array.filter(el => {
    counter ++;
    return undefined;
});

counter;            // 4
undefArray2;        // []
undefArray2.length; // 0

NaN and null

NaN
NaN is not assigned as undefined the same way empty does.
Value of a NaN is NaN-ish, but not really NaN
array = [1, 2, NaN, 3, undefined];
array[2];           // NaN
array[2] + "cy";    // "NaNcy"
array[2] === NaN;   // false
array[2] == NaN;    // false
array[2] === "NaN"; // false
array[2] == "NaN";  // false
Filter callback test function will run on it just like in the case of undefined
But the element can't pass el => el === NaN.
array = [1, 2, NaN, 3, undefined];
let counter = 0;
const nanArray = array.filter(el => {
    counter ++;
    return el === NaN;
});

counter;            // 5
nanArray;           // []
nanArray.length;    // 0
How to catch this NaN then? Using !el. It will return el that is not el
array = [1, 2, NaN, 3, undefined];
let counter = 0;
const nanArray = array.filter(el => {
    counter ++;
    return !el;
});

counter;            // 5
nanArray;           // (2) [NaN, undefined]
nanArray.length;    // 2
Let's be silly. We just want NaN, but don't want undefined.
array = [1, 2, NaN, 3, undefined];
let counter = 0;
const nanOnlyArray = array.filter(el => {
    counter ++;
    return !el && (el !== undefined);
});

counter;            // 5
nanArray;           // [NaN]
nanArray.length;    // 1
Yay!
Well, actually typeof a NaN is "number". It can be filtered together with other numbers
array = [1, 2, NaN, 3, undefined, , true, false, true];
let counter = 0;
const numArray = array.filter(el => {
    counter ++;
    return typeof el === "number";
});

counter;            // 8
numArray;           // (4) [1, 2, NaN, 3]
array = [1, 2, NaN, 3, undefined, , true, false, true];
let counter = 0;
const notNumArray = array.filter(el => {
    counter ++;
    return typeof el !== "number";
});

counter;            // 8
notNumArray;        // (4) [undefined, true, false, true]
Not going to talk about filtering boolean because they are regular normal valid exist data.
null
Might as well checking out null
typeof undefined;   // "undefined"
typeof empty;       // "undefined"
typeof NaN;         // "number"
typeof null;        // "object"
In filter, it can be caught by typeof el === "object"
array = [1, 2, NaN, 3, null, undefined, , null];
let counter = 0;
const nullArray = array.filter(el => {
    counter ++;
    return typeof el === "object";
});

counter;        // 7
nullArray;      // (2) [null, null]
Of course it will include other objects. But let's learn about these special values some other time.

How filter works

What and when actually happen during filtration
  1. When filter method is run, array and the length are noticed. Individual pointer to each cell are taken. Let's call it "test cells"
  2. Testing will be conducted to those test cells, regardless any addition, deletion, and shifting of the array. If the initial length is 6, then cell number 0-5 will be tested. At this point though, only cell address is taken. not the value.
  3. When the test is run to a cell, the initial value of the cell is taken. This is the value that will be pushed to the new result array. Modification can be done however we like to the array, but the initial value of this cell has been checked out and to be used if the test returns true.
  4. When test moved on to the next cell, it records the current element value. It depends on the manipulation in previous test runs. The content might have been modified, a new element might have replaced the initial, or it could have been gone by deletion. If the cell is empty, then the test is just not run to that cell altoghether. Skipped. Not just the test criteria will be failed, but the codeblock is not even entered, any modification within will not be read.

If in the beginning there are n cells of array:

Only to those n number of cells, test going to be conducted.

  • For each cell test run, initial value is "saved" before the code is run.

  • In the case test is true, this saved initial value is pushed to the new array.

    Modification during the test run didn't affect this.

    However the modification affects next cells' initial value.


    Wesbos Javascript30: 04 Array Cardio Practice

    Task: 1. Filter the list of inventors for those who were born in the 1500's
    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 inv15 = inventors.filter(inv => inv.year >= 1500 && inv.year < 1600);
    
    Result
    inv15;
    
    // (2) [{…}, {…}]
        0: {first: 'Galileo', last: 'Galilei', year: 1564, passed: 1642}
        1: {first: 'Johannes', last: 'Kepler', year: 1571, passed: 1630}
    
    6. create a list of Boulevards in Paris that contain 'de' anywhere in the name
    Given: https://en.wikipedia.org/wiki/Category:Boulevards_in_Paris
    Copy the solution to the link's console.
    After inspect the elements, it's found that all the boulevard names are contained in:
    • Class mw-category (a unique category) - target with querySelector instead of querySelectorAll
    • As a text context of links elements (Pretty much no other links other than for boulevard list)
    Solution
    const container = document.querySelector(".mw-category");
    
    const collection = Array.from(container.querySelectorAll("a"));
    
    const boulevardDe = collection
                            .map(link => link.textContent)
                            .filter(boulevard => boulevard.includes("de"));
    
    Result
    boulevardDe;
    // (12) ["Boulevard de l'Amiral-Bruix", 'Boulevard des Capucines', 'Boulevard de la Chapelle', 'Boulevard de Clichy', "Boulevard de l'Hôpital", 'Boulevard des Italiens', 'Boulevard de la Madeleine', 'Boulevard de Magenta', 'Boulevard Marguerite-de-Rochechouart', 'Boulevard de Sébastopol', 'Boulevard de Strasbourg', 'Boulevard de la Zone']
    Check out other solution using map and filter

    References