Inverse of [].filter in JS? - javascript

I realize that I can do:
arr = arr.filter(function(n){ return !filterFunc(n); });
But is there any way to just invert a filter without wrapping the filterer in an anon function?
It just seems cumbersome.

You can use an arrow function:
const a = someArr.filter(someFilter);
const a = someArr.filter(e => !someFilter(e));

Lodash provides a reject function that does the exact opposite of filter.
arr = _.reject(arr, filterFunc);

Take a look at lodash's negate function. It does exactly what #Yury Tarabanko mentions in his comment.
Usage:
arr = arr.filter(_.negate(filterFunc));

I wasn't happy with any of the answers directly, and actually wound up using newer JS features
arr.filter(() => ! filterfunc(...arguments));
This beats most of the others by not having to respecify the context (this) at any point by using an arrow function and passing all parameters accordingly using the spread syntax on the arguments object.
It's also rather succinct, though I would rather an invert flag on the filter function, or a separate function.
The question might be a little on the old side, but it's still relevant.

You can either add your own function or add a static/prototype methods to the Array object.
Code
Array Polyfill Methods
/**
* The not() method creates a new array with all elements that fail
* the test implemented by the provided function.
*
* Syntax
* arr.not(callback[, thisArg])
*
* #param callback
* Function to test each element of the array. Invoked with
* arguments (element, index, array). Return true to keep
* the element, false otherwise.
* #param thisArg
* Optional. Value to use as this when executing callback.
* #return Returns a new array containing all the items which fail
* the test.
*/
Array.prototype.not = function(callback) {
return this.filter(function () {
return !callback.apply(this, arguments);
});
};
/**
* Static method which calls Array.prototype.not on the array
* paramater.
*
* #see Array.prototype.not
*/
Array.not = function (array, callback) {
return array != null ? array.not(callback) : [];
};
Custom Function
function unfilter(array, callback) {
return array.filter(function () {
return !callback.apply(this, arguments);
});
}
This is safer to use than a polyfill, but it doesn't look as elegant in use.
unfilter(items, isFruit) vs items.not(isFruit)
Example
// ================================================================
// Polyfill
// ================================================================
Array.prototype.not = function(callback) {
return this.filter(function () {
return !callback.apply(this, arguments);
});
};
// ================================================================
// Main
// ================================================================
var items = [{
name: 'Apple',
isFruit: true
}, {
name: 'Carrot',
isFruit: false
}, {
name: 'Melon',
isFruit: true
}, {
name: 'Potato',
isFruit: false
}];
var isFruit = function(item, index) {
return item != null && item.isFruit;
};
var getName = function(item, index) {
return item != null ? item.name : '?';
};
document.body.innerHTML = items.not(isFruit).map(getName).join(', ');

filter returns elements which return true in your evaluation. If you want to inverse that, inverse your logic it within the function which tests each element.
Then, you could simply make this function work like so:
arr = arr.filter(filterFunc);

There is some ways to do that:
example:
const randomNumbers = [10, 22, 36, 52, 58];
let NewArray = [];
1. Store it in new array:
NewArray = randomNumbers.filter((n)=> ! (n> 22))
2. Using lodash as #Sascha Klatt mentioned above:
NewArray = _.reject(randomNumbers , ((n)=> ! (n> 22)));
3. Make a function like this:
function rejected(params) {
randomNumbers.forEach((val) => {
if (!params(val)) NewArray.push(val);
});
return NewArray;
}
4. or this function (Almost same as above)
function rejected2(params) {
randomNumbers.forEach((val) => {
if (params(val)) {
} else NewArray.push(val);
});
return NewArray;
}

If you really want to do this. You would have to write a helper function that returns a function that returns the inverted value of the normal output.
How you access this function is up to you. But I put in some examples.
Also I'm not saying it is better than writing a custom filter function for each scenario where you need it. You could even use the function you have ( you may or may not want this dependency ).
// APPROACH 1: CREATE A GENERIC HELPER FUNCTION
const arr = [1,2,"a","b"];
function is_a_string( val ) {
return typeof val === 'string';
}
function not( callback ) {
return ( ...arg ) => !callback( ...arg );
}
console.log( arr.filter( not( is_a_string ) ) );
// APPROACH 2: EXTEND A SPECIFIC FUNCTION ( hoisting issue )
const arr = [1,2,"a","b"];
function is_a_string( val ) {
return typeof val === 'string';
}
// note that hoisting is a thing here
is_a_string.not = ( ...args ) => !is_a_string(...args );
console.log( arr.filter( is_a_string.not ) );
// APPROACH 3: CREATE ANOTHER FUNCTION
const arr = [1,2,"a","b"];
function is_a_string( val ) {
return typeof val === 'string';
}
function is_not_a_string( val ) {
return !is_a_string( val );
// or just `return typeof val !== 'string'`; in a simple case like this
}
console.log( arr.filter( is_not_a_string ) );

Lets take an example
var cars = [{
carname: "indica",
brand: "Tata"
},
{
carname: "accord",
brand: "Toyota"
},
{
carname: "vento",
brand: "volkswagen"
},
{
carname: "polo",
brand: "volkswagen"
},
{
carname: "Manza",
brand: "Tata"
},
{
carname: "Agile",
brand: "Chevrolet"
},
];
var isTata = function(car) {
return car.brand === "Tata"
}
var fiteredCars = cars.filter(isTata); // retuns objects of brand Tata
console.log(fiteredCars)
in reverse of this just change your logic
var isNotTata = function(car) {
return car.brand !== "Tata"
}
var dogs = cars.filter(isNotTata); // returns objects of brand other than Tata

Related

Multiple If Else Statements, only first is run

I have code that requires multiple If Else statements but I'm not sure how to format it so that each runs:
let example = first;
let example2 = second;
let example3 = third;
if (example === something) {
return null;
} else {
return something;
}
if (example2 === somethingElse) {
return null;
} else {
return somethingElse;
}
if (example3 === somethingMore) {
return null;
} else {
return somethingMore;
}
But this doesn't work because of the multiple else statements, I was wondering if there was a way to do this? I also tried to put the data into an array or objects to iterate through but that won't work either.
Please help! :)
return will immediate return from first if, so store all result in object or array and return it as below
let example = 'first';
let example2 = 'second';
let example3 = 'third';
var return_data = {};
if (example === 'something') {
return_data.example = null;
} else {
return_data.example = something;
}
if (example2 === 'somethingElse') {
return_data.example2 = null;
} else {
return_data.example2 = 'somethingElse';
}
if (example3 === 'somethingMore') {
return_data.example3 = null;
} else {
return_data.example3 = 'somethingMore';
}
return return_data;
You have to remove the return in the if / else blocks - using return will immediately exit the function wherever it's encountered. The way your code is now, you are basically short-circuiting the function (which is not what you're trying to do):
It would probably make more sense to restructure your code to use a variable like this:
//Add a variable to keep store your desired output if you want to flow thru all if/else blocks
function getVal(example) {
let val;
if (example === 'something1') {
val = 'a'
} else {
val = 'b';
}
return val;
}
console.log(getVal('something1'));
console.log(getVal('lorem'));
I'm not completely clear on what you are asking, but I think you want to be using "else if" statements: https://ncoughlin.com/javascript-notes-conditional-statements-loops/#If_Else_If_Else
let example = first;
let example2 = second;
let example3 = third;
if (example === something) {
return a;
} else if (example2 === somethingElse){
return b;
} else if (example3 === anotherThing){
return c;
} else {
return null;
}
You can do something like this :
myArray = [];
let example = first;
let example2 = second;
let example3 = third;
if (example === something) {
myArray.push(null);
} else {
myArray.(something);
}
if (example2 === somethingElse) {
myArray.push(null);
} else {
myArray.(somethingElse);
}
if (example3 === somethingMore) {
myArray.push(null);
} else {
myArray.(somethingMore);
}
return myArray;
Like Tom O. said return will immediatly exit your function. You can use something other than an array but remember return is executed only once.
Regardless of your approach, it seems like you want to build a "collection" of some sort (array, object, set, map, etc) then return it at the end.
But, the way you code it depends on the reason your function exists. Let's look at an example...
if (first === undefined) {
return null
} else {
return first
}
...This logic exists solely to ensure a "default" value is used for first - something like the null object pattern. For this use case, I might propose nullish coalescing to keep it simple (or something that could be easily replaced with it in the future):
first ?? null
// or, if you don't use babel/some kind of transpiler, you could want:
first !== undefined && first !== null ? first : null
// and since our default is null anyway, we can shorten this to:
first !== undefined ? first : null
Looking solely at your example, it seems like you could simply want to get default values like this for multiple variables. For that use case, you (or someone else coming across this question) might want a function similar to one in the code snippets below. Using objects and/or arrays for this can be handy because they can also be easily broken back out into multiple variables, if you wanted.
First, example functions using arrays:
// If you want default values for items in an array (static, all same default value)
const buildArrayWithDefault = (vals, defaultVal = null) => vals.map(
v => v !== undefined ? v : defaultVal // could be v ?? defaultVal
)
// If you want default values for items in an array (static, but defaults could all be different)
const buildArrayWithDefaults = (vals, defaultVals) => vals.map(
(v, idx) => v !== undefined ? v : defaultVals[idx] // could be v ?? defaultVals[idx]
)
// If you want default values for items in an array (dynamic via callback)
const buildArrayWithDefaults2 = (vals, getDefaultValue) => vals.map(
(v, idx) => v !== undefined ? v : getDefaultValue(v, idx)
)
// All of these return [ 1, 5, 3 ]
console.log(
buildArrayWithDefault([1, undefined, 3], 5),
buildArrayWithDefaults([1, undefined, 3], [ 4, 5, 6 ]),
buildArrayWithDefaults2([1, undefined, 3], (v, idx) => idx + 4)
)
Next, examples using objects:
// Hard-coded default values for an object (ternary)
const buildObject = (first, second, third) => ({
first: first !== undefined ? first : null, // or first ?? null
second: second !== undefined ? second : null,
third: third !== undefined ? third : null,
})
// Hard-coded default values for an object (default parameters)
const buildObject2 = (
first = null,
second = null,
third = null
) => (
{ first, second, third }
)
// ...or you can just use Object.assign()
const assignDefaults = (obj) => Object.assign(
{ first: null, second: null, third: null }, // defaults
obj
)
// Finally, allowing the function user to define their own defaults
// (At this point, you may just want to use Object.assign() directly)
const assignDefaults2 = (...args) => Object.assign({}, ...args.reverse())
// All of these should return { first: 1, second: null, third: null }
console.log(
buildObject(1),
buildObject2(1),
assignDefaults({ first: 1 }),
assignDefaults2({ first: 1 }, { first: null, second: null, third: null })
)

Compare value within nested object

So I've been trying to find a solution to this for a little while with no luck.
const nameTest = 'testName';
const test = {
RANDOM_ONE: {
NAME: 'testName',
SOMETHING: {...}
},
RANDOM_TWO: {
NAME: 'Name',
SOMETHING: {...}
}
}
Is there any simple, easy way where I can compare the nameTest and the NAME key without knowing what the RANDOM_X is in order to access NAME?
You can use Object.keys() to get the array of all the keys. Then loop through the array to check the property:
const nameTest = 'testName';
const test = {
RANDOM_ONE: {
NAME: 'testName',
SOMETHING: {}
},
RANDOM_TWO: {
NAME: 'Name',
SOMETHING: {}
}
}
let testKeys = Object.keys(test);
testKeys.forEach(function(k){
console.log(test[k].NAME == nameTest);
});
You can use a for ... in loop:
for (let key in test) {
if (test[key].NAME === nameTest) {
// do something
}
}
I hope we know that 2 levels down into test is your object. You could write a function, to compare the name key.
function compare(obj, text){
for(let x in obj){
if(obj.x.name == text) return true;
else ;
}
}
Then call the function with your object and the string.
let a = compare(test, nameTest);
Note: this would compare the object to only ascertain if it contains the nameTest string.
var obj= test.filter(el){
if(el.NAME==nameTest)
{
return el;
}
}
var x= obj!=null?true:false;
You could use find.
The find method executes the callback function once for each index of
the array until it finds one where callback returns a true value. If
such an element is found, find immediately returns the value of that
element. Otherwise, find returns undefined.
So it is more memory efficient, than looping over the whole object with forEach, because find returns immediately if the callback function finds the value. Breaking the loop of forEach is impossible. In the documentation:
There is no way to stop or break a forEach() loop other than by
throwing an exception. If you need such behavior, the forEach() method
is the wrong tool.
1. If you want to get the whole object
var nameTest = 'testName';
var test = {
RANDOM_ONE: {
NAME: 'testName',
SOMETHING: {}
},
RANDOM_TWO: {
NAME: 'Name',
SOMETHING: {}
}
};
function getObjectByNameProperty(object, property) {
var objectKey = Object.keys(object)
.find(key => object[key].NAME === property);
return object[objectKey];
}
var object = getObjectByNameProperty(test, nameTest);
console.log(object);
2. If you just want to test if the object has the given name value
var nameTest = 'testName';
var test = {
RANDOM_ONE: {
NAME: 'testName',
SOMETHING: {}
},
RANDOM_TWO: {
NAME: 'Name',
SOMETHING: {}
}
};
function doesObjectHaveGivenName(object, nameValue) {
var objectKey = Object.keys(object)
.find(key => object[key].NAME === nameValue);
return objectKey ? true : false;
}
console.log( doesObjectHaveGivenName(test, nameTest) );

Shortest way to set value to variable in Knockout

In Knockout I have observable variable location. It is of type LocationEdit. This viewModel has observable and not fields.
I have collection of field names : fields. For each field I want to reset values for location
fields.forEach(field => {
if (this.uniqueField(locs, field)) {
if (ko.isObservable(this.location()[field])) {
this.location()[field](locs[0][field]);
} else {
this.location()[field] = locs[0][field];
}
}
});
To make this code more simpler (remove if-clauses), Can I somehow set value to this.location()[field] in one line?
You could use the conditional operator (... ? ... : ... ;) although it doesn't change much:
fields.forEach(field => {
if (this.uniqueField(locs, field)) {
ko.isObservable(this.location()[field]) ? this.location()[field](locs[0][field]) : this.location()[field] = locs[0][field];
}
});
Or you could write a function:
function upd(arr, index, val) {
ko.isObservable(arr[index]) ? arr[index](val) : arr[index] = val;
}
Usage:
fields.forEach(field => {
if (this.uniqueField(locs, field)) {
upd(this.location(), field, locs[0][field]);
}
});
See demo.
You could even add this function to ko:
if(typeof ko.updatePotentialObservable == 'undefined')
ko.updatePotentialObservable = function (arr[index], val) {
ko.isObservable(obj) ? arr[index](val) : arr[index]= val;
}
Usage:
fields.forEach(field => {
if (this.uniqueField(locs, field)) {
ko.updatePotentialObservable(this.location(), field, locs[0][field]);
}
});
See other demo
To be honest, I think Gôtô's answers are definitely your best options. Basically, you'd want to create a utility function similar to ko.unwrap but setting a value.
But since you said "also want to find another solution", here's a different utility function. I think the most confusing part of your code is the returning calls to locs[0][field] and this.location()[field]. I'd want something with this signature:
reset(source, target, keys);
So, in your code, you could do:
reset(
this.location(),
locs[0],
fields.filter(f => this.uniqueField(locs, f))
);
Now, writing this method, I ended up with this:
const mergePropsObs = (function() {
// Return a method for setting a specific property in object
const getSetter = obj => prop => ko.isObservable(obj[prop])
? obj[prop]
: val => obj[prop] = val;
// Return unique keys for two objects
// (I went with a quick oneliner; there are many ways to do this)
const allKeys = (obj1, obj2) =>
Object.keys(Object.assign({}, obj1, obj2));
return (base, ext, onlyProps) => {
const props = onlyProps || allKeys(base, ext);
const values = props.map(p => ko.unwrap(ext[p]));
props
.map(getSetter(base))
.forEach((setter, i) => setter(values[i]));
};
}());
var base = { a: 1, b: ko.observable(2), c: 5 };
mergePropsObs(
base,
{ a: 2, b: 3 },
["a", "b"]);
console.log(base.a);
console.log(base.b());
console.log(base.c);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
This utility method can be used with the signature mentioned above. It also has a fallback for when you don't provide an array of field names.

Need to pass data into JavaScript forEach function

I want to be able to cycle through a JSON object and return a value based on what I'm looking for. Say the object contains people, each person has a name and a zipcode. I want a function that will return a zipcode when I pass a name to it.
The following code is giving me an 'undefined' error, so it must be something to do with scope.
var myArray=[
{
"name":"Joe",
"zip":90210
},
{
"name":"Bill",
"zip":94109
}
];
function findIt(myArray,target) {
myArray.forEach(function (person) {
if (person.name === target) {
return person.zip;
}
});
}
var zip=findIt(myArray,"Joe");
You're not returning anything from your function
function findIt(myArray,target) { // <--------------------┐
myArray.forEach(function (person) { // <--┐ |
if (person.name === target) { // | |
return person.zip; // returns this func ---┘ |
} // |
}); // |
// you need a return for this function --------------┘
}
Anyway, even if you fix that, your function could still use a bit of work. forEach is going to loop through the entire set of data, even if it finds a match on the first item. forEach is the wrong thing to use here.
A really simple thing you could use is Array.prototype.find. Array.prototype.find will stop iterating through the data as soon as the first match is found.
var myArray=[
{
"name":"Joe",
"zip":90210
},
{
"name":"Bill",
"zip":94109
}
]
const findIt = (data, target) => {
// use Array.prototype.find
let {zip} = myArray.find(({name})=> name === target) || {}
return zip
}
console.log(findIt(myArray, 'Joe'))
// => 90210
console.log(findIt(myArray, 'unmatched name'))
// => undefined
But if you would rather implement the function yourself as a learning exercise, that would look like this. Note this code will operate like Array.prototype.find in that it will return as soon as the first match is found.
var myArray=[
{
"name":"Joe",
"zip":90210
},
{
"name":"Bill",
"zip":94109
}
]
// implement generic find
const find = f => ([x,...xs]) => {
if (x === undefined)
return undefined
else if (f(x))
return x
else
return find (f) (xs)
}
// implement your function using find
const findIt = (data, target) => {
let {zip} = find (({name})=> name === target) (data) || {}
return zip
}
console.log(findIt(myArray, 'Joe'))
// => 90210
console.log(findIt(myArray, 'unmatched name'))
// => undefined
This is a smarter way to write findIt because instead of concerning itself with looping through the list of data, or returning a matching object, it just calls a generic function (find) which can be reutilized by many other functions.
ES5
As a courtesy, I'm providing a pre-ES6 version of the code above
var myArray=[
{
"name":"Joe",
"zip":90210
},
{
"name":"Bill",
"zip":94109
}
]
// implement generic find
function find (f) {
return function(xs) {
if (xs[0] === undefined)
return undefined
else if (f(xs[0]))
return xs[0]
else
return find (f) (xs.slice(1))
}
}
// implement your function using find
function findIt (data, target) {
var p = find (function(x) { return x.name === target }) (data) || {}
return p.zip
}
console.log(findIt(myArray, 'Joe'))
// => 90210
console.log(findIt(myArray, 'unmatched name'))
// => undefined
You are returning zip code from anonymous inner function and not from findIt function.
To solve this problem, you can store result from anonymous inner function in a variable and then return that variable from findIt function as demonstrated below.
var myArray=[
{
"name":"Joe",
"zip":90210
},
{
"name":"Bill",
"zip":94109
}
];
function findIt(myArray,target) {
var answer;
myArray.forEach(function (person) {
if (person.name === target) {
answer = person.zip;
}
});
return answer;
}
var zip=findIt(myArray,"Joe");
console.log(zip);
A callback version, just for fun:
function findIt(myArray,target, cb) {
myArray.forEach(function (person) {
if (person.name == target) cb(person.zip)
});
}
Like:
findIt(myArray,"Joe", function(zip){ alert(zip) });
Update:
An example of finding and using one zip value at a time with the following array.
var myArray=[
{
"name":"Joe",
"zip":90210
},
{
"name":"Bill",
"zip":94109
},
{
"name":"Joe",
"zip":12345
}
];
It should now alert twice once for "90210" and a second time for "12345"
forEach is implemented like the fallowing, it dose not return a valid value (undefined).
forEach(array, callback) {
for (var index in array) {
callback(array[index]);
}
}
you want to return the desired value once find it, so try this:
function findIt(myArray,target) {
for (var index in myArray) {
if (myArray[index].name === target) {
return myArray[index].zip;
}
}
}
if the array have lots of items, this example will stop after find the one matched, rather than scan all items.

How to create a memoize function

I am stumped with this memoize problem. I need to create a function that will check to see if a value has already been calculated for a given argument, return the previous result, or run the calculation and return that value.
I have spent hours on this and while I am new to JS. I cannot get my head around how to do this. I cannot use any built in functions and would really like to understand what I need to do.
Here is what I have so far, which is so wrong it feels like pseudo-code at this point. I have searched existing memoize questions out here but I cannot seem to make any solution work yet. Any help is much appreciated.
myMemoizeFunc = function(passedFunc) {
var firstRun = passedFunc;
function check(passedFunc){
if(firstRun === undefined){
return passedFunc;
}else{return firstRun;}
}
};
Sorry, I should have been more clear. Here are my specific requirements:
myMemoizeFunc must return a function that will check if the calculation has already been calculated for the given arg and return that val if possible. The passedFunc is a function that holds the result of a calculation.
I understand this may seem like a duplicate, but I am marking as not so, as I am having some serious difficulty understanding what I should do here, and need further help than is given in other posts.
This is what my thought process is bringing me towards but again, I am way off.
myMemoizeFunc = function(passedFunc) {
var allValues = [];
return function(){
for(var i = 0; i < myValues.length; i++){
if(myValues[i] === passedFunc){
return i;
}
else{
myValues.push(passedFunc);
return passedFunc;
}
}
}
};
I should not be returning i or passedFunc here, but what else could I do within the if/else while checking for a value? I have been looking at this problem for so long, I am starting to implement code that is ridiculous and need some fresh advice.
I think the main trick for this is to make an object that stores arguments that have been passed in before as keys with the result of the function as the value.
For memoizing functions of a single argument, I would implement it like so:
var myMemoizeFunc = function (passedFunc) {
var cache = {};
return function (x) {
if (x in cache) return cache[x];
return cache[x] = passedFunc(x);
};
};
Then you could use this to memoize any function that takes a single argument, say for example, a recursive function for calculating factorials:
var factorial = myMemoizeFunc(function(n) {
if(n < 2) return 1;
return n * factorial(n-1);
});
Consider this an extension on the answer of Peter Olson.
For a variable number of arguments you could use something like this.
Note: This example is not optimal if you intent to pass complex arguments (arrays, objects, functions). Be sure to read further and not copy/paste blindly.
function memo(fn) {
const cache = {};
function get(args) {
let node = cache;
for (const arg of args) {
if (!("next" in node)) node.next = new Map();
if (!node.next.has(arg)) node.next.set(arg, {});
node = node.next.get(arg);
}
return node;
}
return function (...args) {
const cache = get(args);
if ("item" in cache) return cache.item;
cache.item = fn(...args);
return cache.item;
}
}
This builds the following cache tree structure:
const memoizedFn = memo(fn);
memoizedFn();
memoizedFn(1);
memoizedFn(1, 2);
memoizedFn(2, 1);
cache = {
item: fn(),
next: Map{ // <- Map contents depicted as object
1: {
item: fn(1),
next: Map{
2: { item: fn(1, 2) }
}
},
2: {
next: Map{
1: { item: fn(2, 1) }
}
}
}
}
This solution leaks memory when passing complex arguments (arrays, object, functions) that are no longer referenced afterwards.
memoizedFn({ a: 1 })
Because { a: 1 } is not referenced after the memoizedFn call it would normally be garbage collected. However now it can't be because cache still holds a reference. It can only be garbage collected once memoizedFn itself is garbage collected.
I showed the above first because it shows the base concept and demonstrates the cache structure in a somewhat simple form. To clean up cache that would normally be garbage collected we should use a WeakMap instead of a Map for complex objects.
For those unfamiliar with WeakMap, the keys are a "weak" reference. This means that the keys do not count towards active references towards an object. Once an object is no longer referenced (not counting weak references) it will be garbage collected. This will in turn remove the key/value pair from the WeakMap instance.
const memo = (function () {
const primitives = new Set([
"undefined",
"boolean",
"number",
"bigint",
"string",
"symbol"
]);
function typeOf(item) {
const type = typeof item;
if (primitives.has(type)) return "primitive";
return item === null ? "primitive" : "complex";
}
const map = {
"primitive": Map,
"complex": WeakMap
};
return function (fn) {
const cache = {};
function get(args) {
let node = cache;
for (const arg of args) {
const type = typeOf(arg);
if (!(type in node)) node[type] = new map[type];
if (!node[type].has(arg)) node[type].set(arg, {});
node = node[type].get(arg);
}
return node;
}
return function (...args) {
const cache = get(args);
if ("item" in cache) return cache.item;
cache.item = fn(...args);
return cache.item;
}
}
})();
const fib = memo((n) => {
console.log("fib called with", n);
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n - 1) + fib(n - 2);
});
// heavy operation with complex object
const heavyFn = memo((obj) => {
console.log("heavyFn called with", obj);
// heavy operation
return obj.value * 2;
});
// multiple complex arguments
const map = memo((iterable, mapFn) => {
console.log("map called with", iterable, mapFn);
const result = [];
for (const item of iterable) result.push(mapFn(item));
return result;
});
console.log("### simple argument demonstration ###");
console.log("fib(3)", "//=>", fib(3));
console.log("fib(6)", "//=>", fib(6));
console.log("fib(5)", "//=>", fib(5));
console.log("### exlanation of when cache is garbage collected ###");
(function () {
const item = { value: 7 };
// item stays in memo cache until it is garbade collected
console.log("heavyFn(item)", "//=>", heavyFn(item));
console.log("heavyFn(item)", "//=>", heavyFn(item));
// Does not use the cached item. Although the object has the same contents
// it is a different instance, so not considdered the same.
console.log("heavyFn({ value: 7 })", "//=>", heavyFn({ value: 7 }));
// { value: 7 } is garbade collected (and removed from the memo cache)
})();
// item is garbade collected (and removed from memo cache) it is no longer in scope
console.log("### multiple complex arguments demonstration ###");
console.log("map([1], n => n * 2)", "//=>", map([1], n => n * 2));
// Does not use cache. Although the array and function have the same contents
// they are new instances, so not considdered the same.
console.log("map([1], n => n * 2)", "//=>", map([1], n => n * 2));
const ns = [1, 2];
const double = n => n * 2;
console.log("map(ns, double)", "//=>", map(ns, double));
// Does use cache, same instances are passed.
console.log("map(ns, double)", "//=>", map(ns, double));
// Does use cache, same instances are passed.
ns.push(3);
console.log("mutated ns", ns);
console.log("map(ns, double)", "//=>", map(ns, double));
The structure stays essentially the same, but depending on the type of the argument it will look in either the primitive: Map{} or complex: WeakMap{} object.
const memoizedFn = memo(fn);
memoizedFn();
memoizedFn(1);
memoizedFn(1, 2);
memoizedFn({ value: 2 }, 1);
cache = {
item: fn(),
primitive: Map{
1: {
item: fn(1),
primitive: Map{
2: { item: fn(1, 2) }
}
}
},
complex: WeakMap{
{ value: 2 }: { // <- cleared if { value: 2 } is garbage collected
primitive: Map{
1: { item: fn({ value: 2 }, 1) }
}
}
}
}
This solution does not memoize any errors thrown. Arguments are considered equal based on Map key equality. If you also need to memoize any errors thrown I hope that this answer gave you the building blocks to do so.
There are a number of memoization libraries available. Doing memoization efficiently is not as straight forward as it seems. I suggest a library be used. Two of the fastest are:
https://github.com/anywhichway/iMemoized
https://github.com/planttheidea/moize
See here for a comprehensive(-ish) list of memoization libraries: https://stackoverflow.com/a/61402805/2441655

Categories

Resources