Reduce an array of objects to the unique strings found in a property shared by all objects (javascript)

Viewed 408

I've written a javascript function that successfully returns an array of all the unique classes used in a HTML document.

const elements = document.getElementsByTagName('*');

let classesUsedInHtml = htmlCollection => {
  let uniqClassSelectors = [];
  Array.from(htmlCollection)
    .filter(element => element.classList.length > 0)
    .map(element =>
      element.classList.forEach(
        item =>
          uniqClassSelectors.includes(item)
            ? null
            : uniqClassSelectors.push(item)
      )
    );
  return uniqClassSelectors;
};

console.log(classesUsedInHtml(elements));

I'd like to refactor this function using Array.prototype.reduce(), and skip the need to define the variable uniqClassSelectors, but haven't been able to make it work.

Here is my refactored function that does not work (returns accumulator is undefined). I often get tripped up with reduce() in this regard. Why is accumulator undefined? Am I not using forEach() correctly inside the reduce() function? Can anyone point me in the right direction here? Thanks!

let classesUsedInHtml = htmlCollection => {
  return Array.from(htmlCollection)
    .filter(element => element.classList.length > 0)
    .reduce((accumulator, element) => {
      return element.classList.forEach(
        classSelector =>
          accumulator.includes(classSelector)
            ? accumulator
            : accumulator.concat(classSelector)
      );
    }, []);
};

EDIT: removed the uneccessary map() from the function in question due to Nina Scholz comment

2 Answers

I suggest to use another inner accumulator for the final domTokenList and take the outer accumulator as start value for the inner accumulator acc2.

let classesUsedInHtml = htmlCollection =>
    Array
        .from(htmlCollection)
        .filter(element => element.classList.length)
        .map(element => element.classList)
        .reduce((accumulator, domTokenList) =>
            domTokenList.reduce((acc2, classSelector) =>
                acc2.concat(acc2.includes(classSelector) ? [] : classSelector),
                accumulator
            ),
            []
        );

An other solution could be the use of Set for collecting unique class selectors.

let classesUsedInHtml = htmlCollection =>
    [...Array
        .from(htmlCollection)
        .filter(element => element.classList.length)
        .map(element => element.classList)
        .reduce((acc1, domTokenList) =>
            domTokenList.reduce((acc2, classSelector) => acc2.add(classSelector), acc1),
            new Set)
    ];

A step by step guide of this solution. The comment (// ->) shows the result of this step, which is passed to the next step.

  1. Convert the htmlCollection to an array using the spread syntax:

    [...htmlCollection] // -> [htmlElement, htmlElement, etc...]
    
  2. Array#map each element to an array of classes (spread again):

    [htmlElement, htmlElement, etc...].map(({ classList }) => [...classList])) // -> [[class, class, etc...], [class, class, etc...], etc...]
    
  3. Flatten the sub arrays by spreading into Array#concat:

    [].concat(...[[class, class, etc...], [class, class, etc...]], etc...) // -> [class, class, class, etc...]
    
  4. Convert the array to a Set, and spread back to an array, to get the unique class names:

    [...new Set([class, class, class, etc...])] // -> [class1, class2, class3, etc...]
    

const elements = document.getElementsByTagName('*');

const classesUsedInHtml = (htmlCollection) =>
    [...new Set( // get unique items and convert to array
      [].concat(... // concat all classes' arrays
        [...htmlCollection] // convert the collection to an array
        .map(({ classList }) => [...classList])) // get arrays of classes
      )
    ];
    
const result = classesUsedInHtml(elements);

console.log(result);
<div class="a c">
  <span class="a b"></span>
  <span class="b c"></span>
</div>

Related