Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple event listeners with same function storing separate local variables?

I'm fairly new to closures in javascript (certainly when applying them to event handlers!), and trying to grasp the underlying principles leading to the behavior of this code:

function showClick() {
  var clicks = 0
  return function(e) {
    clicks++
    console.log("click "+clicks+" at "+[e.clientX,e.clientY])
    return clicks
  }
}

document.getElementById("b1").onclick=showClick() 
document.getElementById("b2").onclick=showClick()
<button id="b1">Record my Clicks</button> <!--only stores click history of b1, disregarding b2-->

<button id="b2">Record MY Clicks</button> <!--only stores click history of b2, disregarding b1-->

I can see that the value for "clicks" is being updated and stored (presumably in the event handler function return?) each time I trigger the event, but is only doing so for the respective button being clicked. How are these values being stored, and why aren't they being shared between the two event handlers? Would I need to assign a variable outside the scope of both if I want the value of "clicks" to be shared between both buttons?

like image 632
Jonathan Straus Avatar asked Mar 08 '23 06:03

Jonathan Straus


2 Answers

You have to think in 'scopes'. When you call showClick() there is a scope. Inside this scope it creates a variable, called 'clicks'. This variable is valid within this function and all subfunctions / closures.

The function returns a new function, a so called closure. This closure keeps access to the scope of the encapsulated function. Everytime when you call showClick() a new scope is generated and within this scope the variable clicks. Furthermore there is the closure created and returned. The scopes, the variables within this scopes and the closures are not the same. There different everytime you call showClicks.

That's the reason why they count seperately for both links.

If you want to count both clicks together... there are multiple solutions.

  • First you could remove the closure. you dont't need it.
  • Save the closure as the context of the showClick function and use it everytime if showClick is called
  • save the clicks in global scope (not recommended)

EDIT: Like asked in the comments here two approaches to save the closure and reuse it:

1) The "Singleton-Approach I" (save the closure itself)

function showClick() {
    if(showClick.closure) return showClick.closure;
    var clicks = 0;
    return showClick.closure = function(evt) {
        clicks++;
        console.log(clicks+' at '+evt.clientX+' / '+evt.clientY);
    }
}
document.getElementsByTagName('a')[0].onclick = showClick();
document.getElementsByTagName('a')[1].onclick = showClick();

2) The "Singleton-Approach II" (save the clickcount)

function showClick() {
    showClick.clicks = showClick.clicks || 0;
    return function(evt) {
        showClick.clicks++;
        console.log(showClick.clicks+' at '+evt.clientX+' / '+evt.clientY);
    }
}
document.getElementsByTagName('a')[0].onclick = showClick();
document.getElementsByTagName('a')[1].onclick = showClick();

3) The "Context-Approach" (save the closure as the context of the factory function)

var showClick = (function showClick() {
    return this;
}).bind((function() {
    var clicks = 0;
    return function(evt) {
        clicks++;
        console.log(clicks+' at '+evt.clientX+' / '+evt.clientY);
    };
})());
document.getElementsByTagName('a')[0].onclick = showClick();
document.getElementsByTagName('a')[1].onclick = showClick();
like image 53
Joshua K Avatar answered Mar 15 '23 23:03

Joshua K


You are creating multiple instances of the function. Each one of those instances has their own clicks variable, which is why they're increasing on their own. You have several options, amongst them:

Keep a reference to the function:

function showClick() {
    var clicks = 0;
    return function(e) {
        clicks++
        console.log("click "+clicks+" at "+[e.clientX,e.clientY])
        return clicks
    }
}
var clickEvent = showClick(); // we instance the function only once
document.getElementById("b1").onclick=clickEvent; // and use it here
document.getElementById("b2").onclick=clickEvent; // and here

Keep the variable in the global scope

function showClick() {
    this.clicks = 0; // this === window here
    return function(e) {
        clicks++
        console.log("click "+clicks+" at "+[e.clientX,e.clientY])
        return clicks
    }
}
document.getElementById("b1").onclick=showClick();
document.getElementById("b2").onclick=showClick();
like image 42
ishegg Avatar answered Mar 16 '23 00:03

ishegg