7
\$\begingroup\$

I wanted an elegant way to implement memoizing. Here is what I came up with:

function memoize(fn) {
    var cache = new WeakMap();
    return function() {
        if (!cache[arguments]) {
            cache[arguments] = fn.call(this, arguments);
        }
        return cache[arguments];
    }
}

It's quite nice, but the WeakMap is not well supported. Is there a better, yet clean way to do this?

\$\endgroup\$
  • \$\begingroup\$ You should use fn.apply instead of fn.call if you want to use the original arguments. \$\endgroup\$ – Elmer Nov 19 '15 at 10:56
3
\$\begingroup\$

I use this

function memoizeFirst(fn) {
    const cache = new WeakMap();
    return function(arg) {
        if (!cache.has(arg)) {
            cache.set(arg, fn(arg));
        }
        return cache.get(arg);
    };
}

this function will memoize the result of the function you pass and cache it. The cache will vary on the first argument, and only on the first argument. Do not use this function if you are planning on passing more then one argument to it! Do use this function if you want to pass an object as the first argument.

I use this function for building (temporary) indexes based on an immutable state. Example (the groupBy, where methods come from IxJS):

const getParentObjectIndex = memoizeFirst(state => state.objectSource && state.objectSource.
    where(objectItem => objectItem.parentObject).
    groupBy(objectItem => objectItem.parentObject).
    reduce(function(map, group) {
        map[group.key] = group.
        orderBy(item => item.position).
        toArray();
        return map;
    }, {})
);

Because the state is immutable, it will not change. The only way to change the state is to create a new one. So everytime something changes, we have a new state. That makes is an excellent cache key!

\$\endgroup\$
1
\$\begingroup\$

Is there a better, yet clean way to do this?

"Better" and "clean" are both debatable. There's a way to do it, using JSON serialization.

But speaking to your current code, I'm not sure that maps would the trick. Every call to the function will result in a distinct arguments object (even if the individual arguments are the same), so I think you'd get "duplicate" keys. At least I imagine so. I don't have an up-to-date browser on this machine, so I can't test, and I haven't played enough with Map/WeakMap to know off-hand.

Besides, you need to use has() and set(); using [...] will, I believe, just set a regular property (which implies string coercion of the key).

Now, for alternatives, you could do something like what Addy Osmani's proposes (the entire article is worth a read). The basic idea is to use JSON.stringify() to serialize the arguments in a more robust way than toString.

I'll include it here, as-is:

/*
* memoize.js
* by @philogb and @addyosmani
* with further optimizations by @mathias
* and @DmitryBaranovsk
* perf tests: http://bit.ly/q3zpG3
* Released under an MIT license.
*/
function memoize( fn ) {
    return function () {
        var args = Array.prototype.slice.call(arguments),
            hash = "",
            i = args.length;
        currentArg = null;
        while (i--) {
            currentArg = args[i];
            hash += (currentArg === Object(currentArg)) ? 
            JSON.stringify(currentArg) : currentArg;
            fn.memoize || (fn.memoize = {});
        }
        return (hash in fn.memoize) ? fn.memoize[hash] : 
        fn.memoize[hash] = fn.apply(this, args);
    };
}

Source: Faster JavaScript Memoization For Improved Application Performance

It's a pretty neat solution, though obviously not as clean as just using WeakMap (if indeed that would even work).

\$\endgroup\$
1
\$\begingroup\$

Here you find a WeakMap solution for multiple arguments: memoize in https://github.com/Dans-labs/dariah/blob/master/client/src/js/lib/utils.js

For cases where your function deals with large, immutable objects, stringify is useless (it does not improve performance). Then WeakMap is your friend, because you can use the objects themselves as keys, without preventing them to be garbage collected. But you need a trick to generate a key for multiple arguments. The link above points to the solution.

\$\endgroup\$
0
\$\begingroup\$

I just thought about this:

function memoize(fn) {
    var cache = {};
    return function() {
        var args = Array.prototype.slice.call(arguments).toString();
        if (!cache[args]) {
            cache[args] = fn.call(this, arguments);
        }
        return cache[args];
    }
}

It's very cross-browser. I can't think of any browser not supporting this.

But it does feel kind of ugly. It's still the best I can think of though.

\$\endgroup\$
  • \$\begingroup\$ This isn't equivalent. There can be collisions. For example, ["a,b"] and ["a", "b] (at least my understanding of Array.toString -- developer.mozilla.org/en-US/docs/JavaScript/Reference/…) \$\endgroup\$ – Corbin Feb 1 '13 at 19:06
  • \$\begingroup\$ Indeed. Then I'm lost :P \$\endgroup\$ – Florian Margaine Feb 1 '13 at 19:08
  • \$\begingroup\$ JSON stringifying would probably be wiser \$\endgroup\$ – megawac Aug 15 '14 at 0:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.