loading...

Mastering Hard Parts of JavaScript: Callbacks III

internettradie profile image Ryan Ameri Updated on ・5 min read

Exercise 12

Create a function majority that accepts an array and a callback. The callback will return either true or false. majority will iterate through the array and perform the callback on each element until it can be determined if the majority of the return values from the callback are true. If the number of true returns is equal to the number of false returns, majority should return false.

const isOdd = function (num) {
  return num % 2 === 1;
};
console.log(majority([1, 2, 3, 4, 5, 7, 9, 11], isOdd));

should log true

console.log(majority([2, 3, 4, 5], isOdd));

should log false.

Solution 12

function majority(array, callback) {
  let trueCount = 0;
  let falseCount = 0;
  array.forEach((item) => {
    callback(item) ? trueCount++ : falseCount++;
  });
  return trueCount > falseCount ? true : false;
}

I found this exercice to be on the easy side, as long as you use two variables and initialise them to zero. Also demonstrating the use of the terneray operator, which I find helps in readablity of simple if...else... blocks.

Exercise 13

Create a function prioritize that accepts an array and a callback. The callback will return either true or false. prioritize will iterate through the array and perform the callback on each element, and return a new array, where all the elements that yielded a return value of true come first in the array, and the rest of the elements come second.

const startsWithS = function (str) {
  return str[0] === "s" || str[0] === "S";
};
console.log(
  prioritize(
    ["curb", "rickandmorty", "seinfeld", "sunny", "friends"],
    startsWithS
  )
);

Should log ['sunny', 'seinfeld', 'curb', 'rickandmorty', 'friends']

Solution 13

function prioritize(array, callback) {
  return array.reduce((accum, item) => {
    callback(item) ? accum.unshift(item) : accum.push(item);
    return accum;
  }, []);
}

This is actually very similar to the previous exercise, except now instead of having two variables, we have two arrays, a truthy array and a falsy array. Since the example expects a single array to be returned, we need to concat these two arrays. I decided to save a bit of code and memory and just use a single array, and use two different array methods, unshift() and push() to put truthy and falsy values at the two ends of the array. Also notice that we are again using reduce, but providing an empty array as the accumulator of our reduce.

Edit: For an alternative solution that preserves the order of items, have a look at Khushbu's comment in the discussion below.

Exercice 14

Create a function countBy that accepts an array and a callback, and returns an object. countBy will iterate through the array and perform the callback on each element. Each return value from the callback will be saved as a key on the object. The value associated with each key will be the number of times that particular return value was returned.

console.log(
  countBy([1, 2, 3, 4, 5], function (num) {
    if (num % 2 === 0) return "even";
    else return "odd";
  })
);

should log { odd: 3, even: 2 }

Solution 14

function countBy(array, callback) {
  return array.reduce((obj, item) => {
    let result = callback(item);
    obj[result] ? (obj[result] = obj[result] + 1) : (obj[result] = 1);
    return obj;
  }, Object.create(null));
}

By now we should be familiar with this pattern. We are taking in an array and returning a single object, so we're looking for reduce! We provide the new object as the accumulator to reduce, run the callback on the items in the array, and based on its return value, set the value in the object accordingly.

The power of reduce should be apparent now.

Exercise 15

Create a function groupBy that accepts an array and a callback, and returns an object. groupBy will iterate through the array and perform the callback on each element. Each return value from the callback will be saved as a key on the object. The value associated with each key will be an array consisting of all the elements that resulted in that return value when passed into the callback.

const decimals = [1.3, 2.1, 2.4];
const floored = function (num) {
  return Math.floor(num);
};
console.log(groupBy(decimals, floored));

should log { 1: [1.3], 2: [2.1, 2.4] }

Solution 15

function groupBy(array, callback) {
  return array.reduce((obj, item, index, arr) => {
    let res = callback(item);
    obj[res] = arr.filter((element) => parseInt(element) === parseInt(res));
    return obj;
  }, Object.create(null));
}

The solution here requires knowing that reduce can take more than a callback and the item, it can also (optionally) take the index of the array and the whole array as parameters as well.

If this was being done by traditional for loops, you'd need two nested loops here. Using these Array methods, the first loop is replaced with array.reduce() and the second loop is replaced by arr.filter(). filter() takes a callback function and returns all the elements for which the callback returns true. Here filter returns an array, and we just assign that to be the value inside our newly created (accumulator) object.

It took me a while to get comfortable with this style of declarative programming and using Array methods. Once you do get comfortable with them though, you don't want to go back to for loops, with all the potential off-by-one errors they introduce. But sometimes a loop is just easier to read and implement, as we'll see in the next exercise.

Edit: Also have a look at Khushbu's solution below in the discussion, which is a faster way of solving the problem (albeit without using filter).

Exercise 16

Create a function goodKeys that accepts an object and a callback. The callback will return either true or false. goodKeys will iterate through the object and perform the callback on each value. goodKeys will then return an array consisting only the keys whose associated values yielded a true return value from the callback.

const sunny = {
  mac: "priest",
  dennis: "calculating",
  charlie: "birdlaw",
  dee: "bird",
  frank: "warthog",
};
const startsWithBird = function (str) {
  return str.slice(0, 4).toLowerCase() === "bird";
};
console.log(goodKeys(sunny, startsWithBird));

should log ['charlie', 'dee']

Solution 16

function goodKeys(obj, callback) {
  const arr = [];
  for (let [key, value] of Object.entries(obj)) {
    if (callback(value)) arr.push(key);
  }
  return arr;
}

In this exercise, I really struggled to use Array methods instead. The callback function ended up looking rather ugly, and it really felt like it wasn't the right tool for the job. There is no point being dogmatic, if a loop is easier to read and does the job well enough, we don't need to avoid it! Especially now that we can easily turn objects into iterables with Object.entries() and then use destructuring and the for...of loop to easily iterate through it.

If you have a neat way of using an Array method instead of a loop to solve this, please let me know!

Posted on by:

internettradie profile

Ryan Ameri

@internettradie

Re-discovering frontend development. Professional translator & interpreter. Amateur powerlifter. BLM. He/him/his

Discussion

pic
Editor guide
 

For Challenge 13, as given in the original question, if we were to maintain order,
Your solution will log :

['sunny', 'seinfeld', 'curb', 'rickandmorty', 'friends']

But the expected output is

['seinfeld', 'sunny', 'curb', 'rickandmorty', 'friends']

So to maintain the order following solution might work,

function prioritize(array, callback) {
    let arr = [], sArr = [];
        array.forEach((item, index) => {
         if(callback(item)){
            sArr.push(item);
         } else {
           arr.push(item);
         }
     });
  return [...sArr, ...arr];
}

For challenge 15, it's good that you have shown more use-case of the reduce method.

But it is not an optimal solution as using 2 loops make the time complexity O(N*N).
This problem can be done using a single loop too i.e. O(N).

function groupBy(array, callback) {
    return array.reduce((a,c) => {
          let key = callback(c);
            if(a.hasOwnProperty(key)){
              a[key] = [...a[key], c];
           }else{
             a[key] = [c];
          }
        return a
    }, {})
}

Finding in an object is much efficient than finding it in an array.

 

Thank you Khushbu for going through the challenges and providing improved solutions.

For challenge 13, as you noted, I slightly modified the original challenge as I thought it would be neater if all truthy values come first. I'll add your solution as an alternative.

For challenge 15, your solution is indeed much more performant. My goal was to use declarative array methods as much as possible. I'll include your solution as a faster alternative.

Thanks again for the feedback! 😁

 

for the Solution to Exercise 12, we can have
return trueCount > falseCount
instead of
return trueCount > falseCount ? true : false;
in the funciton majority