对DOM的元素的修改是创建生动灵活的网页的关键,这里我们将看到怎么修改网页中已存在的元素和即时创建新的页面元素。

首先我们看一个简单的例子然后解释一下相关的方法

例子:显示一条消息

首先,让我们来看看怎么在网页上添加一条比 alert 更好看的消息,如下所示:

//可以复制代码到html文件中,然后在网页上查看渲染结果
<style>            
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<div class="alert">
  <strong>Hi there!</strong> You've read an important message.
</div>

这是一个HTML例子。现在让我们使用JavaScript来创建一个相同的div(假设样式表仍是上面页面内的)

创建元素

有两种方法用来创建DOM节点

document.createElement(tag)

用给定的标签(tag)创建新的元素

let div = document.createElement('div');

document.createTextNode(text)

用给定的文本(text)创建新的文本节点

let textNode = document.createTextNode('Here I am');

创建消息

在我们的例子中我们需要创建带有指定类和消息的div

let div = document.createElement('div');
div.className = "alert alert-success";
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

这样,我们就有了一个DOM元素,它现在还只是个变量,因为没有被插入到页面中所以还看不见。

插入方法

为了显示我们的div,我们需要在文档流中的某个地方插入他们,比如在 document.body 中,有个专门的方法来实现:document.body.appendChild(div).

以下是完整代码:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<script>
  let div = document.createElement('div');
  div.className = "alert alert-success";
  div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

  document.body.appendChild(div);
</script>

parentElem.appendChild(node)

将节点做为父元素的最后一个子元素来添加到父元素中

下面的例子添加一个新的<li>标签到父元素<ol>的最后:

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  let newLi = document.createElement('li');
  newLi.innerHTML = 'Hello, world!';

  list.appendChild(newLi);
</script>

parentElem.insertBefore(node, nextSibling)

在父元素内的一个子元素(nextSibling)前插入节点

下面的代码将在列表的第二个子元素前面插入新的元素:

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  let newLi = document.createElement('li');
  newLi.innerHTML = 'Hello, world!';

  list.insertBefore(newLi, list.children[1]);
</script>

想插入为第一个子元素的话可以这样:

list.insertBefore(newLi, list.firstChild);

parentElem.replaceChild(node, oldChild)

node替换parentElem中的子元素oldChild

所有的这些方法都返回被插入的节点,即:parentElem.appendChild(node) 将返回 node, 但是通常这些返回值都没有被用到,我们仅仅是使用这个方法本身而已。

这些方法都过时了:它们出现在以前,我们可以在很多老的脚本中看到它们的身影,不幸的是,有很多功能使用它们不能够解决问题。

比如:我们怎么插入一个字符串样的html?;或者给你一个节点,怎么在它前面插入另一个节点?当然我们可以使用上面的方法实现这些,但是过程会不够优雅。

这里还有2套其他的插入方法来优雅简单的处理这些情况。

prepend/append/before/after

这套方法提供更加灵活的插入:

  • node.append(...nodes or strings) – 在该节点内的最后面插入节点或字符串,
  • node.prepend(...nodes or strings) – 在该节点内的最前面插入节点或字符串,
  • node.before(...nodes or strings) – 在该节点前面插入节点或字符串,
  • node.after(...nodes or strings) – 在该节点后面插入节点或字符串,
  • node.replaceWith(...nodes or strings) – 用一个节点或者字符串替换该节点.

下面是使用这些方法向列表内添加新的节点和在它前后添加文本的例子:

<ol id="ol">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  ol.before('before');
  ol.after('after');

  let prepend = document.createElement('li');
  prepend.innerHTML = 'prepend';
  ol.prepend(prepend);

  let append = document.createElement('li');
  append.innerHTML = 'append';
  ol.append(append);
</script>

//output
before
    1.prepend
    2.0
    3.1
    4.2
    5.append
after

这些方法可以通过仅仅调用一次插入多条节点和文本片段。

比如这里,一个字符串和一个元素同时被插入进去:

<div id="div"></div>

<script>
  div.before('<p>Hello</p>', document.createElement('hr'));
</script>

所有的文本都将仅做为文本被插入,所以最终的输出为:

<p>Hello</p>
<hr>
<div id="div"></div>

也就是,字符串将像element.textContent方法一样以一种很安全的方法被插入,这些方法只能被用来插入DOMnode或者文本片段,但是如果我们想像element.innerHTML 一样插入html文本,并让所有的tag正常的工作该怎么办呢?

insertAdjacentHTML/Text/Element

还有另一种简洁多功能的方法:elem.insertAdjacentHTML(where, html)

第一个参数是一个字符串,指明了在哪里插入,必须为下面的字符串中的一种:

  • "beforebegin" – 在该元素前面插入,
  • "afterbegin" – 在该元素内最前面插入,
  • "beforeend" – 在该元素最后面插入,
  • "afterend" – 在该元素后面插入.

第二个参数是HTML字符串(做为HTML,tag将发挥功能)。

例如:

<div id="div"></div>

<script>
  div.insertAdjacentHTML('beforebegin', '<p>Hello</p>');
  div.insertAdjacentHTML('afterend', '<p>Bye</p>');
</script>

//将输出:

<p>Hello</p>
<div id="div"></div>
<p>Bye</p>

这就是我们该怎么插入专门的HTML到网页中,这个方法跟上面的一套方法插入点是一样的,但是这个方法插入的是HTML。

这个方法还有2个“兄弟”:

  • elem.insertAdjacentText(where, text) – 同样的语法,但是插入的是一个文本字符串而不是HTML,
  • elem.insertAdjacentElement(where, elem) – 同样的语法,但是插入的是一个元素.

这2个方法的存在只是让语法看起来更格式化,实际上,大多数情况下我们只用到了insertAdjacentHTML,因为对于插入元素或者文本来说,我们通常使用append/prepend/before/after—— 它们更简洁。

所以下面是显示一条消息的另一种写法:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<script>
  document.body.insertAdjacentHTML("afterbegin", `<div class="alert alert-success">
    <strong>Hi there!</strong> You've read an important message.
  </div>`);
</script>

克隆节点:cloneNode

怎么插入多条相同的消息呢?

我们可能会去写一个函数然后把插入的消息写进去来实现。但是我们也可以直接克隆已经存在的div并且如果需要的话直接修改里面的文本内容。当我们需要处理一个较大的元素的时候,这将更快速和简单。

  • elem.cloneNode(ture)方法将深度克隆这个元素的所有属性和子元素。而elem.cloneNode(false)只克隆元素本身不克隆它的子元素

克隆这条消息的例子:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<div class="alert" id="div">
  <strong>Hi there!</strong> You've read an important message.
</div>

<script>
  let div2 = div.cloneNode(true); // clone the message
  div2.querySelector('strong').innerHTML = 'Bye there!'; // change the clone

  div.after(div2); // show the clone after the existing div
</script>

移除节点

移除节点有下列方法:

parentElem.removeChild(node)

从父元素中移除子元素。

node.remove()

将节点从他的位置上移除。

我们可以明显看到后者更加的简洁,而前者的存在是为了历史版本的兼容。

!注意

如果我们想将一个元素移动到另一个位置,移动后我们不需要移除移动前的这个元素。

所有的插入方法都会自动的移除移动前的这个元素。

例如,让我们交换一下元素:

<div id="first">First</div>
<div id="second">Second</div>
<script>
  // no need to call remove  不需要调用删除方法
  second.after(first); // take #second and after it - insert #first
</script>

我们来让消息1秒后消失:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<script>
  let div = document.createElement('div');
  div.className = "alert alert-success";
  div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

  document.body.append(div);
  setTimeout(() => div.remove(), 1000);
  // or setTimeout(() => document.body.removeChild(div), 1000);
</script>

浅谈document.write

document.write是一种古老的将一些东西添加到网页的方法。

语法:

<p>Somewhere in the page...</p>
<script>
  document.write('<b>Hello from JS</b>');
</script>
<p>The end</p>

通过上面,我们可以看出来,不像我们上面讲的其他的DOM方法,这个方法在页面加载完毕后就没有用处了,这是它的劣势。

从技术上来讲,当浏览器在渲染HTML的过程中调用document.write方法时,它将向其中插入一些东西,就像是它在浏览器中进行初始化。

这将体现它的优势——这个渲染的过程非常的快速。因为没有对DOM的修改。在DOM还没建立的时候它就直接向网页写入文本,然后浏览器在生成DOM的时候将它置入DOM中。

所以如果我们在网页加载的阶段就想要动态的在HTML中添加大量的文本,并且需要快速的添加,我们就需要使用这个方法了。但是在实际工作中,我们极少会碰到这种情况,通常我们只会在一些老的脚本中看到它,比如你会在QQ的一键分享到QQ空间或QQ好友的代码中看到,用的就是这个document.write(),我们并不认为这样是好的,所以经常拿来了以后自行改成其他的实现方式。

总结

创建节点的方法:

  • document.createElement(tag) – creates an element with the given tag,
  • document.createTextNode(value) – creates a text node (rarely used),
  • elem.cloneNode(deep) – clones the element, if deep==true then with all descendants.

插入和删除节点的方法:

  • 从父级元素中:

    • parent.appendChild(node)
    • parent.insertBefore(node, nextSibling)
    • parent.removeChild(node)
    • parent.replaceChild(newElem, node)

    所有的这些方法对返回该操作的节点

  • 对于一组节点或字符串:

    • node.append(...nodes or strings) – insert into node, at the end,
    • node.prepend(...nodes or strings) – insert into node, at the beginning,
    • node.before(...nodes or strings) –- insert right before node,
    • node.after(...nodes or strings) –- insert right after node,
    • node.replaceWith(...nodes or strings) –- replace node.
    • node.remove() –- remove the node.

    文本string作为文本被插入

  • 对于一段HTML:elem.insertAdjacentHTML(where, html),根据下面的where插入:

    • "beforebegin" – insert html right before elem,

    • afterbegin" – insert html into elem, at the beginning,

    • "beforeend" – insert html into elem, at the end,

    • "afterend" – insert html right after elem.

      相似的方法还有elem.insertAdjacentTextelem.insertAdjacentElement,它们分别用来插入文本和元素,但是用的很少。

  • 在页面加载完成前向页面添加HTML:

    • document.write(html) 当页面加载完成后调用这个方法将擦除文档——通常在老的脚本中使用过。

任务

1. createTextNode vs innerHTML vs textContent

我们现在有一个空的DOM元素elem和一个字符串string.

下面三个命令中执行结果完全一样的是?

  1. elem.append(document.createTextNode(text))
  2. elem.innerHTML = text
  3. elem.textContent = text

答案:1和3,

解析:1和3插入的文本是作为文本插入的,而2插入的文本的tag将发挥作用。

例如:

<div id="elem1"></div>
<div id="elem2"></div>
<div id="elem3"></div>
<script>
  let text = '<b>text</b>';

  elem1.append(document.createTextNode(text));
  elem2.textContent = text;
  elem3.innerHTML = text;
</script>

2. 清除元素

创建一个函数clear(elem)用来清除elem内部的所有元素

<ol id="elem">
  <li>Hello</li>
  <li>World</li>
</ol>

<script>
  function clear(elem)
  {
        /* your code 您的代码 */

  }

  clear(elem);     // clears the list 清除列表
</script>

答案:

首先我们来看看错误的写法:

function clear(elem) {
  for (let i=0; i < elem.childNodes.length; i++) {
      elem.childNodes[i].remove();
  }
}

这个函数无效,因为每次调用remove()elem的子元素的索引都重置为重零开始,但是循环的i继续递增,所以会跳着删除,导致不能删除干净。

for..of loop循环也是一样的道理,不起作用。

正确的写法为:

function clear(elem) {
  while (elem.firstChild) {
    elem.firstChild.remove();
  }
}

还有一种简单的方法:

function clear(elem) {
  elem.innerHTML = '';
}

3.为什么‘aaa’还在?

测试下面的例子,为什么 table.remove() 方法不能删除文本‘aaa’?

<table id="table">
  aaa
  <tr>
    <td>Test</td>
  </tr>
</table>

<script>
  alert(table); // the table, as it should be

  table.remove();
  // why there's still aaa in the document?
</script>

答案: 上面的HTML代码中的‘aaa’不是tabel里面的元素,所以浏览器会自动修复这个问题,结果是浏览器渲染的时候将‘aaa’移动到了table外面,我们可以通过审查元素看到渲染的结果。所以使用了这个remove方法后‘aaa’仍然在。

4. 创建列表

写一个根据用户的输入来创建列表的接口

每个列表单元:

  1. 使用prompt给用户输入内容,
  2. 根据用户的输入创建<li>然后将它添加到<ul>中,
  3. 重复循环指导用户取消输入(通过按Esc按钮或者点击prompt上的取消按钮)。

所有的元素都需要被动态的创建,如果用户输入了HTML的标签,这些标签应当做文本来插入。

答案:


    let ul = document.createElement('ul');
    document.body.append(ul);

    while (true) {
      let data = prompt("Enter the text for the list item", "");

      if (!data) {
        break;
      }

      let li = document.createElement('li');
      li.textContent = data;
      ul.append(li);
    }

5. 根据对象创建树结构

写一个函数createTree来根据一个嵌套的对象生成一个嵌套的ul/li列表

例如:

let data = {
  "Fish": {
    "trout": {},
    "salmon": {}
  },

  "Tree": {
    "Huge": {
      "sequoia": {},
      "oak": {}
    },
    "Flowering": {
      "redbud": {},
      "magnolia": {}
    }
  }
};

语法:

let container = document.getElementById('container');
createTree(container, data); // creates the tree in the container

生成类似下面的树结构:

- Fish
    - trout
    - salmon
- Tree
    - Huge
        - sequoia
        - oak
    - Flowering
        - redbud
        - magnolia

选择下面2种解决方案的一种来完成任务

  • 先生成树的HTML代码,然后将它赋值给container.innerHTML,
  • 先创建树节点,然后使用DOM方法将其添加到container中。

P.S. 树不应含有额外的元素比如空的<ul></ul>标签

答案一:

let data = {
  "Fish": {
    "trout": {},
    "salmon": {}
  },

  "Tree": {
    "Huge": {
      "sequoia": {},
      "oak": {}
    },
    "Flowering": {
      "redbud": {},
      "magnolia": {}
    }
  }
};

function createTree(container, data){
    container.innerHTML = createTreeText(data);
}

function createTreeText(data){
    var li = '';

    for (var key in data) {
        li += '<li>' + key + createTreeText(data[key]) + '</li>';
    }

    if(li) {
        var ul = '<ul>' + li + '</ul>';
    }

    return ul || '';
}

let container = document.querySelector('.container');

createTree(container, data);

答案二:

function createTree(container, obj) {
    container.append(createTreeDom(obj));
}

function createTreeDom(obj){
    if(!Object.keys(obj).length) return;

    let ul = document.createElement('ul');

    for (var key in obj) {
        let li = document.createElement('li');

        li.innerHTML = key;

        let childrenUl = createTreeDom(obj[key]);

        if(childrenUl){
            li.append(childrenUl);
        }

        ul.append(li);

    }

    return ul;
}

let container = document.querySelector('.container');

createTree(container, data);