同一函数的多个事件监听器存储单独的本地变量?

5

我对JavaScript中的闭包还比较陌生(特别是在应用于事件处理程序时!),并且试图掌握导致此代码行为的基本原理:

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-->

我可以看到“clicks”的值在每次触发事件时被更新并存储(可能是在事件处理程序函数返回中?),但仅针对所点击的相应按钮。这些值是如何存储的,为什么它们不在两个事件处理程序之间共享?如果我想要“clicks”值在两个按钮之间共享,我需要在两者范围之外分配一个变量吗?
3个回答

4
您需要考虑作用域。当您调用showClick()时,会创建一个作用域。在这个作用域内,它创建了一个名为“clicks”的变量。此变量仅在此函数及其所有子函数/闭包中有效。
该函数返回一个新函数,即所谓的闭包。这个闭包保持对封装函数作用域的访问。每次调用showClick()时,都会生成一个新的作用域,在此作用域内有clicks变量。此外还会创建并返回闭包。作用域、其中的变量和闭包并不相同,每次调用showClicks时都不同。
这就是它们为什么单独计数的原因。
如果要将两个点击计数在一起......有多种解决方案。
  • 首先,您可以删除闭包。您不需要它。
  • 将闭包保存为showClick函数的上下文,并在每次调用showClick时使用它
  • 将点击保存在全局范围内(不建议)
编辑:如在评论中提问的那样,这里介绍两种保存闭包并重复使用它的方法:

1)"Singleton-Approach I"(保存闭包本身)

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) 单例模式 II (保存点击计数)

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) "上下文方法"(将闭包保存为工厂函数的上下文)

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();

@JoshuaK 第二个要点听起来像我想采取的方法,但你能否更清楚地解释一下这个陈述?我的理解是它是ishegg所描述的更通用版本(即将函数值分配给变量,随后用于两个事件侦听器)。 - Jonathan Straus
@JonathanStraus 我添加了一些例子,希望能有所帮助。如果你有任何进一步的问题,请随时问我。(别忘了在问题得到完全回答后选择一个答案:)) - Joshua K

2
你正在创建多个函数实例。每个实例都有自己的“clicks”变量,这就是它们自行增加的原因。你有几个选项可供选择,其中之一是:
保留对该函数的引用:
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();

这里的 this 指的是 window。不要将变量放在全局作用域中,没有必要。只需将其放置在工厂函数上方的作用域中即可。 - Joshua K
将“函数值”与一个共同的变量联系起来对我来说很有意义!但我不知道你可以在没有其他潜在不良副作用的情况下以这种方式使用“事件”关键字。我假设如果我将函数表达式“var showClick = function() { ... }”放在原始函数声明的位置,它也同样有效? - Jonathan Straus
1
@JoshuaK 当然,这只是为了举例说明。 - ishegg
1
@JonathanStraus 我刚意识到event变量名的问题!我会改掉它...而且,不,这样也行不通:如果你这样调用函数,它仍然会被实例化两次。 - ishegg

1
您正在两次调用showClick(),并将每次调用的结果分配给其各自的事件侦听器。以下是一个简单的查看方式:
function showClick() {
  var clicks = 0
  return function(e) {
    clicks++
    console.log("click "+clicks+" at "+[e.clientX,e.clientY])
    return clicks
  }
}

var clickHandler1 = showClick();
document.getElementById("b1").onclick=clickHandler1;
var clickHandler2 = showClick();
document.getElementById("b2").onclick=clickHandler2;

这是另一种看待它的方式:

document.getElementById("b1").onclick=(function() {
  var clicks = 0
  return function(e) {
    clicks++
    console.log("click "+clicks+" at "+[e.clientX,e.clientY])
    return clicks
  }
})();
document.getElementById("b2").onclick=(function() {
  var clicks = 0
  return function(e) {
    clicks++
    console.log("click "+clicks+" at "+[e.clientX,e.clientY])
    return clicks
  }
})();

这些函数具有相同的功能定义,但它们不共享实例变量。
如果您想在两者之间共享变量,只需完全忘记工厂函数(请注意,这不会调用函数,而是将函数分配给事件侦听器):
var clicks = 0
function showClick(e) {
  clicks++
  console.log("click "+clicks+" at "+[e.clientX,e.clientY])
  return clicks
}

document.getElementById("b1").onclick=showClick;
document.getElementById("b2").onclick=showClick;

这并不是调用函数,而是将函数分配给事件监听器。这可能是我对事件处理程序中的内部函数如何工作的关键误解。感谢您的澄清! - Jonathan Straus
例子2:你可以将clickcount保存为上下文,而不是使用匿名函数。这样会更短一些。例子3:在全局范围内使用“clicks”并不是一个好主意。这里是使用包装匿名函数来保持作用域清洁的正确位置。同样地:你可以将clickcount保存为上下文来避免这个“问题”。 - Joshua K
针对我的回答:“完全忘记工厂函数”,请阅读正确的命名方式(链接在我对您评论的回复中)。外部函数是工厂函数,内部函数是闭包。另一个来源:https://wiki.php.net/rfc/closures,在我的其他回复中,您还可以找到另外三个链接 :) - Joshua K
@JoshuaK 关于“全局作用域”,最好很少在全局作用域中运行任何东西。我会假设所有这些都会被包装在某种函数中。关于闭包命名,抱歉我在脑海中把它搞混了。你是正确的。我已经删除了对你答案的评论,但由于投票锁定,直到你的帖子以某种方式被编辑,我无法更改我的投票。 - user4639281
@TinyGiant没问题 :) 谢谢。这种情况发生在最好的人身上。 - Joshua K

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接