onclick=""与事件处理程序的区别

42

如果我想要执行一个函数,我更喜欢使用内联js:

<p id="element" onclick="doSomething();">Click me</p>

因为这样更容易调试。

然而,我听到有人说不要使用内联JS,而应该使用:

document.getElementById('element').onclick = doSomething;

为什么推荐使用JS事件监听器?


当内联时,关注点的分离和可组合性都会丧失。也就是说,你将显示与行为混合在一起,如果你想要改变行为(例如添加功能),你需要在多个地方进行修改。 - Oded
2
也被称为不显眼的JS。另一个问题是<body onload="...">,它很容易被window.onload覆盖,因此请将事件处理程序放在一个地方。 - mplungjan
1
我目前正在开发一个小型单页应用,我的 JavaScript 文件已超过 700 行。如果我必须在 HTML 代码中搜索所有的事件处理程序绑定,我可能会失去理智 :) - Šime Vidas
3
查看此问题,可以更直观地了解为什么永远不要使用内联事件处理程序属性:您永远不知道会有哪些对象属性被放置到您的范围中,可能隐藏全局变量并使您的代码在某些浏览器或某个未来版本的浏览器中崩溃。 - bobince
1
相关:解释“不冗余JavaScript”(Unobtrusive JavaScript)https://dev59.com/0G855IYBdhLWcg3wMRVW - Adriano
显示剩余2条评论
9个回答

46
这里有一个反对内联事件处理程序的主要论点,也是其他答案所解决的问题,即将表现与逻辑分离
然而,在我看来,实际上存在着一个更大的问题:内联事件处理程序的计算方式相当难以捉摸。
正如你可能知道的那样,on* 属性的内容将用作事件处理程序函数的主体。但是这个函数具有什么特征呢?
其中一个令人惊讶的特点是,某些祖先元素的属性元素本身的属性在内联事件处理程序的范围内。

<form>
    <input name="foo" />
    <button type="button" onclick="console.log(foo); console.log(window.foo);">
        Click me
    </button>
    <div onclick="console.log(foo);">Click me as well!</div>
</form>

点击 按钮 记录日志

<input name="foo"></input>
undefined

在控制台。 window.fooundefined,这告诉您没有全局变量foo。那么变量foo从哪里来?为什么console.log(foo)记录输入元素而不抛出引用错误?因为form元素的属性在事件处理程序的范围内,并且form元素对于其包含的每个命名表单控件元素都有一个属性。您可以使用console.log(document.querySelector('form').foo)轻松测试此内容。
现在,单击div元素实际上会引发引用错误:

ReferenceError: foo is not defined

因此,显然form元素仅适用于表单控件元素,而不适用于任何后代。多么令人困惑啊!

同样地,document对象的属性也在内联事件处理程序的作用域中,这可能会导致一些令人惊讶的错误(你知道document有一个plugins属性吗?)。

内联事件处理程序的具体评估方式已在HTML5规范中正式规定。请特别注意第10步,其中描述了作用域链的创建。


结论:

由于元素和内联事件处理程序之间的隐式连接,错误可能会非常难以跟踪。如果您只想测试某些内容,则使用内联事件处理程序当然是可以的。但在生产代码中使用它们将带来更高的维护成本。

quirksmode.org上的文章很好地解释了不同绑定事件处理程序的方式及其(不)优点。


有关作用域的相关文章:http://jibbering.com/faq/names/event_handler.html - Bergi
更不用说内联点击处理程序需要在全局范围内注册事件了。 - CervEd
古老的问题,我意识到,但它在不久前的一个问题中出现了。看起来至少一些现代浏览器现在不再使用那种奇怪的 with 类似的作用域包装器了;你知道这是真的吗? - Pointy
啊,这只适用于具有表单所有者的元素;显然一些WebKit版本会将其赋予<a>元素,或者至少新问题声称是这样。不要紧。 - Pointy

15

基本上,这与整个保持一切分离有关。所以要将HTML/CSS/JS全部分离。这使得您的HTML更加整洁,我认为没有这些混杂在一起会更容易导航。

然后,如果需要进行大规模更改,您就有足够的空间来将内联JS转移到外部文件中,或者如果您想将相同的功能应用于多个按钮,则代码量更少。而且少代码是一个更愉快的地方。

如果您的JS文件已经妥善并全面记录,那么外部人员就可以更轻松地浏览它们。


1
如果您的JavaScript代码逻辑清晰易懂,那么内联JS的优势就非常低。但是它并没有为外部处理增加优势。此外,由于您只是添加一个函数名称(可能带有参数),因此仍然可以保持所有内容分离。 - Johan
我不知道这是否算是优势,只是一种做法是保持所有内容分开。所以我想回答你的问题,它们之间实际上没有任何优势。正如上面有人指出的,如果您有很多动态内容,有时内联可能更容易,但如果没有,我个人更喜欢外部文件。我认为这可能只是个人偏好。如果我错了,请有人纠正我。 - LeeR

5

避免使用内联JavaScript的原因有很多,其中最重要的原因之一是代码可维护性。

这里举一个快速示例(我仅使用jQuery作为演示目的)。

<p class="element" onclick="doSomething();">Click me</p>
<p class="element" onclick="doSomething();">Click me</p>
<p class="element" onclick="doSomething();">Click me</p>
<p class="element" onclick="doSomething();">Click me</p>
<p class="element" onclick="doSomething();">Click me</p>
<p class="element" onclick="doSomething();">Click me</p>

如果突然有人要求你将所有段落更改为执行另一个函数,你会怎么做?在你的示例中,你需要手动更改HTML代码中的所有内容。但是,如果你选择将HTML与JavaScript分离,你可以像这样简单地完成它。

<p class="element">Click me</p>
<p class="element">Click me</p>
<p class="element">Click me</p>
<p class="element">Click me</p>
<p class="element">Click me</p>
<p class="element">Click me</p>

$('.element').bind('click', doSomethingElse);

HTML代码更加简洁,这使得设计师可以专注于设计而不用担心在进行涉及其他人的项目时会破坏其它部分。
编辑:在下面提供我的评论示例。
Project = {
    // All the variables/constants/objects that need to be globally accessible inside the Project object.

    init : function(){
        // Main entry point...
        this.MainMenu.init();

        // Rest of the code which should execute the moment Project is initiated.
    }
}

Project.MainMenu = {
    // All the variables/constants/objects that need to be accessible only to MainMenu.

    init : function(){ // Is run immediatelly by Project.init()
        // Event handlers relevant to the main menu are bound here

        // Rest of the initialization code
    }
}

Project.SlideShow = {
    // All the variables/constants/objects that need to be accessible only to SlideShow.

    init : function(){ // Is run only on pages that really require it.
        // Event handlers for the slideshow.
    }
}

1
@Johan 是哪个参数?通常你可以从事件对象中获取所有信息,它是事件处理程序的默认第一个参数。 - Šime Vidas
事件处理程序通常利用它们所附加的对象来执行它们应该执行的操作。即使您需要额外的未绑定参数(您也可以不引人注目地收集它们),在单独的JavaScript文件中外部执行所有事件处理操作仍然更加清晰。底线是,在小型项目中,您可能看不到将JavaScript代码与HTML分离的优势,但是当您开始进行大型项目时,您将很快看到它提供的所有好处 :) 在这种情况下,分离就是拯救 :) - user188654
1
@holodoc,我想过这个方法,但是由于我目前在一个相当大的项目中工作,我们不断遇到事件被其他事物覆盖的问题。当然,问题不仅仅是事件注册,而且很难判断一个元素是否已经附加了事件。特别是在较大的项目中,事件注册可能会变得非常复杂。 - Johan
1
好吧,我该说什么呢。如果你发现内联样式更有用,而且不容易被你的队友滥用,那么请尽管使用它 :) 没有规则禁止你这样做。只要记住,来自许多人的话语必须有一定的分量,他们声称不显眼的JavaScript是正确的选择 :) - user188654
1
同意,这正是我发布这个问题的原因,想听听“另一方”的论点 :) 总会有特定情况下内联JavaScript更好,而其他情况下事件监听更好。但对于普通用途呢?到目前为止,我仍然更喜欢内联,但也无妨。 - Johan
显示剩余4条评论

4
尽管其他人可能认为不好,但我认为在标记中内联侦听器具有可取之处。具体来说,它给了你更多自由修改DOM节点的机会。如果你通过JavaScript添加侦听器,在替换任何父节点的innerHTML时,侦听器会丢失。如果你在标记中内联侦听器,你可以克隆节点并对其进行修改,然后用刚才克隆和修改的节点替换原始节点。
也许在一个特定的用例中更好地描述这个问题。我想对文档的多个部分进行更改而不触发多次回流。因此,在这种情况下,我可以克隆节点,进行任何更改(因为它是解除连接的,所以没有引起回流),然后用修改后的节点替换先前的节点(触发一次回流)。使用内联侦听器,这可以防止在替换时丢失任何侦听器。

利用 HTML 的声明式表单(这样)使我能够添加自己的 DOM 元素检查器,可以实时编辑与元素相关的功能,基本上是免费的。如果改为使用所有事件侦听器,除非有系统地跟踪和公开,否则所有绑定都是不可见的,这是不免费的。 - wtr

2

我看到有人说需要在展示和业务逻辑方面进行分离。

在他的例子中,OP确实展示了这种分离!内联事件处理程序中没有逻辑,只是一个函数引用/调用,将在“click”事件上执行...逻辑本身可以在其他地方单独维护。

我个人更喜欢这种方法,因为它具有逻辑流的可发现性。如果我以前从未见过一个应用程序...我会开始遍历代码DOM,然后就会清楚哪些事件处理程序正在运行,哪些函数正在提供处理程序的逻辑。使用例如“JQuery.On”事件处理方法,仅仅通过浏览html,你无法知道哪些处理程序实际上已经连接并提供功能。

简单提供指向函数的内联事件处理程序仅仅是连接事件,并没有将逻辑泄漏到演示中。


1
同意,我甚至会更强烈地表态 - OP的方法促进了关注点分离,将实现细节抽象化。这样,JS代码只关心“如何”,而不必关心“在哪里”使用它。由DOM指定适用于特定元素的逻辑,使编辑DOM更加灵活,并尽可能避免在JS中使用HTML id。 - yoniLavi

0

我很惊讶没有人提到内容安全策略(CSP)作为使用事件属性替代事件属性的原因。我认为这比迄今为止给出的任何个人偏好都更重要。

当正确实现时(即不添加“unsafe-inline”,在我看来完全是无稽之谈),CSP1.0基本上阻止了所有形式的内联脚本。使内联事件属性变得不可能。虽然CSP2.0通过为内联脚本提供“nonce”来试图解决此问题,但它并未为内联事件属性提供此类方法。CSP3.0为内联事件添加了“unsafe-hashes”,但仍没有nonce的迹象。

因此,在缺少内联事件nonce的情况下,从CSP的角度来看,事件属性(或事件侦听器)非常适合。


当链接到您自己的网站或内容(或与您有关联的内容)时,您必须在答案中披露您的关联,以便不被视为垃圾邮件。在用户名中具有与URL相同的文本或在个人资料中提及它不被视为足够的披露根据Stack Exchange政策。 - cigien

0

你可以用相同的方式来说Cascading Style Sheets(CSS)和包含内联样式。不必浏览css文件就能找到当前的类/ID或者父元素/子元素,这是很容易做到的。

将点击事件与10个不同的项绑定行不行呢?还是只有一个定位到一个类的事件处理程序?即使你不需要10个单击处理程序,也应该考虑为可维护性和未来升级设置代码。


但是在这种方式中,您正在包含内联样式,难道您不会添加类和ID属性以便在CSS中进行样式设置吗?当然它们也有其他用途,但仍然。 - Johan
@Johan,ID 和 class 名称只是名称(JavaScript 代码和 CSS 代码都使用它们)。包含内联样式意味着将 CSS 代码硬编码直接插入 HTML 标签中。 - Šime Vidas
1
我并不建议将整个函数代码放在onclick中,只需要一个名称即可。就像你在类或ID中仅放置一个名称一样,onclick也可以被CSS使用(尽管这通常不是好的做法)。 - Johan
@johan 现在的区别是,如果你更改了函数名称或签名,你现在必须在各个地方更新名称。我不应该需要查看你的标记以找到你正在使用的 JS。此外,在标记中包含这些点击事件现在使您的标记依赖于特定的 JS。 - Loktar
1
@Johan 但是现在你有一个额外的名称,这是一个缺点。另一方面,当你用JavaScript绑定事件处理程序时,处理程序函数本身是匿名的 - 你将一个匿名函数绑定到一个或多个元素上,这些元素是使用已经定义在这些元素上的ID和类名选择的。而你所拥有的是用于CSS选择的ID和类名,再加上所有事件处理程序函数的另一组名称。 - Šime Vidas

0

我不知道使用内联处理程序与稍后附加处理程序相比有什么优势。根据我的经验,当我必须处理动态元素时,例如如果我想要为每个图像添加一个处理程序并且图像的数量根据其他数据(例如数据库中的图像)而变化时,我更喜欢使用内联方法。在这种情况下,使用内联允许我为每个图像添加一个处理程序,否则我必须重新计算JavaScript文件中的图像数量以附加处理程序。

当然,当您可以使用诸如jQuery之类的库时,您可以轻松获取元素列表,内联就没有用处了。


0

除了编码标准的角度来看

使用属性:

<p id="element" onclick="doSomething();">点击我</p>

如果您再次添加相同的属性(如下所示)-则将考虑最新的属性。

<p id="element" onclick="doSomethingMore();">点击我</p>

使用事件处理程序:

document.getElementById('element').onclick = doSomething;

假设您添加了以下行

document.getElementById('element').onclick = doSomethingMore; 两个处理程序都会被调用。


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