为什么在HTML中使用 onClick() 是一个不好的习惯?

154

我听说在HTML中使用JavaScript事件,如onClick(),是个不好的实践,因为它不符合语义化。我想知道这样做的缺点是什么,并且如何修复以下代码?

<a href="#" onclick="popup('/map/', 300, 300, 'map'); return false;">link</a>

11
http://en.wikipedia.org/wiki/Unobtrusive_JavaScript - gen_Eric
7
onclick 没有问题,但是将 href 设为 #,会导致禁用 JavaScript 的用户无法进行任何操作。对于某些网站来说这是好事,但是仅仅是打开一个窗口却不提供“真实”的链接,这是非常愚蠢的做法。 - Marc B
3
一定要阅读那个链接。它与语义学关系不大,更多地涉及了那个页面上的所有内容 :-) - morewry
尝试在新标签页中打开您的链接,您会看到一个示例,说明为什么这是错误的... - Sebastien C.
3
应该使用 <button>,因为链接应该指向一个实际资源。这样更具语义化,并且更清晰地呈现给屏幕阅读器用户。 - user6560716
Stackexchange 也有很棒的答案。https://softwareengineering.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting - ohkts11
10个回答

180

你可能在谈论不侵入式JavaScript,它看起来像这样:

<a href="#" id="someLink">link</a>

使用一个中央的JavaScript文件,其中的逻辑看起来像这样:

$('#someLink').click(function(){
    popup('/map/', 300, 300, 'map'); 
    return false;
});

优点是:

  • 行为(JavaScript)与表现(HTML)分离
  • 不会混淆语言
  • 如果使用像jQuery这样的Javascript框架,大部分跨浏览器问题可以由它来处理
  • 您可以同时向许多HTML元素添加行为,而无需重复代码

37
你列举了许多优点,但缺点呢?调试蓝烟呢? - abelito
2
@abelito:不确定调试是一个劣势。如果说有什么好处的话,那就是你可以在任何浏览器调试工具中逐步执行 JavaScript 代码。如果代码是内联和onClick方式,可能就没有这么容易了。你还可以编写单元测试,如果你愿意的话,对你的脚本进行测试也很困难,特别是当代码被放在onClick里面且没有分离出来逻辑时。我个人没有问题使用不干扰式 JavaScript 进行调试,管理和测试 JavaScript 代码的好处太多了,所以一定要使用它。 - Nope
58
主要的缺点是发现性差:查看 HTML 并不能告诉你它附加了哪些回调函数,甚至无法确定是否有任何回调函数被附加。 - Michael Borgwardt
1
@abelito,我完全同意。在这种方法下,对于大型代码库来说,调试变得具有挑战性。特别是当你主要使用你没有编写的代码时。因此,我更喜欢在HTML属性内部编写普通的JS代码。对于那些来自React.js世界的人来说,这应该不是什么大问题 :) - artnikpro
6
在这个讨论中,还有一个观点适用于新手——使用onclick可以更明确地映射元素交互和效果(函数调用)之间的因果关系,并减轻认知负荷。许多教程会让学习者直接使用addEventListener,这会将许多更复杂的语法概念压缩在一起。此外,最近的组件化框架,如 Riot 和 React,使用了onclick属性,正在改变分离应该是什么样子的概念。从HTML的onclick转移到支持此功能的组件可能是更有效的“步骤”学习过程。 - jmk2142
显示剩余13条评论

43
如果您正在使用jQuery,则可以使用以下方法:
HTML:
 <a id="openMap" href="/map/">link</a>

JS:

JavaScript:
$(document).ready(function() {
    $("#openMap").click(function(){
        popup('/map/', 300, 300, 'map');
        return false;
    });
});

这样做的好处是即使没有 JavaScript,或者用户使用中键点击链接仍然可以正常工作。

这还意味着我可以通过再次重写来处理通用弹出窗口:

HTML:

 <a class="popup" href="/map/">link</a>

JS:

$(document).ready(function() {
    $(".popup").click(function(){
        popup($(this).attr("href"), 300, 300, 'map');
        return false;
    });
});

通过给链接添加 popup 类,您可以将弹出窗口添加到任何链接。

这个想法甚至可以进一步扩展,像这样:

HTML:

 <a class="popup" data-width="300" data-height="300" href="/map/">link</a>

JS:

$(document).ready(function() {
    $(".popup").click(function(){
        popup($(this).attr("href"), $(this).data('width'), $(this).data('height'), 'map');
        return false;
    });
});

现在我可以在整个网站上使用同一段代码来处理很多不同的弹窗,而不必写大量的onclick代码!重用性大有可为!

这也意味着如果以后我决定弹出窗口是不好的做法(它们确实是!),并且我想要用类似于灯箱式模态窗口的方式替换它们,我只需要更改:

popup($(this).attr("href"), $(this).data('width'), $(this).data('height'), 'map');

myAmazingModalWindow($(this).attr("href"), $(this).data('width'), $(this).data('height'), 'map');

现在我的整个网站上的弹出框都表现得完全不同了。我甚至可以进行功能检测以决定在弹出框上该执行什么操作,或存储用户的偏好以允许或不允许它们。使用内联 onclick,这需要大量复制和粘贴。


6
不使用JavaScript?那就需要jQuery。它需要浏览器启用JavaScript才能工作,对吧? - Thomas Shields
3
可以,但在这种情况下,该链接可以重定向到一个页面,在没有JavaScript的情况下执行该操作。 - ThiefMaster
13
这个链接不需要JavaScript也可以使用。请注意该链接具有普通的href属性。如果用户没有JavaScript,该链接仍将是一个链接,用户仍然可以访问内容。 - morewry
4
@Thomas Shields:他的意思是,即使你没有启用JavaScript,这个链接仍然会带你到/map/...。 - Robert
2
啊,Rich!我们都喜欢你提供的这个最巧妙的解决方案! - Nirmal
显示剩余5条评论

22

在非常庞大的JavaScript应用程序中,程序员正在更多地使用代码封装来避免污染全局作用域。要使函数可用于HTML元素中的onClick操作,它必须位于全局作用域中。

您可能已经看到过像这样的JS文件...

(function(){
    ...[some code]
}());

这些是立即调用的函数表达式(IIFEs),在其中声明的任何函数只存在于其内部作用域中。

如果你在IIFE中声明了function doSomething(){},然后将doSomething()设置为HTML页面元素的onClick操作,你会得到一个错误。

另一方面,如果你在那个IIFE中创建了一个元素的事件监听器,并在监听器检测到单击事件时调用doSomething(),那么就没问题了,因为监听器和doSomething()共享IIFE的作用域。

对于小型Web应用程序来说,代码量很少,这并不重要。但是如果你有志写大型、可维护的代码库,那么onclick=""是一个应该避免的习惯。


3
如果你想编写可维护的大型代码库,建议使用像Angular、Vue、React这样的JS框架...它们推荐在HTML模板中绑定事件处理程序。 - Kamil Kiełczewski

21

有几个原因不太好:

  • 它混合了代码和标记
  • 以这种方式编写的代码会经过eval
  • 并在全局范围内运行

最简单的方法是给你的<a>元素添加一个name属性,然后可以这样做:

document.myelement.onclick = function() {
    window.popup('/map/', 300, 300, 'map');
    return false;
};

现代最佳实践是使用id而不是name,并使用addEventListener()而不是onclick,因为这允许您将多个函数绑定到单个事件。


即使建议这种方式,也会让用户面临许多事件处理程序冲突的问题。 - preaction
3
就像内联事件处理程序一样,因此我的答案建议使用addEventListener(),并解释为什么这样做更好。 - Alnitak
4
有人能否将这个解释得更详细一些?“用这种方式编写的代码会经过 eval 处理”... 我在网上搜不到相关内容。你是说内联 JavaScript 存在某些性能或安全方面的劣势吗? - Kalnode

15

修订

无侵入式JavaScript方法在过去曾经很好用 - 特别是在HTML中绑定事件处理程序被认为是一种不好的做法(主要原因是onclick事件在全局范围内运行,可能导致意外错误,这是由YiddishNinja提到的)。

然而...

目前看来,这种方法有些过时,需要进行更新。如果有人想成为专业的前端开发者并编写大型和复杂的应用程序,则需要使用诸如Angular、Vue.js等框架......但是这些框架通常使用(或允许使用)HTML模板,其中事件处理程序直接绑定在html模板代码中,非常方便、清晰和有效 - 例如,在angular模板中,通常人们会写:

<button (click)="someAction()">Click Me</button> 

在原始的js/html中,等价于此的内容将是:

<button onclick="someAction()">Click Me</button>

区别在于原始js的onclick事件在全局范围内运行,但框架提供了封装。

那么问题出在哪里?

问题在于,当新手程序员总是听说html-onclick不好,而总是使用btn.addEventListener("onclick", ... )想要使用一些具有模板的框架时(addEventListener也有缺点-如果我们使用innerHTML=以动态方式更新DOM(这相当快速),则会丢失以该方式绑定的事件处理程序)。然后他将面临一些不良习惯或错误的框架使用方法-因为他将主要关注js部分而不是模板部分(并产生不清晰和难以维护的代码)。为了改变这种习惯,他将浪费很多时间(可能还需要一些运气和老师)。

因此,在我的经验中,更好的选择是让他们一开始使用html-handlers-bind。正如我所说,确实是调用处理程序处于全局范围内,但在这个阶段,学生通常创建容易控制的小型应用程序。要编写更大的应用程序,他们选择一些框架。

那么该怎么办?

我们可以更新Unobtrusive JavaScript方法,并允许在html中绑定事件处理程序(最多带有简单参数)(但仅绑定处理程序-不像OP问题中那样放置逻辑到onclick中)。因此,在我看来,在原始js / html中,这应该是允许的。

<button onclick="someAction(3)">Click Me</button>
或者

function popup(num,str,event) {
   let re=new RegExp(str);  
   // ... 
   event.preventDefault();
   console.log("link was clicked");
}
<a href="https://example.com" onclick="popup(300,'map',event)">link</a>

但以下示例不应被允许

<button onclick="console.log('xx'); someAction(); return true">Click Me</button>

<a href="#" onclick="popup('/map/', 300, 300, 'map'); return false;">link</a>

现实在不断变化,我们的观点也应该随之改变。


嗨Kamil,我是JavaScript的初学者,也对onclick的使用感到困惑,请问您能否解释一下onclick的缺点,具体如下:1.您只能分配一个内联事件。2.内联事件被存储为DOM元素的属性,并且像所有对象属性一样,它可以被覆盖。3.即使您删除了onclick属性,此事件仍将继续触发。 - mirzhal
1
广告1:是的,只需要一个,但我的经验(使用Angular)表明在大多数情况下这已经足够了 - 但如果不够,那就使用addEventListener。广告2:是的,它们可以被覆盖(但通常没有人这样做) - 那有什么问题吗?广告3:删除onclick属性后,处理程序将不会被触发 - 证明在此处 - Kamil Kiełczewski
6
React、Vue和Angular看起来可能在使用“内联事件处理程序”,但它们并没有像原问题描述的那样使用HTML属性。它们使用了指令,在底层它们将使用addEventListener(click)onclick绝对不是等价的。 - Colin
@Colin 是的 - 但是错误的习惯仍然存在。 - Kamil Kiełczewski
2
依我之见,你的 onclick="someAction(3)" 是一个“错误习惯”的例子。someAction 函数正在污染全局命名空间。 - Colin

12

有几个原因:

  1. 我发现将标记,即HTML和客户端脚本分开会有助于维护。例如,jQuery让以编程方式添加事件处理程序变得容易。

  2. 您提供的示例在任何不支持JavaScript或已关闭JavaScript的用户代理中都将失效。 渐进增强 的概念鼓励为没有 JavaScript 的用户代理使用简单的超链接指向 /map/,然后为支持 JavaScript 的用户代理以编程方式添加点击处理程序。

例如:

标记:

<a id="example" href="/map/">link</a>

Javascript:

$(document).ready(function(){

    $("#example").click(function(){
        popup('/map/', 300, 300, 'map');
        return false;
    });

})

2
谢谢你提醒我渐进增强,所以这仍然符合这个范例:<a href="/map/" onclick="popup('/map/', 300, 300, 'map'); return false;">link</a> - Daniel Sokolowski

8
这是一种名为“Unobtrusive JavaScript”的新范例。当前的“Web标准”要求将功能和呈现分离。
这并不是一个“不好的实践”,只是大多数新标准希望您使用事件监听器而不是内联JavaScript。
此外,这可能只是个人喜好,但我认为使用事件监听器更容易阅读,特别是当您想运行多个JavaScript语句时。

2
维基百科上关于这个主题的页面已经超过4年了。它不再是一个的范例了。 :) - Quentin
@David:这可能不是“新”的,但仍有人没有使用它,所以对他们来说是“新”的。 - gen_Eric
所以我的担忧在于,当我编写此函数并将其添加为事件侦听器时,如果出现错误并且其他人想要修复它,在HTML方式中,他们可以检查按钮,查看触发的函数,然后将其复制并粘贴到浏览器控制台窗口中,轻松找到该函数。但是对于事件侦听器,我必须搜索特定ID,这可能在同一应用程序中具有多个实例。 - Marcel Marino

6

我想你的问题会引起讨论。总体思路是,将行为和结构分离是有益的。另外,据我所知,内联点击处理程序必须进行eval才能“变成”一个真正的JavaScript函数。虽然这有点老派,但这是一个相当脆弱的论点。嗯,可以在@quirksmode.org了解更多相关信息。


我不想再引发一场圣战,我正在努力寻找真相:\ - NiLL

2
  • 点击事件在全局范围内运行,可能会导致意外错误。
  • 将点击事件添加到许多DOM元素中会降低性能和效率。

1

有两个不使用内联处理程序的理由:

它们可能需要繁琐的引号转义问题

给定一个任意字符串,如果你想构造一个调用该字符串的函数的内联处理程序,为了得到通用解决方案,你将不得不转义属性分隔符(与相关的HTML实体一起),并且你还将不得不转义用于属性内部字符串的分隔符,如下所示:

const str = prompt('What string to display on click?', 'foo\'"bar');
const escapedStr = str
  // since the attribute value is going to be using " delimiters,
  // replace "s with their corresponding HTML entity:
  .replace(/"/g, '&quot;')
  // since the string literal inside the attribute is going to delimited with 's,
  // escape 's:
  .replace(/'/g, "\\'");
  
document.body.insertAdjacentHTML(
  'beforeend',
  '<button onclick="alert(\'' + escapedStr + '\')">click</button>'
);

那太丑了。从上面的例子可以看出,如果您没有替换',将导致SyntaxError,因为alert('foo'"bar')不是有效的语法。如果您没有替换",那么浏览器会将其解释为onclick属性的结束(在上面用"分隔),这也是不正确的。

如果一个人习惯于使用内联处理程序,则必须确保记住做类似于上述操作(并且正确地执行)每次,这很繁琐,一眼很难理解。最好完全避免使用内联处理程序,以便可以在简单的闭包中使用任意字符串:

const str = prompt('What string to display on click?', 'foo\'"bar');
const button = document.body.appendChild(document.createElement('button'));
button.textContent = 'click';
button.onclick = () => alert(str);

这不是更好吗?


内联处理程序的作用域链是非常奇特的

您认为以下代码将输出什么?

let disabled = true;
<form>
  <button onclick="console.log(disabled);">click</button>
</form>

试一下,运行代码片段。它可能不是你期望的结果。为什么会产生这样的结果?因为内联处理程序在 with 块中运行。上面的代码在三个 with 块中:一个用于 document,一个用于 <form>,一个用于 <button>

let disabled = true;
<form>
  <button onclick="console.log(disabled);">click</button>
</form>

enter image description here

由于disabled是按钮的属性,内联处理程序中引用disabled指的是按钮的属性,而不是外部的disabled变量。这非常令人困惑。使用with会有很多问题:它可能是混淆错误的来源,并且会显着减慢代码。在严格模式下甚至根本不允许使用。但是对于内联处理程序,您必须通过with来运行代码,而且不仅要通过一个with,而且要通过多个嵌套的with。这太疯狂了。 with在代码中永远不应该被使用。因为内联处理程序隐式地要求使用with以及所有混乱的行为,所以应该避免使用内联处理程序。

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