万字长文总结DOM操做

0、前言

DOM操做是JS中的一大重点,本次回顾DOM操做,总结如文。javascript

尽管是一篇总结,但我尽可能把内容说清,逻辑理顺,我的认为读起来仍是容易接受。对许多细节和注意点进行标注,须要展开的内容,也附上拓展连接。css

但愿能合各位看官的口味,废了好几天,但愿求各位一赞,你看收藏夹那么多灰,不介意一块儿分点吧?刚开始写点博文,但愿给点鼓励。html

配套脑图: html5

一、DOM基本概念

1.一、DOM、DOM节点与DOM树

  • DOM:(Document Object Model, 文档对象模型)是文档内容(HTML或XML)在编程语言上的抽象模型,它建模了文档的内容和结构,并提供给编程语言一套完整的操纵文档的API。

本文面向HTML,不讨论XML等文档,下文中“文档===HTML”。java

  • DOM节点:简称节点(Node),是DOM模型的组成单元。HTML的基本单元是标签,节点经常与标签对应,但连续的文本内容也是一个文本标签
  • DOM树:DOM树是DOM结构的表示形式,DOM把文档的每一个节点根据父子关系链接,造成DOM树。

面试官:如何理解DOM?为何须要DOM?node

1.二、节点、节点类型和节点类

  • 节点:前面提过,节点是DOM树的组成单元。在JS看来,一个节点就是JS对象。下面用node表示任意的节点。git

  • 节点类型:并不是全部的节点都是同样的,DOM规定文档中有12种节点类型,分别用常量1 ~ 12(有与之对应的常量名称Node.XXX_NODE)表示,能够经过node.nodeType属性获取节点的类型常量。web

    如今,有些类型的节点已经弃用了,常见到的只有几种类型的节点,包括:面试

    • 元素节点: 类型常量为Node.ELEMENT_NODE1。最多见的一类节点,对应文档中的元素。大部分DOM操做都是在元素节点层次的。编程

    • 文本节点:类型常量为Node.TEXT_NODE2。对应文档中的文本,任何文档内容都有对应的文本节点,即便空格和换行符

      空格和换行不会对页面内容产生影响,但它们确实以文本节点的形式存在于DOM树中。

    • Document节点:类型常量为Node.DOCUMENT_NODE9。它不对应文档的内容,而是做为文档的入口节点,每一个文档都有且仅有一个入口,由于这种独特性,赋予一个特殊的变量名称document

    • 注释节点:类型常量为Node.COMMENT_NODE8。它对应文档中的注释标签,文档的注释内容也是可读取和修改的。

  • 节点类:DOM内置许多节点类,类之间存在继承关系,造成一套节点类框架。每一个节点对象都属于节点类,拥有该类和其父类的方法与属性,这使得操做节点十分简单。节点类框架的一部分大概如图:

    这些节点上有丰富的属性和方法,是继承的结果,能够看到,一个HTML标签元素至少有四层的继承关系。

    <a>标签为例,它属于HTMLAnchorElement类,得到了a.targeta.download等属性,接着继承了HTMLElement类上的title, hidden等属性和click()等方法,又从Element类继承了tagName, className等属性和getAttribute(), setAttribute()等方法,再从Node类继承了nodeType(前面说过的节点类型), appenChild(), removeChild()等方法,最后从EventTarget类中继承了事件相关的属性和方法。

不要混淆节点类型和节点类这两个概念。前者是一个生活中的类别,后者是编程意义上的。节点对象的nodeType属性表示了它的类型,而节点类是该节点的从属的类。由于Dode是一个抽象类,因此,若是知道了某个节点从属的类,咱们就知道它的节点类型。

区分节点与元素节点。咱们常常关心元素节点(简称元素),由于这是一类最常使用的节点,可是并不是全部节点都是元素。

1.三、探索DOM结构

前面的说法太抽象了,让咱们用实际的例子看看文档、DOM树与节点的关系。

Live DOM Viewer是一个能够根据HTML文档实时查看DOM树的网站。你把下面的例子复制过去,或者本身去探索。

一个简单的HTML文档:

<!DOCTYPE HTML>
<html>
<body>
  A simple text.
  <ol title="this is a title">
    <li>czpcalm</li>
    <!-- comment -->
  </ol>
</body>
</html>
复制代码

它对应的DOM图(颜色区分了节点类型):

留意这个图,你要注意几点:

  • 总共有4种类型的节点,分别是标签节点(红色),文本节点(灰色),注释节点(黄色)和DOCTYPE节点(紫色)。

  • 文档没有<head>标签却出现了HEAD节点。这是由于HTML必然存在<html><head><body>标签,不存在时会自动补上。顺便一提,当出现<table>标签时,也必定会有<tbody>标签。

  • 文档中的文本都会造成文本节点的内容,包括空格␣换行↵。第一,单独的空格和换行都会造成对应的文本节点;第二,有内容的文本节点的值包含前导和后继的空白。

    不是说HTML中的空白字符都被忽略吗?怎么这里又说全都是有效的字符?

    在从文档解析生成DOM树的过程当中,HTML中的任何字符都是有效的;不过,在接下去的页面渲染的过程当中,空白内容被忽略。因此从文档到页面的整过过程当中,空白确实被忽略了。

  • 理解DOM树中的父子关系对应文档中的包含(嵌套)关系。一个极佳的类比是文件树,把元素看作文件夹,文本看作文件,文件夹中能够存放文件和新的文件夹,而后一层层深刻下去,DOM树也是如此。

几个原则能够帮咱们快速理解这个DOM树的构建:

  • 自动补全, 上面提到的自动添加必要元素,另外,还会自动补齐缺乏的关闭标签。
  • 文档有的DOM树都有。这个原则要求空白也会有。不过,做为补充的,<head>前面的空白会被忽略(历史缘由)。
  • <body>后若是有内容,会被移到<body>里面。

二、导航与搜索

操做节点前,先要找到节点。导航是从一个节点到另外一个节点;搜索是从一个范围中选出知足条件的节点。

2.一、节点导航

Node的类规定了节点具备的许多属性,方便咱们从某个节点中找到跟它相关的另外节点。

顶级节点通常直接获取:

  • document -- 入口节点。
  • document.documentElement -- HTML节点。
  • document.head -- head节点。
  • document.body -- body节点。

node节点,有如下属性:

  • node.parentNode -- 获取节点的父节点。

  • node.previousSibling -- 获取节点的上一个兄弟节点。

  • node.nextSibling -- 获取节点的下一个兄弟节点。

  • node.childNodes -- 获取节点的孩子节点列表。没有子节点返回空列表。

  • node.firstChildnode.lastChild -- 获取第一个和最后一个孩子节点,同node.childNodes[0]node.childNodes[node.childNodes.length-1]

助记:都是两个单词拼写的。

这些是属性,不是方法。不要错误使用bode.childNodes()之类的。

以上导航是基于节点的,包括元素节点、文本节点、注释等。如1.3中的例子,document.body.firstChild获得的是A simple text所在的文本节点。

由于咱们常常只关心元素节点,DOM也提供了一组纯元素的导航属性, 对元素elem或者节点node, 有:

  • node.parentElement -- 父元素节点,该属性来自Node类。
  • elem.previousElementSibling -- 上一个兄弟元素
  • elem.nextElementSibling -- 下一个兄弟元素
  • elem.children -- 孩子元素列表
  • elem.firstElementChildelem.lastElementChild,第一个和最后一个孩子元素节点

助记:children特殊,其它都有Element,没有Node。

解惑:node.parentNodenode.parentElement有区别吗?父节点不该该都是元素节点吗?通常状况下,两者等效,但html.parentNode === document, document不是元素节点。

提醒:上面的导航属性都是只读属性。elem.parentElement = anotherElem; 是错误的

2.二、搜索节点(重点)

常常地,咱们老是直接从文档中找出知足某些条件的元素,从而获取到目标元素。

2.2.一、document/element.getElementBy*()系列

getElementBy*系列方法大家确定不陌生,但我想要提醒的是,注意个人写法document/element.getElementBy*(),表示这是两个不一样的类上的方法(参看上面节点类)。

首先,是两个来自Document类的方法:

  • document.getElementById(id) : 根据id获取文档中的元素。
  • document.getElementsByName(name): 根据name获取文档中的元素。不多使用。

其次,是Document类和Element类都具备的方法:

  • document/element.getElementsByTagName(tagName): 根据标签名称获取文档或某个元素内的元素。
  • document/element.getElementByClassName(className):根据类名获取文档或某个元素内的元素。

注意:没有element.getElementById(id)

注意:不要忘记或多加了s。除了document.getElementById(),其它方法的返回结果都是一个集合,没有知足条件的元素则是空集合。

2.2.二、document/element.querySelectorAll/querySelector()

如今,推荐使用一组更强大的搜索新方法, 它们支持CSS选择器:

  • document/element.querySelectorAll(CSSSelector):返回知足选择器的一组节点列表。
  • document/element.querySelector(CSSSelector): 返回第一个知足选择器的元素。

补充:elem.matches(selector)能够检查某个元素是否与选择器匹配。

2.2.三、两组搜索方法的对比

querySelector*源自Selectors API 规范,与CSS选择器结合,更加灵活,强大,已被全部现代浏览器支持。是如今比较推荐的作法。

getElementBy*()源自DOM2标准,被认为是传统接口,老项目中普遍使用。或者须要兼容IE8以前使用。可是有较好的性能,如今仍然有人在用。

补充:做为结果的集合。在以集合返回结果的时候,或者返回一个NodeList对象,或者返回一个HTMLCollection对象,通常来讲,两者都是可迭代的类数组对象,能够经过下标范围或for...of遍历。

注意:大部分结果集,包括getElementBy*node.childNode等,都是动态的,即便查询并保存,以后若是修改文档,该结果集也会随之变化。但querySelectorAll()返回的结果是静态的。

2.2.四、特殊的搜索方法

有一些特殊的元素搜索方法,就我所知有:

  • elem.closest(selector) -- 在elemd的父元素上查找最近一个知足选择器的元素。
  • document.elementFromPoint(clientX,clientY) -- 返回相对当前视口坐标嵌套最深的(最上方)的元素。

三、节点操做

这一节涉及对某个节点的操做,通常不会引发文档内容(DOM树)的变化。

3.一、通用节点操做

这些操做基于**Node**接口,对全部节点都是通用的。

  • 判断节点类型node.nodeTypenode instanceof <NodeClass>。二者均可以用于判断节点类型,当须要明确的节点类的时候,只能经过后者。

    node.nodeType === Node.ELEMENT_NODE; //或node.nodeType === 1;
    node instanceof Element;       //与上面等效
    node instanceof HTMLInputElement;   //判断是不是输入元素
    复制代码
  • 获取节点名称:node.nodeName,对于元素节点,返回对应的标签名称,如audio。对其它类型节点,返回#与节点类型字符串,如#text, #comment,#document。也能经过节点名称判断节点类型,但基本不用。

  • 获取或设置节点值node.nodeValue文本节点或注释节点返回文本内容,元素节点与document节点返回null。读写属性,支持node.nodeValue = "A simple text"。一样的,空白文本也被包含在内容里。

    提醒:文本节点和注释节点有一个data属性,使用与nodeValue相同,但它不是在Node接口上的。

  • 判断节点是否拥有子节点:node.hasChildNodes(),返回true当节点有子节点时。

  • 判断节点是否拥有特定子节点:node.contains(childNode),返回true当childNode是node的子节点。

3.二、元素节点操做(重点)

大部分状况下咱们都是在元素节点上操做它的文本子节点,因此元素是咱们最关心的节点,**Element**接口提供了更多的属性和方法。这里只考虑HTML元素。

解惑:那何时须要操做文本节点?会看1.3节的例子,A simple text的父节点是body,但body有其它元素节点。假如没有文本这种类型的节点,很难在body节点上只修改A simple text的内容。

  • 判断元素类型:elem.tagNameelem.nodeName,效果同样,返回标签的字符串,如audio。另外,使用instanceof能够实现不一样级别的类型判断。

  • 元素内容:有几个属性和元素内容相关:

    • elem.innerHTML -- 获取或设置元素内的HTML片断。设置的内容会被当成HTML片断解析,可能会引发文档结构的变化。

      注意:HTML片断内的脚本不会执行。

    • elem.textContent -- 获取或设置元素的文本内容(标签被忽略)。设置的文本以安全模式(不会被解析)写入。

    使用区别请参看实验探究innerHTML,innerText,textContent的使用区别

3.2.一、元素的特性和属性

特性(attribute)是指html中写在标签内的特性,而属性(property)是只元素节点做为编程对象具备的属性。

  • 特性 -- 属性同步机制:对标准规定的特性,元素对象具备响应的读写属性, 如a.href。这种机制极大的方便了在JS中获取或修改元素的特性。

    提醒:对不一样HTML元素,规定的特性不一样,属性也就不一样,如: 存在a.href但不存在div.href

  • 通用的特性操做接口:

    • elem.hasAttribute(name) -- 检查是否存在某个特性。
    • elem.getAttribute(name) -- 获取某一特性的值。
    • elem.setAttribute(name, value) -- 设置某一特性。
    • elem.removeAttribute(name) -- 删除某一特性。
    • elem.attributes() -- 获取全部的特性对,每一个特性对具备name,value属性。
  • 特殊的data-*data-*特性是一种合法且安全的传递自定义数据的方式。可经过elem.dataset.name读取或修改data特性的值。属性名称采用驼峰写法,如elem上的data-apple-price对应elem.dataset.applePrice

3.2.二、元素的类和样式

修改样式有两种方式,一是把样式写到某个类里,而后在代码中修改元素的类,一是直接修改elem.style.*。前者适用于随状态改变样式的状况,在代码可维护性上更加,用得较多。后者适用于频繁计算或切换的样式。

  • elem.classList: 一个包含elem全部类的可迭代的类数组对象。这个对象有几个方法,方便咱们改变元素的类。
    • elem.classList.contains(class) -- 检查是否有某个类。
    • elem.classList.add(class) -- 添加某个类。
    • elem.classList.remove(class) -- 移除某个类。
    • elem.classList.toggle(class) -- 切换某个类,若是有就删除,没有就添加。
  • elem.className: 一个读写属性,把元素的class特性当成一个总体看待。如HTML中某个元素elem存在特性class="first red",那么elem.className的值为first red

区分:经过elem.classListelem.className均可以对元素的类进行改动,前者更加灵活,且具备相应的方法,后者是一个总体的字符串属性,适合删除全部的类从新设置。

若是须要直接设定元素的样式,能够设置elem.style.*

elem.style.width = "20px";     //设置元素宽度
elem.style.backgroundColor = "red";     //设置颜色
复制代码

提醒:多词CSS属性的转化方式也是驼峰写法,如padding-left对应elem.style.paddingLeft-webkit-border-radius对应elem.style.WebkitBorderRadius-意味着大写。

补充:常用node.hidden = true实现节点隐藏,也能用于隐藏文本,在元素上,它的效果与elem.style.display:none一致。

这种方式其实是经过元素上的style特性实现的,它的优先级高于通常的CSS,因此都是有效的,除非你在其它地方用了!importantelem.style.*也是可读取的,**可是它们只会读取元素中style特性存在的属性,**对其它CSS是无效的。但咱们每每须要的是最终应用在元素的样式数值。这种时候,须要使用

  • getComputedStyle(elem) :获取元素最终应用的样式,它返回一个样式对象,好比,可经过getComputedStyle(elem).backgroundColor获取颜色值。

注意:getComputedStyle不支持简写属性,如getComputedStyle(elem).padding是无效的。

<style> div { font-size: 20px; } </style>
<div id="div" style="padding:20px;">
    czpcalm
</div>
<script> console.log(div.style.fontSize); //"" style特性上没有该CSS属性,因此为空 console.log(div.style.padding); //20px console.log(getComputedStyle(div).fontSize); //20px </script>
复制代码

若是须要清除代码设定的样式, 能够把elem.style.cssAttr设为""(空字符串),反作用是原来html中的特性样式也会被清除。

还有一种基本不会使用的重写整个元素style特性的方式elem.cssText = csstext,csstext不包含选择器和花括号。

3.2.三、元素的位置和尺寸

当设计元素的大小变化或位置移动时,咱们须要获取元素的位置或尺寸。设置则用CSS方式设置。

位置是相对于参照物的,一个元素,有相对于定位父元素相对于视口相对于文档三种关系位置。

提醒:定位父元素是指CSS定位元素(position为relative,absolute,fixed)或td,th,table元素,或者是body元素。

  • 相对于定位父元素:elem.offsetLeft/offsetTop ,相对于参照父节点的左/上边距。elem.offsetParent获取元素的定位父元素。

  • 相对于视口:**elem.getBoundingClientRect()**获取元素的定位矩形elemRect

    • elem.getBoundingClientRect().left/top/right/bottom分别表示元素盒子(含边框)四角到视口左或上边的距离。
    • elem.getBoundingClientRect().width/height, 与elem.offsetWidth/offsetHeight等效,盒子的宽高。
    • elem.getBoundingClientRect().x/y ,通常状况下,与elem.getBoundingClientRect().left/top等效,且不兼容IE,不推荐使用。
  • 相对于文档:没有直接获取的方式,但能够经过相对于视口+滚动距离简单计算。

    • 盒子上方相对于到文档的距离:elem.scrollTop + elem.getBoundingClientRect().top
    • 盒子左边到文档的距离:elem.scrollLeft + elem.getBoundingClientRect().left

元素盒子的尺寸也有多种状况,须要考虑边框、内边距、是否为标准盒子模型、甚至是否有滚动条。

  • 含边框的盒子尺寸:elem.offsetWidth/offsetHeight或者elem.getBoundingClientRect().width/height均可以获取含边框的宽高。

  • 边框宽度:elem.clientLeft/clientTop -- 元素左上角边框的宽度。能够理解为左边框宽度和上边框的宽度,若是存在滚动条,也包含滚动条的宽度。

    补充:通常来讲,上边框和左边框是经常使用的。若是四条边框宽度不一,能够经过getComputedStyle(elem).borderRight获取,注意这是含单位的字符串。

  • elem.clientWidth/clientHeight -- 内容宽度高度,包含padding,不包含滚动条。

  • 不含边框:elem.clientWidth/clientHeight,获取元素盒子内容宽高,不含边框和滚动条,含padding。

  • 内边距问题与盒子类型:在涉及内边距的时候,须要getComputedStyle(elem)方法获取,而且须要考虑是否为标准盒子。

若是元素的内容存在滚动时,咱们可能须要知道与滚动相关的尺寸:

  • elem.scrollLeft/scrollTop -- 水平和垂直方向上滚动的长度。可写属性,经过设置该值改变控制内容滚动。

  • elem.scrollWidth/scrollHeight -- 元素内容的长度和宽度,包括滚动的内容。

    区分:在没有内容溢出发生滚动时,clientWidth/HeightscrollWidth/Height等效;存在滚动时,前者是盒子的可视内容大小,后者是内容的大小,包括须要滚动查看的部分。

关于位置和尺寸,没有图片难以说清,不理解请参看:

四、修改文档(重点)

DOM操做中,常常须要修改文档结果或内容。这类操做涉及节点的插入、移除、替换等。

文档的操做有一套普遍使用的传统方法。也有一套新推出的API方法,它们更加灵活易用,但IE不兼容。

4.一、插入节点

插入节点能够分三步走:

  1. 建立一个节点:

    • 建立一个元素节点: let elem = document.createElement(tagName)
    • 建立一个文本节点:·let text = document.createTextNode(data)
    • 从已有节点克隆:let dupNode = node.cloneNode(deep), deep为true表示深拷贝,经常使用。默认为false。
  2. 编辑节点的属性和内容

  3. 把节点插入文档树中

    • 传统方式:传统方式须要在父节点上执行对节点的插入

      • parentNode.appendChild(node) -- 把node做为最后一个子节点插入。
      • parentNode.insertBefore(node, nextSibling) -- 在nextSibling以前插入node。
    • 现代方式:能够实现多位置插入,你能够在父节点上执行插入或在兄弟节点上执行插入。

      • parentNode.prepend(...nodes or strings)。在第一个子节点以前插入。
      • parentNode.append(...nodes or strings)。在最后一个子节点以后插入。
      • nextSibling.before(...nodes or strings)。在本节点以前同级插入。
      • previousSibling.after(...nodes or strings)。在本节点以后同级插入。

      提醒:参数的形式说明它们支持一次插入多个,而且字符串会做为文本节点插入。

有一种状况,咱们但愿直接描述节点的插入HTML代码段,这种时候,可使用以前的elem.innerHTML属性, 或者使用**elem.insertAdjacentHTML(position, html)**进行插入。其中,position的可选值有:"beforebegin", "afterbegin", "beforeend", afterend.

区分:节点对象存在不一样于节点在文档树中。前者只是在代码中能够经过node等变量名称引用节点,但建立一个节点,并不会对文档树结构有影响,只有插入文档树后,节点才成为文档的一部分。反之,从文档中移除节点,并不会致使节点变量实效,它仍是存在,能够继续被修改,并在某个时间点从新插入文档中。

4.二、移除节点

从文档树中移除更加简单。

  1. 找到须要移除的节点。参考节点导航与搜索小节。
  2. 移除节点:node.remove(),移除节点。但IE不兼容,须要使用传统方式,获取其父节点,在父节点上移除子节点:node.parentNode.removeChild(node)

4.三、替换节点

与移除相似,使用node.repalceWith(...nodes or strings),一样IE使用parentNode.replaceChild(newNode, node)

五、事件处理

5.一、基本概念

  • 事件是某事发生的信号, 全部的DOM对象都具备这些信号。
  • 事件处理程序是当一个事件信号发生时运行的函数,用于对事件做出响应。
  • 事件处理就是为事件分配正确的处理函数,在事件发生时做出正确处理。
  • 事件类型 是事件的分类,常见的事件类型有
    • 鼠标事件:click, contextmenu, dbclick, mousedown, mouseup,mouseover, mouseout等。
    • 键盘类型:keydown, keyup
    • 焦点事件:focus, blur, focusin, focusout,
    • 表单事件: submit, reset
    • 剪切板事件: cut, copy, paste
    • 资源事件:load, unload, error, abort
  • 事件对象是事件在编程上的对象,具备不少和事件相关的属性。经常在处理函数调用时自动做为参数传入。

5.二、事件流

**假设一个div元素中放了一个button元素,若是点击了button,算不算点击了div?若是算,是先点击了button仍是先点击了div?**由于元素的嵌套关系,使得事件的产生对象每每是嵌套的,而不是独立的,在上面的中,还有body元素,html元素,甚至整个文档document。

第一个问题,对大多数的事件来讲,应该是确定的,一个在子元素上发生的事件,也看作在该元素上发生了。

第二个问题,也被当作事件流问题,即如何肯定事件在节点中的传播顺序。巧的是,当年IE和Netscape分别提出了事件冒泡事件捕获这两个几乎彻底相反的概念。

  • 事件冒泡:事件从最小的发生对象开始依次往外围对象冒泡。即button->div->body->html->document
  • 事件捕获:事件从整个文档开始依次向最小目标捕获。即document->html->body->div->button

为了统一这两种观点,DOM事件规范提出了3阶段的事件流:事件捕获阶段->目标阶段->事件冒泡阶段

然而,事件捕获阶段不多被使用,下面的讨论会忽略事件捕获,默认事件从目标上开始冒泡。

注意:并不是全部事件都会冒泡。

5.三、事件处理函数

5.3.一、3种事件处理方式

有3种为设置事件处理函数的方式:

  1. HTML特性处理:在HTML元素标签中使用on<event>特性。
  2. DOM0级事件处理:把处理函数赋给节点的对象的on<event>属性。
  3. DOM2级事件处理:使用node.addEventListener(event, handler, capture)removeEventListener(event, handler)capture是一个布尔值,表示是否在捕获阶段响应。

对比:HTML特性的方式具备很大的局限性,代码量限制,维护性差,某些事件不支持等。DOM0级事件也对某些事件不支持,关键在于没法为一个事件类型分配多个处理函数。DOM2解决了这些问题,是最通用的方式,推荐使用。

<div id="div1" onclick="alert(event.type+': div1')">div1</div>
<div id="div2" onclick="handler('div2',event)">div2</div>
<div id="div3">div3</div>
<div id="div4">div4</div>
<script> function handler(message, event) { alert(event.type + ": " + message); } div3.onclick = e => handler("div3", e); //处理函数具备多参数,须要嵌套调用。 div4.addEventListener("click", e => handler("div4", e), false); </script>
复制代码

上面的例子中, div1和div2使用了HTML特性添加事件处理,div3使用DOM0级事件处理,div4使用DOM2级事件处理。

HTML特性的内容会成为事件处理函数的内容,DOM事件处理赋值的是函数对象。好比对一个处理函数func,特性上的写法是onclick="func()",而DOM0级事件处理的写法是node.onclick=func。区分它们的主要点在于理解特性的内容会被套上一个函数外壳,而后成为处理函数。onclick="func()"最后的效果是:

node.onclick = function(event){   //传递的事件参数名为event
    func();         //这一行是来自特性
}
复制代码

这样,你应该能明白为何特性上是函数调用,而DOM上是函数对象赋值了。

由于这种设定,还产生两个要注意的点。

第一是事件对象参数event,若是你习惯了在DOM事件处理中使用变量名称e而不是event,那么在特性内容上可能写出相似e.type的表达,这是错误的,由于特性传入的事件变量名是event

第二,在试图阻止事件的默认响应的时候,可能会写出这样的代码:

<a href="www.baidu.com" onclick="handler()">百度</a>
<script> function handler(){ /*....处理点击事件*/ return false; //返回false阻止跳转 } </script>
复制代码

然而点击以后仍是发生跳转了,问题出在哪里?让咱们看一下最后生成的处理函数:

a.onclick = function(event){
    handler();
}
复制代码

事件处理函数只是调用了handlerhandler返回了false,而事件处理函数并无理会,因此,外部看来,事件处理函数没有返回值。正确的写法应该是onclick="return handler()"或者onclick="handler();return false;"

DOM2级事件处理以前,一个事件只能对应一个事件处理函数。若是你想解除事件处理函数,能够简单的采用node.on<event>=null的方式。

DOM2node.addEventListener()支持添加多个处理函数,这些处理函数的执行顺序与添加顺序一致,可使用node.removeEventListener(event, handler)解除处理函数。

注意:removeEventListener(handler)必须使用添加时的同一个函数,而不是具备相同执行体的函数。

node.addEventListener("click", ()=>alert("直接添加函数表达式将没法别移除!"), false);
let handler = ()=>alert("something")
node.addEventListener("click", handler, false);
node.removeEventListerner("click", ()=>alert("somthing"));    //无效,传入的是不一样的函数对象
node.removeEventListener("click", handler);         //成功移除
复制代码

5.3.二、事件处理函数的参数

让咱们看另外一个问题,事件处理函数的参数问题。

咱们只编写事件处理函数,函数在事件发生时自动被调用,它的调用语句相似这样

node.handler(event);   //event在这里是实参,这句语句是函数调用
复制代码

咱们要根据调用规范本身的handler函数。

调用时传入了一个参数,根据JS函数参数的特色,若是在事件处理函数中没有使用到event,那处理函数能够不声明这个参数。其次,形参可使用任何合法变量名,咱们能够不函数定义写成handler(e),而后在函数内使用e而不是event

它的调用方式决定了handler最多只能有一个参数。但在事件处理时确实有须要多个参数的时候,这种时候要使用嵌套的函数调用,如上面的例子。

最后,注意这种调用方式使得函数内部this === node。这颇有用,方便咱们在事件处理的时候获取到事件发生对象的信息。

5.四、事件对象

事件对象event在调用事件处理函数时自动传入,它具备不少属性和方法,在事件处理的时候大有用处。

  • event.preventDefault() -- 取消事件的默认行为,只有event.cancelable为true时才有效。

    补充:若是使用on<event>方式添加处理函数,在函数返回false也能够取消默认事件。

  • event.stopPropagation() -- 中止事件的继续冒泡,上层的事件响应不会再发生。

  • event.stopImmediatePropagation() -- 中止事件继续响应。包括事件冒泡和当前目标的其它处理函数也不会发生。

  • event.type -- 事件类型字符串,在使用一个处理函数处理多类事件时,能够判断当前发生的事件类型。

  • **event.targetevent.currentTarget **-- 事件的最小目标和事件的当前目标。

    区分:事件在最小目标上发生,而后往上冒泡的过程当中,event.target始终不变,指向最小目标,但event.currentTarget === this,会随着冒泡过程指向当前正在处理的节点。

  • event.phase -- 事件流阶段,整数,1表明捕获,2表明目标阶段,3表明冒泡阶段。

  • event.bubbles -- 布尔值,事件是否冒泡。

  • event.cancelable -- 布尔值,事件是否可取消默认行为。

  • event.trusted -- 布尔值,若是事件是浏览器发生的,为true,若是事件是js代码发生的,为false。

上面的属性都是只读的。

这些属性和方法是通用的,对具体的事件类型,有更多的事件属性和方法,将在每种事件中详细说明。

5.五、鼠标事件

鼠标事件是最多见的一类事件,有:

  • click -- 鼠标左键点击触发,或触摸屏的点击。

    提醒:点击一次的含义是鼠标在目标上按下并松起,若是鼠标按下后滑动到元素外部松起,或者元素位置变化致使鼠标松起时再也不元素上方,不能造成有效点击。

  • contextmenu -- 鼠标右键点击事件,该事件浏览器通常有默认的菜单,若是须要实现自定义菜单,须要阻止默认行为。

  • dbclick -- 双击鼠标左键。双击具备选择文本的默认行为。

  • mousedown/mouseup -- 鼠标任意键按下/松起。

  • mouseenter/mouseleave -- 鼠标进入/离开元素,不会冒泡。

  • mouseover/mouseout -- 鼠标进入/离开元素,会冒泡,进入/离开子元素时也会触发。

    区分:mouseenter/mouseleavemouseover/mouseout事件相似,可是后者会冒泡,且在进入子元素会触发mouseout

  • mousemove -- 鼠标按下后松起前发送移动。

鼠标点击时,会发生一系列事件,它们具备特定的顺序,以某次双击为例,依次触发事件mousedown->mouseup->click->mousedown->mouseup->click->dbclick

鼠标事件有一些适合获取事件相关信息的属性:

  • 鼠标按键:按键属性只对mousedown/mouseup有意义,event.button,数字1~5,表明鼠标上的按键,依次是:鼠标左键,中键,右键,前进,后退。

  • 坐标:它们是事件发生时刻(定点类)的坐标,或者实时的(mousemove)坐标。

    • event.pageX/pageY -- 相对于文档的坐标。
    • event.clientX/clientY -- 相对于窗口的坐标。
    • event.screenX/screenY -- 相对于屏幕的坐标,较少使用。
  • 组合键:在鼠标事件发生时,若是下列按键被按下,对应的属性为true。用于在一个事件类型上绑定多种任务。

    • event.shiftKey -- shift键是否被按下。
    • event.ctrlKey -- ctrl键是否被按下。
    • event.altKey -- alt键是否被按下。
    • event.mateKey -- cmd键(Mac专用)是否被按下。

    提醒:若是想处理ctrl键,应该注意在Mac下使用cmd键,因此应该判断if(event.ctrlKey||event.cmdKey)

  • 相关目标:event.relatedTargetmouseenter/mouseleavemouseover/mouseout事件的属性。若是鼠标从divA->divB,在divA上发生mouseout(mouseleave),event.target === divA而且event.relatedTarget === divB。相反,在divB上发生mouseover(mouseenter),event.target ===divBevent.relatedTarget === divA

这是一篇鼠标拖放事件的文章,应该能对鼠标事件的使用有所帮助。

5.六、键盘事件

键盘事件经常被用于建立各类热键。如今普遍使用的有两类键盘事件:

  • keydown -- 按下任意键盘触发。持续按住按键会持续触发该事件。
  • keyup -- 松开任意按键。

补充:不要试图监听Fn键,它是在比OS更低的级别上实现的,没有键盘事件。

补充:有一个被取消的keypress事件,这里不讨论。

键盘事件最重要的属性是键码,咱们经常须要获取键盘的键码,根据按下的键盘作出响应。

  • event.code -- 键码。键码是惟一的。在按键判断时,常用的属性。键码是字符串,常见规则:

    • 数字键码为Digit<num>Numpad<num>(小键盘)。如Digit2, Numpad2
    • 字符按键Key<letter>, 如KeyZ, KeyA
    • 功能键通常为按键名称,如F4, Tab,Enter
    • 使用Left/Right区分左右,左shift的键码为ShiftLeft, 右shift的键码为ShiftRight

    补充:一个被废弃的event.keyCodeevent.code有一样的功能,但键码是基于数字的。可能在兼容IE时须要用到。

  • event.key -- 键。表征按键的含义而不是位置。跟是否按下shift、键盘语言有关。如按下z键时表示字符z,而shift+z表示字符Z。左右shift的key也同样。只在使用基于意义的时候才使用event.key判断。

键盘事件也一样支持组合键,可使用event.ctrlKey, event.shiftKey, event.altKey, event.metaKey获取其它按键是否被按下,而不用单独监听它们。

5.七、资源事件

有一类与资源相关的事件,认识它们的最好方式是经过页面生命周期:

  • DOMContentLoaded -- 文档被加载而且DOM树构建完成后(图片、样式可能还未加载)。
  • load -- 页面内容加载完毕,全部图片、样式资源都已应用。此时进行的操做都是安全的。
  • beforeunload -- 用户正打算离开,能够在此时讯问用户是否离开,或者保存一些数据。
  • unload -- 用户已经离开,但仍能够进行少许操做,如释放资源,发送数据。

load事件是这里最重要的一个事件。它不只能够用于确保页面加载完成后执行任务,还能够添加到img, script, style这些节点中,在它们完成资源加载时候进行事件处理。

六、番外篇:表单操做

6.一、表单导航

表单是一类特殊的元素,须要频繁的获取或修改,因此,通用的节点获取方式上,增长了额外的属性,方便表单操做。

  • document.forms -- 获取文档中的表单元素集合。这是一个命名集合。命名意味着能够经过表单名称方式document.forms.formName获取表单,而集合自己又支持下标方式document.form[0]

  • form.elements -- 获取表单form的输入元素集合,一样,这是一个动态的命名集合。能够经过form.elements.inputName或者form.elements[index/inputName]获取。

  • form.inputNameform[index/inputName] -- 获取表单form的输入元素。form.elements.inputName的简写。

    注意:

    (1)radio、checkbox等多个输入共享一个name时,form.elements.inputNameform.inputName返回一个集合。

    (2)没法获取到type为image的输入组件。

    (3)若是表单中有<fieldset>(输入组),它会成为一个form.elements的一个元素,可进一步经过fieldset.elements获取表单组中的输入控件集合。

  • input.formName -- 获取输入元素所在的表单。form.inputName的反向引用。

<form name="myForm">
  	<input type="text" name="user"/>
      <input type="radio" name="sex" value="male"/>
      <input type="radio" name="sex" value="female"/>
  </form>
  <script> let form = document.forms.myForm; //或者document.forms[0] let userInput = form.user; //或者form.elements.user let sexInput = form.sex; sexInput[0].checked = true; //选择第一个radio console.log(sexInput.value); //male </script>
复制代码

6.二、表单基础

根据属性--特性同步,表单对象有如下属性,它们与对应的特性意义相同。

  • form.action
  • form.method
  • form.name
  • form.target

另外,表单有两个经常使用的方法:

  • form.submit() -- 手动提交表单
  • form.reset() -- 初始化表单内容

一个表单含有多个输入字段,它们能够是各种型的input标签select标签textarea标签button标签。每这些表单字段有一些通用的属性(固然还有其它的同步属性):

  • input.value -- 字段的值。对type="file"的input,它是只读的,表示计算机上的文件地址。
  • input.name -- 字段名称。
  • input.type -- 字段类型,对textarea"textarea"。对select"select-one""select-multiple"
  • input.tabindex -- 字段的tab索引。
  • input.disabled -- 字段是否被禁用。
  • input.readOnly -- 字段是否只可读。

这些属性都是可写的,大大便利了咱们对表单元素的操做。

最关键,要数input.value,对不一样字段,它有不一样的用法:

  • button, reset, submit类型:表示用于显示的字符串。
  • radio, select(单选)类型:一个与被选中字段value相同的值。
  • textboxselect(多选)类型:一个数组,对应被勾选的选线的值。
  • 其它类型:与填写的值一致。

6.三、表单事件

有些事件是表单特有的:

  • ·submit -- 表单提交发生的事件。
  • reset -- 点击reset按钮时。
  • input -- 在字段输入任意内容后。
  • change -- 字段失去焦点后,与以前输入发生变化时。

此外,也有一些事件在表单上发挥了主要做用:

  • pressdown

  • focus/blur -- 得到或焦点时,不会冒泡。

  • focusin/focusout -- 得到或失去焦点时,与上相似,可是会冒泡。

  • cut/copy/paste -- 剪切板事件

    补充:能够经过e.preventDefault()(或返回false)取消剪切板事件默认行为,来禁用页面/输入的剪切板功能。event.clipboardData可用于读写剪切板内容。

    input.addEventListener("past", (event)=>{
        let data = e.clipboardData;
        alert("Data in clipboard: " + data+". But you can't paste!");
        return false;
    })
    复制代码

6.四、表单验证与表单提交

表单客户端验证几乎成为表单最重要的一个功能,这但是JS诞生的原因呀!

html5提供了一套表单的验证API,但我的认为难用且界面依赖浏览器,使用很少,更可能是自定义表单的验证方式。

表单验证主要有两种思路,一是在客户完成每一个字段输入时,独立验证,每一个字段,全部字段都经过后才容许提交操做。这种方式主要利用change, input等事件。

一是在用户发起提交操做时,对整个表单进行验证,若是不经过则提醒用户更正,阻止表单提交,直到全部字段正确。这种方式主要使用submit事件。

七、后记

累了,贴个图总结吧。

花了我好几天,一边学习,一边整理。感谢阅读!

相关文章
相关标签/搜索