原文链接:http://javascript.info/default-browser-action,translate with ❤️ by zhangbao.

许多事件会自动触发浏览器行为。

例如:

  • 点击链接:跳到对应 URL 地址。

  • 点击表单中的提交按钮:向服务器提交请求。

  • 在文本上按下鼠标按键并且移动:选择文本。

在 JavaScript 中操作事件时,经常不需要默认的浏览器行为。幸运的是,我们可以阻止它。

阻止浏览器行为

有两种方式来阻止浏览器默认行为:

  • 主要是通过 event 对象的 preventDefault() 方法。

  • 如果处理器是使用 on<事件名> 方式绑定的(用非 addEventListener),在处理器中使用 return false 也能阻止。

点击下面的链接,不会触发到新 URL 地址的跳转:

  1. <a href="/" onclick="return false">点击这儿</a>
  2. <!-- 或者 -->
  3. <a href="/" onclick="event.preventDefault()">还有这儿</a>

浏览器默认行为 - 图1没必要返回 **true**``

事件处理器中返回的值通常会被忽略。

有一个例外,就是使用 on<事件名> 绑定的事件处理器时,可以使用 return false 来阻止默认事件行为。

所有其他情况,返回值没有必要,而且也不会被处理。

例子:菜单

有如下的网页菜单结构:

  1. <ul id="menu" class="menu">
  2. <li><a href="/html">HTML</li>
  3. <li><a href="/javascript">JavaScript</li>
  4. <li><a href="/css">CSS</li>
  5. </ul>

这里的每个菜单项使用了 <a> 标签,而非是按钮,这样用有几点好处:

  • 许多人喜欢使用“右键”->“在新窗口打开”。如果我们使用 <button> 或者 <span>,就不会有这个功能了。

  • 搜索引擎会索引 <a href="..."> 链接。

因此我们选择使用 <a>。 但正常我们会使用 JavaScript 处理点击逻辑,因此我们应该阻止浏览器的默认行为。

代码如下:

  1. menu.onclick = function (event) {
  2. if (event.target.tagName === 'A') {
  3. let href = event.target.getAttribute('href');
  4. alert(href);
  5. return false; // 阻止浏览器默认行为(不跳转)
  6. }
  7. };

如果忽略使用 return false,那么在我们的代码执行结束后,浏览器“默认行为”仍会执行——跳转到 href 中指定的 URL。

顺便提一下,使用事件委托能让我们的菜单组件更加灵活。我们可以添加嵌套列表,并且使用 CSS 做出“下滑”效果。

阻止进一步事件的发生

某些事件发生之后会接着触发另一个事件。如果我们阻止了前面事件的发生,也就不会有进一步触发另一个事件的情况发生。

例如,在 <input> 上发生 mousedown 事件后,会导致该输入框被聚焦,接着触发该输入框的 focus 事件。如果我们阻止了 mousedown 事件,进一步的 focus 事件就不会发生。

查看下面的网页代码:

  1. <input value="点击我" onfocus="this.value=''">
  2. <input onmousedown="return false" onfocus="this.value=''" value="点击我">

我们点击第一个输入框,focus 事件发生,这是正常情况。

点击第二个输入框,就不会发生 focus 事件,因为 mousedown 的默认行为被阻止了。当然,我们还可以使用另外一种方式聚焦输入框,例如,无需鼠标点击,连续按下 Tab 键,就可以从第一个输入框切换聚焦到第二个输入框。

event.defaultPrevented

如果某个事件的浏览器默认行为被阻止,那么对应 event.defaultPrevented 属性值即为 true,否则为 false

有一个有趣的用例。

还记得《冒泡和捕获》一章里讨论的 event.stopPropagation() 吗;以及我们为什么说除非必要,否则无需阻止冒泡?

有时我们可以使用 event.defaultPrevented

我们来看一个实例,看起来在这里阻止冒泡很合适,但实际上不用也可以。

默认浏览器的 contextmenu 事件(鼠标右键)会显示一个包含标准菜单选项的上下文菜单,我们可以阻止这个行为显示自定义菜单,类似这样:

  1. <button>右键点击,显示上下文菜单</button>
  2. <button oncontextmenu="alert('我们要写自己的菜单了'); return false">
  3. 右键点击,显示我们自定义上下文菜单
  4. </button>

现在,我们实现文档层面的自定义上下文菜单,包含我们自己设定的选项;同样文档中的元素也各有自己的上下文菜单:

  1. <p>右键点击出现默认上下文菜单</p>
  2. <button id="elem">右键点击出现按钮上下文菜单</button>
  3. <script>
  4. elem.oncontextmenu = function (event) {
  5. event.preventDefault();
  6. alert('按钮上下文菜单');
  7. };
  8. document.oncontextmenu = function (event) {
  9. event.preventDefault();
  10. alert('文档上下文菜单');
  11. };
  12. </script>

现在的问题是,当我们点击 elem 时,会弹出两个菜单:按钮级别的和(因为事件冒泡)文档级别的。

怎样修复呢?解决方案之一是:在按钮事件处理器中使用 event.stopPropagation() 阻止事件冒泡:

  1. <p>右键点击出现上下文菜单</p>
  2. <button id="elem">右键点击出现按钮菜单(用 event.stopPropagation 停止事件传播)</button>
  3. <script>
  4. elem.oncontextemnu = function (event) {
  5. event.preventDefault();
  6. event.stopPropagation();
  7. alert('按钮上下文菜单');
  8. };
  9. document.oncontextmenu = function (event) {
  10. event.preventDefault();
  11. alert('文档上下文菜单');
  12. };
  13. </script>

现在按照预期,只显示了按钮菜单。但是这样的代价有点高,我们永远阻止了按钮 contextemnu 事件的向外传播,包括收集统计数据的分析系统,这非常不明智。

一个可选方案是在 document 处理器里检查默认事件行为是否被阻止。如果是阻止了,说明事件已被处理,我们无需对它再做响应。

  1. <p>右键点击出现默认上下文菜单</p>
  2. <button id="elem">右键点击出现按钮上下文菜单(固定 event.defaultPrevented 的值,即 true)</button>
  3. <script>
  4. elem.oncontextmenu = function (event) {
  5. event.preventDefault();
  6. alert('按钮上下文菜单');
  7. };
  8. document.oncontextmenu = function (event) {
  9. if (event.defaultPrevented) { return ; }
  10. event.preventDefault();
  11. alert('文档上下文菜单');
  12. };
  13. </script>

现在一切正常运行。如果我们有嵌套的元素,并且每个元素都有自定义上下文菜单,这也是可行的。只要保证在每个 contextmenu 处理器里检查 event.defaultPrevented 属性值就行。

浏览器默认行为 - 图2event.stopPropagation() 和 event.preventDefault()

我们可以清楚的看到,event.stopPropagation()event.preventDefault()(或者是 return false)是两样不同的东西:前者是阻止事件传播,后者是阻止浏览器默认行为,彼此之间并没有关联。

浏览器默认行为 - 图3嵌套上下文菜单架构

还有其他方法可以实现嵌套的上下文菜单。其中之一是使用一个特殊的全局对象,对象中包含一个处理document.oncontextmenu`` 的方法,以及允许在其中存储各种“低层”处理程序的方法。

该对象将捕获任何右键单击,查看存储的处理程序并运行相应的处理程序。

但是,每个需要上下文菜单的代码都应该知道该对象并使用它而不是自己的 contextmenu`` 处理程序。

总结

存在许多浏览器默认行为:

  • mousedown:开始选择(移动鼠标进行选择)。

  • <input type="checkbox"> 上点击:选中/不选中 input

  • submit:在表单中点击 <input type="submit"> 或者按下 Enter 提交表单。

  • wheel:滚动鼠标产生滚动效果也是默认行为。

  • keydown:按下按键会往表单框里输入一个字符,或者其他行为。

  • contextmenu:鼠标右键点击时触发的事件,会显示浏览器默认的上下文菜单。

  • ……

如果我们想要通过 JavaScript 专门处理事件,则可以阻止所有的事件默认行为。

阻止默认行为,可以使用 event.preventDefault()return false,第二种方式只在 on<事件名> 绑定的事件处理器中有效。

如果事件的默认行为被阻止,那么 event.defaultPrevented 的值就变为 true,否则是 false

浏览器默认行为 - 图4保持语义,不要滥用

技术上讲,通过阻止默认行为和添加 JavaScript,我们可以自定义任何元素的行为。例如,我们可以让链接 像按钮一样工作,让 <button> 表现的像链接(重定向到另一个 URL 等)。

但我们通常应该保持 HTML 元素的语义。 例如,<a> 应该就是用来导航的,而不是作按钮用。

除了“只是一件好事”之外,这也使得你的 HTML 在可访问性方面更加出色。

此外,如果我们考虑使用 <a> 标签的示例,那么请注意:浏览器允许在新窗口中打开此类链接(通过右键单击或其他方式),人们喜欢这样。但是如果我们使用 JavaScript 让按钮表现为链接,甚至使用 CSS 让它看起来就是个链接,但那些特定于 <a> 标签的功能仍然不可用。

练习题

问题

一、为什么“return false”不起作用?

为什么下面代码里的 return false 不起作用?

  1. <script>
  2. function handler() {
  3. alert( '...' );
  4. return false;
  5. }
  6. </script>
  7. <a href="http://w3.org" onclick="handler()">浏览器会跳转到 w3.org 网站</a>

点击上面的链接,浏览器会执行默认的跳转行为,但是我们不想。

怎样去修复呢?

二、捕获元素的所有链接

点击 id=”contents” 元素中所有链接时,都要询问一下是否确认离开,如果用户选择否,就不执行跳转。

效果图:

浏览器默认行为 - 图5

详细:

  • 元素中的 HTML 结构可能是会动态改变的,所以我们不能去查找所有的链接,然后为每个都添加一个事件处理器。请选择使用事件委托。

  • 内容中可能包含有嵌套标签的,比如像 <a href=".."><i>...</i></a>

三、图片画廊

通过单击缩略图更新画廊中显示的大图。

效果图:

浏览器默认行为 - 图6

HTML 结构:

  1. <p><img id="largeImg" src="https://en.js.cx/gallery/img1-lg.jpg" alt="Large image"></p>
  2. <ul id="thumbs">
  3. <li>
  4. <a href="https://en.js.cx/gallery/img2-lg.jpg" title="Image 2"><img src="https://en.js.cx/gallery/img2-thumb.jpg"></a>
  5. </li>
  6. ...
  7. </ul>

答案

一、为什么“return false”不起作用?

当浏览器读取 on* 特性的时候,以 onclick 为例,会使用特性值作为内容创建处理程序。

比如使用 onclick="handler()" 的话,就会创建这样一个处理器函数:

  1. function(event) {
  2. handler() // onclick 特性值内容
  3. }

我们发现,调用 handler() 之后返回的结果并没有使用,所以不会影响默认浏览器行为。

这个修复起来比较简单:

  1. <script>
  2. function handler() {
  3. alert("...");
  4. return false;
  5. }
  6. </script>
  7. <a href="http://w3.org" onclick="return handler()">w3.org</a>

或者可以使用 event.preventDefault(),像这样:

  1. <script>
  2. function handler(event) {
  3. alert("...");
  4. event.preventDefault();
  5. }
  6. </script>
  7. <a href="http://w3.org" onclick="handler(event)">w3.org</a>

二、捕获元素的所有链接

这是事件委托模式的一个很好的应用场景。

我们只需要为 #contents 绑定 onclick 事件,在事件处理器内部判断一下,如果点击的是链接的话,就打开确认框进行询问。

获取链接地址的话,最好是使用 link.getAttribute('href') 而不是 link.href

  1. <fieldset id="contents">
  2. <legend>#contents</legend>
  3. <p>
  4. How about to read <a href="http://wikipedia.org">Wikipedia</a> or visit <a href="http://w3.org"><i>W3.org</i></a> and learn about modern standards?
  5. </p>
  6. </fieldset>
  7. <script>
  8. contents.onclick = function(event) {
  9. function handleLink(href) {
  10. let isLeaving = confirm(`Leave for ${href}?`);
  11. if (!isLeaving) return false;
  12. }
  13. let target = event.target.closest('a');
  14. if (target && contents.contains(target)) {
  15. return handleLink(target.getAttribute('href'));
  16. }
  17. };
  18. </script>

在线例子

三、图片画廊

解决方法是为容器绑定事件处理器,来跟踪发生在容器内部的点击事件。如果点击发生在 链接上,则修改 #largeImg 元素的 src 值为缩略图链接的 href 值。

  1. thumbs.onclick = function(event) {
  2. let thumbnail = event.target.closest('a');
  3. if (!thumbnail) return;
  4. showThumbnail(thumbnail.href, thumbnail.title);
  5. event.preventDefault();
  6. }
  7. function showThumbnail(href, title) {
  8. largeImg.src = href;
  9. largeImg.alt = title;
  10. }

(完)