views:

1256

answers:

1

I recently tried to use an implementation of map in javascript to create a bunch of items, then apply them to an objects add method.

Firstly with a bog standard implementation of map.

var map = function (fn, a)
{
    for (i = 0; i < a.length; i++)
    {
        a[i] = fn(a[i]);
    }
}

Setup.

var translateMenu = new Menu;

var languages = [ ['Chinese'   , 'zh-CN']
                , ['German'    , 'de']
                , ['French'    , 'fr']
                , ['Portugese' , 'pt']
                , ['Hindi'     , 'hi']
                ];

And my function... (not anonymous, as it's later used when adding the translateMenu to mainMenu.)

var langItem = function (language, subMenu) 
    { 
       return new MenuItem(language[0], 'http://translate.google.com/translate?u=www.example.com&amp;hl=en&amp;ie=UTF-8&amp;tl=en&amp;sl=' + language[1] , "" , subMenu); 

    }

map ( langItem , languages );

This all worked fine, I now had an array of MenuItems to throw around.

Trying to call map( Menu.add , languages ) would result in internal variables of Menu being undefined, and the call failing.
Now I'm certain this has to do with the scope of the Menu.add() method, so i thought if I passed in the object as well, it might work.

I tried creating a new map function that would accept objects and functions, but had the same undefined error.

objMap (fn , obj , a) {
    for (i = 0; i < a.length; i++)
    {
        obj.fn(a);
    }   
}
objMap ( add , translateMenu , languages );   // failed

I worked around this by extending Menu with addAll() to take an array, which works fine...

Menu.prototype.addAll = function (items){
    for (i = 0; i < items.length; i++)
    {
        this.add(items[i]);
    }
}

translateMenu.addAll( languages ); // yay! but I want a more elegant solution.

Anyway, my question is, how could I implement map (or a similar generic function) to actually support using object methods as my mapped functions?.

+8  A: 

Trying to call map( Menu.add , languages )

Here your problem is almost certainly that of JavaScript's lack of bound methods.

The setting of ‘this’ for a function is determined only at call-time, by examining how the method was obtained. If you say one of:

obj.method();
obj['method']();

JavaScript will pick up the reference to ‘obj’ and set ‘this= obj’ inside the method call. But if you say:

obj2.method= obj.method;
obj2.method();

Now ‘this’ inside the function will be obj2, not obj!

Similarly, if you pick the method off its object and refer to it as a first-class object:

var method= obj.method;
method();

There will be no object for ‘this’ to get set to, so JavaScript sets it to the global object (aka ‘window’ for web browsers). This is probably what is happening in your case: the ‘Menu.add’ method loses all reference to its owner ‘Menu’, so when it gets called back it is most likely unknowingly writing to members of the ‘window’ object instead of a menu.

This is of course highly unusual for an OO language, and almost never what you want, but hey, that's how JavaScript rolls. Causing silent, hard-to-debug errors is all part of the language's rationale.

To get around this problem you could pass an object reference in to your map function, then use Function.call()/apply() to set the ‘this’ reference correctly:

function mapMethod(fn, obj, sequence) {
    for (var i= 0; i<sequence.length; i++)
        sequence[i]= fn.call(obj, sequence[i]);
}

mapMethod(Menu.add, Menu, languages)

A more general way would be to bind function references manually, using a closure:

function bindMethod(fn, obj) {
    return function() {
        fn.apply(obj, arguments)
    };
}

map(bindMethod(Menu.add, Menu), languages)

This capability will be built into a future version of JavaScript:

map(Menu.add.bind(Menu), languages)

And it is possible to add this facility to current browsers by writing to Function.prototype.bind — indeed, some JS frameworks do already. However note:

  • ECMAScript 3.1 promises you'll also be able to pass extra arguments into bind() to do partial function application, which requires a little more code than bindMethod() above;

  • IE loves to leak memory when you start leaving references like bound methods on DOM objects like event handlers.

bobince
Thank you, that's an awesome answer. :D
garrow