Given an array (the first argument in the destroyer function) remove all elements from the initial array that are of the same value as these arguments in the function. Note: You have to use the arguments object
Algorithmic challenges such as the one above (from FreeCodeCamp) are fun ways to stretch your problem solving muscles. You can usually come up with a few solutions but what if you tried solving it in as many ways as you can?
I have always wanted to do that so when I found myself with some free time, I began dreaming of ways I could torture JavaScript into doing things which would be a sackable offence if they ever found their way into a production codebase.
The challenge
The problem itself is fairly straightforward. You have a function which takes multiple inputs (the target array plus one or more values) and returns an array which contains the target array minus the values entered as the other arguments. From this explanation we can deduce the following:
- The solution will involve array manipulation
- The solution has to be able to handle any number of arguments (via the arguments object)
Solution 1: Make it work
function destroyerForEach(arr, ...args) {
return arr.filter((el) => {
let passed = el;
args.forEach((num) => {
if (num === el) passed = null;
});
return passed !== null;
});
}
Whenever I am solving a problem, I create a working solution as fast as possible and then improve it afterwards. destroyerForEach
takes the more long-winded approach of filtering through the target array and then looping through the rest of the arguments for each item in the target array. It is not pretty but it works. You could improve your programming street cred with this one liner args.forEach(num => num === el ? passed = null: null)
in the .forEach
function.
Solution 2: Filter and find
function shouldItemBeDestroyed(targetElement, comparisonArr) {
return comparisonArr.find((el) => el === targetElement);
}
function destroyer(arr, ...args) {
return arr.filter((el) => el !== shouldItemBeDestroyed(el, args));
}
If the .forEach
and .filter
combination is not to your taste, you can reach for .find
. This solution has the added benefit of splitting the logic between different functions, thus improving the testability of the code. Again, you could unleash your inner one line maximalist with this:
const destroyer = (arr, ...args) =>
arr.filter((el) => el !== args.find((item) => item === el));
Solution 3: Short and simple
function destroyerIncludes(arr, ...args) {
return arr.filter((item) => !args.includes(item));
}
This solution gets to the crux of the matter without much ceremony. You will notice that .filter
has been a mainstay in each solution thus far. This is because it is perfectly suited to tasks such as this. An interesting thing to note is that .includes
returns a boolean whilst .filter
's testing function returns a value which coerces to either true
or false
. This is useful if you like to avoid implicit coercions. You can take this solution to new heights by indulging both your ES6 and one liner tendencies to create this beauty:
const destroyerIncludes = (arr, ...args) =>
arr.filter((item) => !args.includes(item));
Solution 4 & 5: Indexing
function destroyerIndexOf(arr, ...args) {
return arr.filter((item) => args.indexOf(item) < 0);
}
// OR
function destroyerLastIndexOf(arr, ...args) {
return arr.filter((item) => args.lastIndexOf(item) < 0);
}
We can continue keeping things simple by using array indexes to determine which values need purging. This solution only works if we use the spread syntax to turn the arguments object from an Array-like object into an array. We also need to perform this spread operation in the parameter declaration. Had we done it like this, for example:
function destroyerIndexOf(arr) {
const args = [...arguments];
// ... rest of the code goes here
}
destroyerIndexOf([1, 2, 3, 4], 2, 3);
// args would be [ [ 1, 2, 3, 4 ], 2, 3 ]
We would be including the target array in our array of elimination values.
Solution 6: Give the filter some
function shouldItemBeDestroyed(target, comparisonArr) {
return comparisonArr.some((el) => el === target);
}
function destroyerSome(arr, ...args) {
return arr.filter((el) => !shouldItemBeDestroyed(el, args));
}
A similar solution to the one using .find
, with the difference being .some
returns a boolean instead of a value.
Solution 7: #nofilter
function destroyerValuesIterator(arr, ...args) {
let finalArr = [];
const iterator = arr.values();
for (const value of iterator) {
if (!args.includes(value)) finalArr.push(value);
}
return finalArr;
}
What's that? No filtering?! Yes, it is possible to live without .filter
and we do that by relying on for..of
to handle the iteration. The .values
methods returns an Array Iterator object which contains the values for each index in the array.
Solution 8: ?!?!?!
function destroyerArrOwnProp(arr, ...args) {
args.forEach((item) => {
Object.defineProperties(Array, {
[item]: {
value: item,
writable: true,
configurable: true, // so we can use delete to clean up after ourselves
},
});
});
return arr.filter((item) => {
return !Array.hasOwnProperty(item);
});
I cannot think of a scenario where this is even an option but it is reassuring to know we can create such monstrosities should the mood strike. Here we are extending the built-in Array object so we can use the .hasOwnProperty
method later to weed out the repeated values. In this solution's defence, it sets the configurable
property to true
so we can cover our tracks by deleting the properties and pretending this never happened.
Solution 9: Splicin' it up
function destroyerSpliceAndFromAndForEach(arr, ...args) {
const copiedArr = Array.from(arr);
arr.forEach((item) => {
args.forEach((num) => {
if (num === item) {
const index = copiedArr.indexOf(item);
copiedArr.splice(index, 1);
}
});
});
return copiedArr;
}
Here we use Array.from
to create a shallow copy of the target array and then enlist the services of .splice
to hack away the repeat elements. We can safely perform surgery on copiedArr
because whilst it has the same values as arr
, they are different array objects, so we don't have to worry about any mutation.
Solution 10: Functional preparation
function destroyerFromMap(arr, ...args) {
const mapFn = (item) => ({ value: item, isSameVal: args.includes(item) });
const copiedArr = Array.from(arr, mapFn);
return copiedArr.filter((item) => !item.isSameVal).map((item) => item.value);
}
We're not done with .from
just yet. This method has two optional arguments, the first of which is a map function that is called on every element of the array being copied. We can take advantage of this to prepare our array during the copy process by creating an object and adding a property on it which checks if the item being filtered against the arguments.
Solution 11: Let's get reducin'
function destroyerReducerConcat(arr, ...args) {
return arr.reduce((seedArray, elementFromSourceArr) => {
if (!args.includes(elementFromSourceArr)) {
return seedArray.concat(elementFromSourceArr);
}
return seedArray;
}, []);
}
This is one of my favourite solutions because it taught me a new way of using the .reduce
method. For a more in-depth and comprehensive explanation of the mechanics behind this solution, this article has you covered. With the .reduce
method, we can either provide a second argument or omit it, in which case it defaults to the first element of the array being worked on. In our case, we can "seed it" with an empty array and then populate that array with the passing elements. The other new method which makes its first appearance is .concat
and we use it to merge values into the seed array.
Solution 12: Let's get fancy with our reducin'
const destroyerReducerOneLinerSpread = (arr, ...args) =>
arr.reduce(
(seedArray, elementFromSourceArr) =>
!args.includes(elementFromSourceArr)
? (seedArray = [...seedArray, elementFromSourceArr])
: seedArray,
[]
);
As if solution 11 wasn't fancy enough, we can really flex our ES6 muscles by swapping .concat
for the spread operator and using a ternary to really drive home that we can write one-liners with the best of them.
const destroyerReducerOneLinerSpread = (arr, ...args) =>
arr.reduce(
(seedArray, elementFromSourceArr) =>
!args.includes(elementFromSourceArr)
? (seedArray = [...seedArray, elementFromSourceArr])
: seedArray,
[]
);
Solution 13: Settin' things up
function destroyerSet(arr, ...args) {
const argsSet = new Set(args);
let uniqueVals = [];
for (let i = 0; i < arr.length; i++) {
if (!argsSet.has(arr[i])) uniqueVals = [...uniqueVals, arr[i]];
}
return uniqueVals;
}
The final solution is another gratuitous use of a random JavaScript feature. Here we have cajoled a Set into storing our arguments and then used a for loop to iterate through the Set and find the unique values.
Top comments (0)