为啥要使用节点操作
主要是根据节点之间的关系来更方便的获取元素对象.
例如: 之前写过的点击计数示例, 前面的做法是使用 querySelector 获取两个 input 标签, 再进行操作.
但是两个 input 标签其实是兄弟关系, 咱们可以借助节点操作, 更方便的获取到 input 的元素对象.
<input type="text" id="text" value="0"><input type="button" id="btn" value='点我+1'><script> var text = document.querySelector('#text'); var btn = document.querySelector('#btn'); btn.onclick = function () { var num = +text.value; console.log(num); num++; text.value = num; }script>核心关系: 父子, 兄弟.
页面中的一切内容都是节点
文本, 元素, 属性等都是节点.
每个节点对象主要有三个属性:
节点名称 (nodeName)
节点类型 (nodeType): 元素节点为 1, 属性节点为 2, 文本节点为 3(文字, 空格, 换行都算文本节点) , 开发中主要操作元素节点.
节点值 (nodeValue)
<div class="box">div><script> var div = document.querySelector('.box'); console.dir(div);script>
获取父节点
体会 HTML 中的父子关系
<html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>父子关系title>head><body> <div class="grandfather"> <div class="father"> <div class="son">div> div> div>body>html>在这个 html 中:
html 是 head 和 body 的父节点.
head 是 meta, title 的父节点.
body 是 .grandfather 的父节点.
.grandfather 是 .father 的父节点.
.father 是 .son 的父节点.
使用 parentNode 属性获取到该节点的父节点.
<div class="grandfather"> <div class="father"> <div class="son">div> div>div><script> var son = document.querySelector('.son'); console.log(son); console.log(son.parentNode); console.log(son.parentNode.parentNode);script>获取所有子节点
1) 使用 childNodes 属性获取到该节点的子节点
<ul> <li>1li> <li>2li> <li>3li>ul><script> var ul = document.querySelector('ul'); console.log(ul.childNodes);script>注意! 结果是 NodeList 类型, 得到的是 "节点", 而不是 "元素" (节点的范围比元素更广)

第一个 li 和 ul 之间存在换行.
最后一个 li 和 ul 之间存在换行.
li 和 li 之间也存在换行.
这些换行也都是节点(文本节点).
如果想只获取元素节点, 可以手动写个函数, 把 type 为 1 的节点保留下来.
function getElementNode(nodeList) { var result = []; for (var i = 0; i < nodeList.length; i++) { if (nodeList[i].nodeType == 1) { result.push(nodeList[i]); } } return result;}console.log(getElementNode(ul.childNodes));当然, 这样做比较麻烦.

2) 使用 children 属性获取子节点中的元素节点(非标准, 但是各个浏览器都支持)
<ul> <li>1li> <li>2li> <li>3li>ul><script> var ul = document.querySelector('ul'); console.log(ul.children);script>children 属性得到的是一个 HTMLCollection , 只包含元素对象.
获取第一个节点/最后一个节点
firstChild: 获取到第一个节点(不一定是元素节点)
lastChild: 获取到最后一个节点(不一定是元素节点)
firstElementChild: 获取到第一个元素节点 (IE9+ 以上才支持)
lastElementChild: 获取到最后一个元素节点 (IE9+ 以上才支持)
<ul> <li>1li> <li>2li> <li>3li> <li>4li>ul><script> var ul = document.querySelector('ul'); console.log(ul.firstChild); console.log(ul.lastChild); console.log(ul.firstElementChild); console.log(ul.lastElementChild);script>实际开发更常用的办法是直接借助 children 得到子元素数组, 然后通过下标来操作.
ul.children[0]; // 第一个节点ul.children[ul.length - 1]; // 最后一个节点获取兄弟节点
nextSibling: 获取下一个兄弟节点(不仅仅是元素节点)
previousSibling: 获取上一个兄弟节点(不仅仅是元素节点)
nextElementSibling: 获取下一个兄弟元素节点 (非标准)
previousElementSibling: 获取上一个兄弟元素节点 (非标准)
<div class="parent"> <div class="child1">11div> <div class="child2">22div>div><script> var child1 = document.querySelector('.child1'); console.log(child1.nextSibling); console.log(child1.previousSibling); console.log(child1.nextElementSibling); console.log(child1.previousElementSibling);script>sibling 单词本意就是 兄;弟;姐;妹;
新增节点
分成两个步骤
创建元素节点
把元素节点插入到 dom 树中.
第一步相当于生了个娃, 第二步相当于给娃上户口.
1. 创建元素节点
使用 createElement 方法来创建一个元素. options 参数暂不关注.
var element = document.createElement(tagName[, options]);代码示例:
<div class="container">div><script> var div = document.createElement('div'); div.id = 'mydiv'; div.className = 'box'; div.innerHTML = 'hehe'; console.log(div);script>此时发现, 虽然创建出新的 div 了, 但是 div 并没有显示在页面上. 这是因为新创建的节点并没有加入到 DOM 树中.
上面介绍的只是创建元素节点, 还可以使用:
createTextNode 创建文本节点
createComment 创建注释节点
createAttribute 创建属性节点
我们以 createElement 为主即可.
2. 插入节点到 dom 树中
1) 使用 appendChild 将节点插入到指定节点的最后一个孩子之后
element.appendChild(aChild) <div class="container">div><script> var div = document.createElement('div'); div.id = 'mydiv'; div.className = 'box'; div.innerHTML = 'hehe'; var container = document.querySelector('.container'); container.appendChild(div);script>
2) 使用 insertBefore 将节点插入到指定节点之前.
var insertedNode = parentNode.insertBefore(newNode, referenceNode);
insertedNode被插入节点(newNode)
parentNode新插入节点的父节点
newNode用于插入的节点
referenceNodenewNode将要插在这个节点之前如果
referenceNode为null则newNode将被插入到子节点的末尾.注意: referenceNode 引用节点不是可选参数.
<div class="container"> <div>11div> <div>22div> <div>33div> <div>44div>div><script> var newDiv = document.createElement('div'); newDiv.innerHTML = '我是新的节点'; var container = document.querySelector('.container'); console.log(container.children); container.insertBefore(newDiv, container.children[0]);script>
注意1: 如果针对一个节点插入两次, 则只有最后一次生效(相当于把元素移动了)
<div class="container"> <div>11div> <div>22div> <div>33div> <div>44div>div><script> var newDiv = document.createElement('div'); newDiv.innerHTML = '我是新的节点'; var container = document.querySelector('.container'); console.log(container.children); // 此处的 children 里有 4 个元素 container.insertBefore(newDiv, container.children[0]); // 此处的 children 里有 5 个元素(上面新插了一个), 0 号元素是 新节点, // 1 号元素是 11, 2号节点是 22, 所以是插入到 22 之前. container.insertBefore(newDiv, container.children[2]);script>
注意2: 一旦一个节点插入完毕, 再针对刚刚的节点对象进行修改, 能够同步影响到 DOM 树中的内容.
<div class="container"> <div>11div> <div>22div> <div>33div> <div>44div>div><script> var newDiv = document.createElement('div'); newDiv.innerHTML = '我是新的节点'; var container = document.querySelector('.container'); console.log(container.children); container.insertBefore(newDiv, container.children[0]); // 插入完毕后再次修改 newDiv 的内容 newDiv.innerHTML = '我是新节点2';script>
代码示例: 待办事项

功能:
界面上包含一个输入框和新增任务按钮. 点击新增则把输入框中的任务加入到任务列表中.
任务列表中显示当前待办的所有任务.
每个任务前面包含一个复选框, 勾选则表示任务完成, 任务自动被放到另外一个已完成列表.
a) 页面布局
<div class="container"> <div class="todo"> <h3>未完成h3> div> <div class="done"> <h3>已完成h3> div>div><div class="footer"> <input type="text"> <button>新建任务button>div>b) 页面样式
* { margin: 0; padding: 0; box-sizing: border-box;}body,html { height: 100%;}body,html,input,button { font-size: 20px; line-height: 50px;}body { background-color: #f3f3f3;}.container { width: 800px; height: 100%; margin: 0 auto; background-color: #fff;}.todo,.done { float: left; width: 50%; height: 100%; overflow: auto;}.container h3 { height: 50px; text-align: center; font-weight: 400; background-color: #333; color: #fff;}.container .todo h3 { border-right: 1px solid #fff;}.footer { width: 800px; height: 50px; position: absolute; bottom: 0; /* 水平居中 */ left: 50%; margin-left: -400px;}.footer input { float: left; width: 600px; height: 50px; outline: none;}.footer button { float: left; width: 200px; height: 50px; border: none; background-color: skyblue; color: #fff;}c) 实现新增任务
// 实现新增任务var addTaskButton = document.querySelector('.footer>button');addTaskButton.onclick = function () { // 1. 获取到任务内容的输入框 var input = document.querySelector('.footer>input'); // 2. 获取到输入框内容 var taskContent = input.value; // 3. 根据内容新建一个元素节点 var row = document.createElement('div'); row.className = 'row'; var checkbox = document.createElement('input'); checkbox.type = 'checkbox'; var space = document.createTextNode(' '); // 需要创建一个文本节点, 表示空格 var span = document.createElement('span'); span.innerHTML = taskContent; row.appendChild(checkbox); row.appendChild(space); row.appendChild(span); // 4. 把新节点插入到 todo 中 var todo = document.querySelector('.todo'); todo.appendChild(row);}d) 实现完成任务
把复选框勾选中的元素移动到已完成部分.
在 addTaskButton.onclick 中设置 checkbox 的点击事件.
注意, 这个代码必须放到 addTaskButton.onclick 的回调函数内部.
// 给 checkbox 注册点击事件checkbox.onclick = function () { var row = this.parentNode; // 注意! 是先触发 checked 为 true, 然后再调用 onclick 函数 if (this.checked) { var target = document.querySelector('.done'); } else { var target = document.querySelector('.todo'); } target.appendChild(row);}注意:
目前我们学习了两种创建节点的方式:
createElement
element.innerHTML
这两种方式都很常用.
还有一种方式为
document.write这种方式会导致整个页面全部重绘(页面原有内容丢失). 只在特定场景下才会使用.
删除节点
使用 removeChild 删除子节点
oldChild = element.removeChild(child);
child 为待删除节点
element 为 child 的父节点
返回值为该被删除节点
被删除节点只是从 dom 树被删除了, 但是仍然在内存中, 可以随时加入到 dom 树的其他位置.
如果上例中的
child节点不是element节点的子节点,则该方法会抛出异常.
代码示例: 待办事项2
在上面的案例中, 新增删除任务功能

给每一个任务中增加删除按钮
<div class="row"> <input type="checkbox"> <span>吃饭span> <button>删除button>div>设置按钮样式
.row { /* 受行高影响, row 的实际高度并不是 50px, 需要显式设定 */ height: 50px;}.row button { /* 让按钮显式在右侧 */ float: right; padding: 0 10px; height: 50px;}在新建任务时创建删除按钮
// addTaskButton.onclick = function () { 中var button = document.createElement('button');button.innerHTML = '删除';...row.appendChild(button);注册删除事件
// 给删除按钮注册点击事件button.onclick = function () { var row = this.parentNode; var grandParent = row.parentNode; grandParent.removeChild(row);}拷贝节点
使用 node.cloneNode() 拷贝节点
var dupNode = node.cloneNode(deep);node: 将要被克隆的节点
dupNode: 克隆生成的副本节点
deep: 可选. 是否采用深度克隆,如果为true, 则该节点的所有后代节点也都会被克隆,如果为 false,则只克隆该节点本身.
注意1: 在 DOM4 规范中(实现于Gecko 13.0(Firefox 13.0 / Thunderbird 13.0 / SeaMonkey 2.10)),
deep是一个可选参数。如果省略的话,参数的默认值为true,也就是说默认是深度克隆。如果想使用浅克隆, 你需要将该参数设置为false。在最新的规范里,该方法的行为已经改变了,其默认值变成了 false。虽然该参数仍旧是可选的,但是你必须要为该方法设置 deep 参数,无论是为了向前还是向后兼容考虑。假如开发者没设置参数的话,Gecko 28.0 (Firefox 28 / Thunderbird 28 / SeaMonkey 2.25 / Firefox OS 1.3)) 版本的控制台会发出警告。从 Gecko 29.0 (Firefox 29 / Thunderbird 29 / SeaMonkey 2.26)) 开始该方法默认为浅复制而不是深度复制。
注意2: 克隆一个元素节点会拷贝它所有的属性以及属性值,当然也就包括了属性上绑定的事件(比如
onclick="alert(1)"),但不会拷贝那些使用addEventListener()方法或者node.onclick = fn这种用JavaScript动态绑定的事件.
<div class="container"> <button>我是一个按钮button>div><script> var button = document.querySelector('button'); button.onclick = function () { alert('haha'); } // var newButton = button.cloneNode(); var newButton = button.cloneNode(true); var container = document.querySelector('.container'); container.appendChild(newButton);script>当 cloneNode 参数为空时, 复制的 newButton 不包含文本内容.
当 cloneNode 参数为 true 的时候, 复制的 newButton 才包含文本内容.
另外, 通过 onclick 方式注册的点击事件不会被拷贝.