Artur Eshenbrener
Artur Eshenbrener

Reputation: 2020

Javascript: reuse bounded functions

Standard Function.prototype.bind method creates a new function on each call. I need to store somewhere and reuse bounded variants of that function.

It is not rocket science to implement function bindArg which works for object arguments:

const fnStorage = new WeakMap();
function bindArg(fn, arg) {
  if (!fnStorage.has(fn)) {
    fnStorage.set(fn, new WeakMap());
  }
  if (!fnStorage.get(fn).has(arg)) {
    fnStorage.get(fn).set(arg, fn.bind(null, arg));
  }
  return fnStorage.get(fn).get(arg);
}

this solution works fine, but only for object arguments. It could be done for scalar arguments by changing WeakMap to Map. But this is not good at all, because Map will keep reference to bounded variant of function and prevents from garbage collecting it.

Is there any way to implement bindArg function, which is pure immutable without memory leaking for any type of arguments?

Upvotes: 1

Views: 136

Answers (1)

Aadit M Shah
Aadit M Shah

Reputation: 74234

You can use a Map for primitive data types and a WeakMap for objects:

function isPrimitive(value) {
    return Object(value) !== value;
}

var primStore = new Map;
var objStore = new WeakMap;

function bindArg(fn, arg) {
    var store = isPrimitive(arg) ? primStore : objStore;
    if (!store.has(arg)) store.set(arg, new WeakMap);
    if (!store.get(arg).has(fn)) store.get(arg).set(fn, fn.bind(null, arg));
    return store.get(arg).get(fn);
}

Note that we return store.get(arg).get(fn) instead of store.get(fn).get(arg). This flipping doesn't make any difference to the actual semantics of the bindArg function but it's necessary when we want to distinguish between primitive data types and objects.


Edit: Alternatively, you could create a new WeakMap2 data structure which stores primitive values in a Map and objects in a WeakMap as follows:

var WeakMap2 = defclass({
    constructor: function () {
        this.map1 = new Map;
        this.map2 = new WeakMap;

        if (arguments.length > 0) {
            for (var object in argument[0]) {
                if (isPrimitive(object))
                    throw new TypeError("Iterator value " +
                        object + " is not an entry object");
                else this.set(object[0], object[1]);
            }
        }
    },
    get: function (key) {
        return (isPrimitive(key) ? this.map1 : this.map2).get(key);
    },
    set: function (key, value) {
        return (isPrimitive(key) ? this.map1 : this.map2).set(key, value);
    },
    has: function (key) {
        return (isPrimitive(key) ? this.map1 : this.map2).has(key);
    },
    delete: function (key) {
        return (isPrimitive(key) ? this.map1 : this.map2).delete(key);
    }
});

function defclass(prototype) {
    var constructor = prototype.constructor;
    constructor.prototype = prototype;
    return constructor;
}

function isPrimitive(value) {
    return Object(value) !== value;
}

However, I wouldn't recommend it because it adds a layer of abstraction, making your code slower.

Upvotes: 2

Related Questions