0

In another Stack Overflow question (Creating a clickable grid in a web browser), an answerer posted this as an example: http://jsfiddle.net/6qkdP/2/.

I was attempting to do something similar while learning JavaScript, but this answer provided more questions than answers. Now, I think I understand the concept of functions that return functions. However, the JS posted on that jsfiddle link confuses me, and here's why...

On line 15, the function is created:

function clickableGrid( rows, cols, callback ){...}

It seems simple: pass in the number of rows and columns, and pass whatever function is being called back (right?). Up top, on line 2, it calls that function, and for the callback, it's passing a function that accepts a few things (el, row, col, i) and provides output in the Console.

var grid = clickableGrid(10,10,function(el,row,col,i){...}

That all seems easy to understand (assuming, of course, I understand it correctly!). But then this bit starting on line 24 confuses me:

It creates a click event listener for the <td> element being created. This event listener calls a new function that accepts 4 parameters...

            cell.addEventListener('click',(function(el,r,c,i){

...which returns another function...

                return function(){

...which is a callback of the main function, passing those same 4 parameters (?)...

                    callback(el,r,c,i);
                }

...and then this bit is what gets passed during the click event?

            })(cell,r,c,i),false);

Is that right? It all seems overly confusing to me...for instance, I don't understand why one function was created that accepts the same parameters, and changes the style of the <td> element right then and there, without requiring all of these callbacks? I'm missing something, and hoping that the more experienced folks here can provide input on what it is that I'm not grasping.

Code, in case JSFiddle is down:

var lastClicked;
var grid = clickableGrid(10,10,function(el,row,col,i){
    console.log("You clicked on element:",el);
    console.log("You clicked on row:",row);
    console.log("You clicked on col:",col);
    console.log("You clicked on item #:",i);

    el.className='clicked';
    if (lastClicked) lastClicked.className='';
    lastClicked = el;
});

document.body.appendChild(grid);
     
function clickableGrid( rows, cols, callback ){
    var i=0;
    var grid = document.createElement('table');
    grid.className = 'grid';
    for (var r=0;r<rows;++r){
        var tr = grid.appendChild(document.createElement('tr'));
        for (var c=0;c<cols;++c){
            var cell = tr.appendChild(document.createElement('td'));
            cell.innerHTML = ++i;
            cell.addEventListener('click',(function(el,r,c,i){
                return function(){
                    callback(el,r,c,i);
                }
            })(cell,r,c,i),false);
        }
    }
    return grid;
}
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
SalarianEngineer
  • 624
  • 4
  • 10
  • 20

3 Answers3

0
   cell.addEventListener('click',(function(el,r,c,i){
                return function(){
                    callback(el,r,c,i);
                }
            })(cell,r,c,i),false);

Javascript variables are function scoped.IE his code is the same as writing.

function clickableGrid( rows, cols, callback ){
    var i,grid,r,c,tr,cell;
    ...

In order to retain the value of each variable with each iteration of the loops one need to create another function scope.Therefore el,r,c,i are properly scoped by creating a closure.

If he didnt write that code.All callbacks would have the same cell,r,c,i values, ie the values at the last iteration of the 2 loops.

try,it remove the immediatly invoked function

   cell.addEventListener('click', function(){
                    callback(cell,r,c,i);
                }
            ,false);

All your click will only only pass the callback the last cell,the last r,c,i values when all the looping is done.

it's exactly like when one writes.

var x=5;

window.getX=function(){
   return x;
}

x = 6;

console.log(getX()) // 6

x inside getX function will always refer to the last value of x.

but If I write

var x=5;

(function(x){

window.getX=function(){
   return x;
}

}(x))

x = 6;

console.log(getX()) // 5

x in getX refers to x in the closure of the immediatly invoked function expression(IIFE).

There is a lot more,and trust me it's a lot more complicated than it seems at first place.Grab the book "Javascript Allongé" to get the whole picture.

mpm
  • 20,148
  • 7
  • 50
  • 55
0

OK, so this is the interesting part:

 cell.addEventListener('click',
    (function(el,r,c,i){
        return function(){
            callback(el,r,c,i);
        }
    })(cell,r,c,i),
    false);

A more naive implementation might look like this:

cell.addEventListener('click',
    function(){
        callback(cell,r,c,i);
    },
false);

If you try this out, it will always say “You clicked on cell #100". This is because the variables cell, r, c, i are captured themselves in the closure and not a copy. That is, when their value changes in the next loop itaration, the value used for all callbacks will change.

This is avoided by wrapping the closure inside a function, which takes the said varibles as parameters and is immediately executed, effectively creating new local variables.

The pattern

(function(args){
    …
})(stuff);

is commonly used in JS to create a new variable scope inside.

wonce
  • 1,893
  • 12
  • 18
0

OK, this part:

cell.addEventListener('click',(function(el,r,c,i){
    return function(){
        callback(el,r,c,i);
    }
})(cell,r,c,i),false);

Is basically doing this:

cell.addEventListener('click',eventHandler,false);

So whatever the part that replaces the eventHandler does, it must return a function as an event handler. So let's rewrite that for clarity:

var eventHandler = (function(el,r,c,i){
    return function(){
        callback(el,r,c,i);
    }
})(cell,r,c,i);
cell.addEventListener('click',eventHandler,false);

Now, that eventHandler bit is a self calling function. Furthermore, it's a function factory (it returns a function). So let's call it eventHandlerFactory. Now rewrite it for clarity:

function eventHandlerFactory (el,r,c,i) {
    return function(){
        callback(el,r,c,i);
    }
}
var eventHandler = eventHandlerFactory(cell,r,c,i);
cell.addEventListener('click',eventHandler,false);

Now that's a lot easier to read. The eventHandlerFactory returns an anonymous function which is then assigned to eventHandler (ignore the content of that anonymous function for now). The eventHandler function generated by eventHandlerFactory is then assigned to the cell's onclick event. When the cell is clicked the eventHandler function gets executed. And the content of the eventHandler function (you can stop ignoring it now) is simply a call to the callback function supplied earlier.

The question is, why the added complication of wrapping the eventHandler in another layer of a function factory? The answer is the classic closure in a loop problem. The variables r, c, i, and cell are captured as closures in the loop and this is a problem because it would make all cells share the same variables. The only way to break closures in javascript is to pass your references as function arguments. So the additional function factory is there to break the closure thereby ensuring that each cell operate with the correct values for the variables.

slebetman
  • 109,858
  • 19
  • 140
  • 171