4

I am designing a small library, and trying to keep the API as small as possible, the idea of using a function as a dictionary/object itself looks appealing.

The idea is to be able to call a function normally like:

fn('hello', 'some other extra info to be processed', etc...)

This call will process the information and then store it somewhere. This processed information can be accessed in certain conditions (not the typical use case), and it would be great to fetch information in this fashion:

fn['hello']
//-> some stuff

In python for instance it would be very easy to overload the [] operator, but AFAIK there is not an easy and reliable way in JS that works in most environments (proxies seem to do the trick, but we are not so far yet). Getters and setters are not an option since the user can input any value.

Therefore, I am left with setting attributes of the function object, which seems hacky because I might overwrite the original attributes of the function, for instance:

  • apply
  • prototype
  • __proto__

However, many things in the JS world are hacky and we happily do them everyday. The question: is this unsafe and will lead to the death of thousands of kittens?

bgusach
  • 14,527
  • 14
  • 51
  • 68
  • I dom't see the advantage of using `fn['hello']` over `fn('hello')`. Why not just stick with the function-call? As you mentioned, adding properties to the function may fail. – Thomas May 18 '16 at 13:33
  • such approach `fn['hello'] <-> fn('hello')` looks confusing and ambiguous – RomanPerekhrest May 18 '16 at 13:42
  • @Thomas, there are two operations. Calling the function would process and register some data, and the `[]` access would return this data. Anyway, the use case is not worth explaining in depth, the question is if this will explode or not. My second approach would be to attach a `.get` method to the function. – bgusach May 18 '16 at 13:49
  • @RomanPerekhrest, I updated my question to bring some extra info. I think my users would not confuse the two things. – bgusach May 18 '16 at 13:50

2 Answers2

5

You could use setter and getter functions for it.

The disadvantage is, you have to know in advance which keys you use.

function fn(s) {
    if (!fn[s]) {
        Object.defineProperty(fn, s, { get: function () { document.write('hello '+s+'!<br>'); }, set: function () { } });
    }
    document.write('hello ' + s + '<br>');
}

fn('stackoverflow');
fn['stackoverflow'];
fn('42');
fn['42'];
fn['stackoverflow'];
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
4

@Thomas, there are two operations. Calling the function would process and register some data, and the [] access would return this data. Anyway, the use case is not worth explaining in depth, the question is if this will explode or not.

Yes, it will explode, not because of the properties you already mentioned, but because of length and name. These properties are more likely to show up on a dictionary (than apply, prototype or proto), and will cause you trouble, because you can't just overwrite them on a function. You can redefine them with defineProperty, but all that just increases the complexity, memory-footprint, yada yada yada ... for no good reason.

A better approach would be a combined getter and setter, especially since this pattern is aready in use by Libs like jQuery for example:

function getterSetter(key, value){
    if(arguments.length > 1){
        this[key] = value;
    }else{
        return this[key];
    }
}
//for explicitely telling wether you want to get() or set()
function getter(key){ return this[key]; }
function setter(key, value){ this[key] = value; }

function dict(){
    var cache = Object.create(null);
    var fn = getterSetter.bind(cache);
    fn.get = getter.bind(cache);
    fn.set = setter.bind(cache);
    return fn;
}

Usage

var cache = dict();
//set
cache.set("hello", "kitty");
//or just
cache("hello", "kitty");

//get
cache.get("hello");
//or
cache("hello");

and even when you want to map over an Array:

var values = ["foo", "bar", "hello"].map( cache.get );

in this case, you can't use the shortcut .map(cache) because the map-function passes two more arguments, the index and the Array, and cache() would interpret the index as the value to set.

Or you can account for that:

funciton isArrayLike(obj){
    return !!obj && typeof obj === "object" && (+obj.length) === (obj.length >>> 0);
}

function getterSetter(key, value, arr){
    if(arguments.length > 1 && !isArrayLike(arr)){
        this[key] = value;
    }else{
        return this[key];
    }
}

var values = ["foo", "bar", "hello"].map(cache);

But that are implementation-details on your lib. aka. your decision.

Thomas
  • 3,513
  • 1
  • 13
  • 10
  • The first paragraph has all the info I was looking for. Thank you very much! You saved the life of many kittens. – bgusach May 18 '16 at 14:20