Javascript 修改文檔(document)

2023-02-17 10:54 更新

DOM 修改是創(chuàng)建“實(shí)時(shí)”頁(yè)面的關(guān)鍵。

在這里,我們將會(huì)看到如何“即時(shí)”創(chuàng)建新元素并修改現(xiàn)有頁(yè)面內(nèi)容。

例子:展示一條消息

讓我們使用一個(gè)示例進(jìn)行演示。我們將在頁(yè)面上添加一條比 alert 更好看的消息。

它的外觀如下:

<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>


這是一個(gè) HTML 示例。現(xiàn)在,讓我們使用 JavaScript 創(chuàng)建一個(gè)相同的 div(假設(shè)樣式已經(jīng)在 HTML/CSS 文件中)。

創(chuàng)建一個(gè)元素

要?jiǎng)?chuàng)建 DOM 節(jié)點(diǎn),這里有兩種方法:

?document.createElement(tag) ?

用給定的標(biāo)簽創(chuàng)建一個(gè)新 元素節(jié)點(diǎn)(element node)

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

?document.createTextNode(text) ?

用給定的文本創(chuàng)建一個(gè) 文本節(jié)點(diǎn)

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

大多數(shù)情況下,我們需要為此消息創(chuàng)建像 div 這樣的元素節(jié)點(diǎn)。

創(chuàng)建一條消息

創(chuàng)建一個(gè)消息 div 分為 3 個(gè)步驟:

// 1. 創(chuàng)建 <div> 元素
let div = document.createElement('div');

// 2. 將元素的類設(shè)置為 "alert"
div.className = "alert";

// 3. 填充消息內(nèi)容
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

我們已經(jīng)創(chuàng)建了該元素。但到目前為止,它還只是在一個(gè)名為 div 的變量中,尚未在頁(yè)面中。所以我們無(wú)法在頁(yè)面上看到它。

插入方法

為了讓 div 顯示出來(lái),我們需要將其插入到 document 中的某處。例如,通過(guò) document.body 將其插入到 <body> 元素里。

對(duì)此有一個(gè)特殊的方法 appenddocument.body.append(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";
  div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

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

在這個(gè)例子中,我們對(duì) document.body 調(diào)用了 append 方法。不過(guò)我們可以在其他任何元素上調(diào)用 append 方法,以將另外一個(gè)元素放入到里面。例如,通過(guò)調(diào)用 div.append(anotherElement),我們便可以在 <div> 末尾添加一些內(nèi)容。

這里是更多的元素插入方法,指明了不同的插入位置:

  • ?node.append(...nodes or strings)? —— 在 ?node末尾 插入節(jié)點(diǎn)或字符串,
  • ?node.prepend(...nodes or strings)? —— 在 ?node開(kāi)頭 插入節(jié)點(diǎn)或字符串,
  • ?node.before(...nodes or strings)? —— 在 ?node前面 插入節(jié)點(diǎn)或字符串,
  • ?node.after(...nodes or strings)? —— 在 ?node后面 插入節(jié)點(diǎn)或字符串,
  • ?node.replaceWith(...nodes or strings)? —— 將 ?node? 替換為給定的節(jié)點(diǎn)或字符串。

這些方法的參數(shù)可以是一個(gè)要插入的任意的 DOM 節(jié)點(diǎn)列表,或者文本字符串(會(huì)被自動(dòng)轉(zhuǎn)換成文本節(jié)點(diǎn))。

讓我們?cè)趯?shí)際應(yīng)用中看一看。

下面是使用這些方法將列表項(xiàng)添加到列表中,以及將文本添加到列表前面和后面的示例:

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

<script>
  ol.before('before'); // 將字符串 "before" 插入到 <ol> 前面
  ol.after('after'); // 將字符串 "after" 插入到 <ol> 后面

  let liFirst = document.createElement('li');
  liFirst.innerHTML = 'prepend';
  ol.prepend(liFirst); // 將 liFirst 插入到 <ol> 的最開(kāi)始

  let liLast = document.createElement('li');
  liLast.innerHTML = 'append';
  ol.append(liLast); // 將 liLast 插入到 <ol> 的最末尾
</script>


這張圖片直觀地顯示了這些方法所做的工作:


因此,最終列表將為:

before
<ol id="ol">
  <li>prepend</li>
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>append</li>
</ol>
after

如上所述,這些方法可以在單個(gè)調(diào)用中插入多個(gè)節(jié)點(diǎn)列表和文本片段。

例如,在這里插入了一個(gè)字符串和一個(gè)元素:

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

請(qǐng)注意:這里的文字都被“作為文本”插入,而不是“作為 HTML 代碼”。因此像 <> 這樣的符號(hào)都會(huì)被作轉(zhuǎn)義處理來(lái)保證正確顯示。

所以,最終的 HTML 為:

&lt;p&gt;Hello&lt;/p&gt;
<hr>
<div id="div"></div>

換句話說(shuō),字符串被以一種安全的方式插入到頁(yè)面中,就像 elem.textContent 所做的一樣。

所以,這些方法只能用來(lái)插入 DOM 節(jié)點(diǎn)或文本片段。

但如果我們想要將內(nèi)容“作為 HTML 代碼插入”,讓內(nèi)容中的所有標(biāo)簽和其他東西都像使用 elem.innerHTML 所表現(xiàn)的效果一樣,那應(yīng)該怎么辦呢?

insertAdjacentHTML/Text/Element

為此,我們可以使用另一個(gè)非常通用的方法:elem.insertAdjacentHTML(where, html)

該方法的第一個(gè)參數(shù)是代碼字(code word),指定相對(duì)于 elem 的插入位置。必須為以下之一:

  • ?"beforebegin"? —— 將 ?html? 插入到 ?elem? 之前,
  • ?"afterbegin"? —— 將 ?html? 插入到 ?elem? 開(kāi)頭,
  • ?"beforeend"? —— 將 ?html? 插入到 ?elem? 末尾,
  • ?"afterend"? —— 將 ?html? 插入到 ?elem? 之后。

第二個(gè)參數(shù)是 HTML 字符串,該字符串會(huì)被“作為 HTML” 插入。

例如:

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

……將導(dǎo)致:

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

這就是我們可以在頁(yè)面上附加任意 HTML 的方式。

這是插入變體的示意圖:


我們很容易就會(huì)注意到這張圖片和上一張圖片的相似之處。插入點(diǎn)實(shí)際上是相同的,但此方法插入的是 HTML。

這個(gè)方法有兩個(gè)兄弟:

  • ?elem.insertAdjacentText(where, text)? —— 語(yǔ)法一樣,但是將 ?text? 字符串“作為文本”插入而不是作為 HTML,
  • ?elem.insertAdjacentElement(where, elem)? —— 語(yǔ)法一樣,但是插入的是一個(gè)元素。

它們的存在主要是為了使語(yǔ)法“統(tǒng)一”。實(shí)際上,大多數(shù)時(shí)候只使用 insertAdjacentHTML。因?yàn)閷?duì)于元素和文本,我們有 append/prepend/before/after 方法 —— 它們也可以用于插入節(jié)點(diǎn)/文本片段,但寫起來(lái)更短。

所以,下面是顯示一條消息的另一種變體:

<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">
    <strong>Hi there!</strong> You've read an important message.
  </div>`);
</script>

節(jié)點(diǎn)移除

想要移除一個(gè)節(jié)點(diǎn),可以使用 node.remove()。

讓我們的消息在一秒后消失:

<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";
  div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

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

請(qǐng)注意:如果我們要將一個(gè)元素 移動(dòng) 到另一個(gè)地方,則無(wú)需將其從原來(lái)的位置中刪除。

所有插入方法都會(huì)自動(dòng)從舊位置刪除該節(jié)點(diǎn)。

例如,讓我們進(jìn)行元素交換:

<div id="first">First</div>
<div id="second">Second</div>
<script>
  // 無(wú)需調(diào)用 remove
  second.after(first); // 獲取 #second,并在其后面插入 #first
</script>

克隆節(jié)點(diǎn):cloneNode

如何再插入一條類似的消息?

我們可以創(chuàng)建一個(gè)函數(shù),并將代碼放在其中。但是另一種方法是 克隆 現(xiàn)有的 div,并修改其中的文本(如果需要)。

當(dāng)我們有一個(gè)很大的元素時(shí),克隆的方式可能更快更簡(jiǎn)單。

調(diào)用 elem.cloneNode(true) 來(lái)創(chuàng)建元素的一個(gè)“深”克隆 —— 具有所有特性(attribute)和子元素。如果我們調(diào)用 elem.cloneNode(false),那克隆就不包括子元素。

一個(gè)拷貝消息的示例:

<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); // 克隆消息
  div2.querySelector('strong').innerHTML = 'Bye there!'; // 修改克隆

  div.after(div2); // 在已有的 div 后顯示克隆
</script>

DocumentFragment

DocumentFragment 是一個(gè)特殊的 DOM 節(jié)點(diǎn),用作來(lái)傳遞節(jié)點(diǎn)列表的包裝器(wrapper)。

我們可以向其附加其他節(jié)點(diǎn),但是當(dāng)我們將其插入某個(gè)位置時(shí),則會(huì)插入其內(nèi)容。

例如,下面這段代碼中的 getListContent 會(huì)生成帶有 <li> 列表項(xiàng)的片段,然后將其插入到 <ul> 中:

<ul id="ul"></ul>

<script>
function getListContent() {
  let fragment = new DocumentFragment();

  for(let i=1; i<=3; i++) {
    let li = document.createElement('li');
    li.append(i);
    fragment.append(li);
  }

  return fragment;
}

ul.append(getListContent()); // (*)
</script>

請(qǐng)注意,在最后一行 (*) 我們附加了 DocumentFragment,但是它和 ul “融為一體(blends in)”了,所以最終的文檔結(jié)構(gòu)應(yīng)該是:

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

DocumentFragment 很少被顯式使用。如果可以改為返回一個(gè)節(jié)點(diǎn)數(shù)組,那為什么還要附加到特殊類型的節(jié)點(diǎn)上呢?重寫示例:

<ul id="ul"></ul>

<script>
function getListContent() {
  let result = [];

  for(let i=1; i<=3; i++) {
    let li = document.createElement('li');
    li.append(i);
    result.push(li);
  }

  return result;
}

ul.append(...getListContent()); // append + "..." operator = friends!
</script>

我們之所以提到 DocumentFragment,主要是因?yàn)樗厦嬗幸恍└拍睿?nbsp;template 元素,我們將在以后討論。

老式的 insert/remove 方法

老式用法

這些內(nèi)容有助于理解舊腳本,但不適合用于新代碼的開(kāi)發(fā)中。

由于歷史原因,還存在“老式”的 DOM 操作方法。

這些方法來(lái)自真正的遠(yuǎn)古時(shí)代。如今,沒(méi)有理由再使用它們了,因?yàn)橹T如 appendprepend,before,afterremove,replaceWith 這些現(xiàn)代方法更加靈活。

我們?cè)谶@兒列出這些方法的唯一原因是,你可能會(huì)在許多腳本中遇到它們。

?parentElem.appendChild(node)?

將 node 附加為 parentElem 的最后一個(gè)子元素。

下面這個(gè)示例在 <ol> 的末尾添加了一個(gè)新的 <li>

<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)?

在 parentElem 的 nextSibling 前插入 node

下面這段代碼在第二個(gè) <li> 前插入了一個(gè)新的列表項(xiàng):

<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>

如果要將 newLi 插入為第一個(gè)元素,我們可以這樣做:

list.insertBefore(newLi, list.firstChild);

?parentElem.replaceChild(node, oldChild)?

將 parentElem 的后代中的 oldChild 替換為 node。

?parentElem.removeChild(node)?

從 parentElem 中刪除 node(假設(shè) node 為 parentElem 的后代)。

下面這個(gè)示例從 <ol> 中刪除了 <li>

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

<script>
  let li = list.firstElementChild;
  list.removeChild(li);
</script>

所有這些方法都會(huì)返回插入/刪除的節(jié)點(diǎn)。換句話說(shuō),parentElem.appendChild(node) 返回 node。但是通常我們不會(huì)使用返回值,我們只是使用對(duì)應(yīng)的方法。

聊一聊 “document.write”

還有一個(gè)非常古老的向網(wǎng)頁(yè)添加內(nèi)容的方法:document.write。

語(yǔ)法如下:

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

調(diào)用 document.write(html) 意味著將 html “就地馬上”寫入頁(yè)面。html 字符串可以是動(dòng)態(tài)生成的,所以它很靈活。我們可以使用 JavaScript 創(chuàng)建一個(gè)完整的頁(yè)面并對(duì)其進(jìn)行寫入。

這個(gè)方法來(lái)自于沒(méi)有 DOM,沒(méi)有標(biāo)準(zhǔn)的上古時(shí)期……。但這個(gè)方法依被保留了下來(lái),因?yàn)檫€有腳本在使用它。

由于以下重要的限制,在現(xiàn)代腳本中我們很少看到它:

document.write 調(diào)用只在頁(yè)面加載時(shí)工作。

如果我們稍后調(diào)用它,則現(xiàn)有文檔內(nèi)容將被擦除。

例如:

<p>After one second the contents of this page will be replaced...</p>
<script>
  // 1 秒后調(diào)用 document.write
  // 這時(shí)頁(yè)面已經(jīng)加載完成,所以它會(huì)擦除現(xiàn)有內(nèi)容
  setTimeout(() => document.write('<b>...By this.</b>'), 1000);
</script>

因此,在某種程度上講,它在“加載完成”階段是不可用的,這與我們上面介紹的其他 DOM 方法不同。

這是它的缺陷。

還有一個(gè)好處。從技術(shù)上講,當(dāng)在瀏覽器正在讀?。ā敖馕觥保﹤魅氲?HTML 時(shí)調(diào)用 document.write 方法來(lái)寫入一些東西,瀏覽器會(huì)像它本來(lái)就在 HTML 文本中那樣使用它。

所以它運(yùn)行起來(lái)出奇的快,因?yàn)樗?nbsp;不涉及 DOM 修改。它直接寫入到頁(yè)面文本中,而此時(shí) DOM 尚未構(gòu)建。

因此,如果我們需要向 HTML 動(dòng)態(tài)地添加大量文本,并且我們正處于頁(yè)面加載階段,并且速度很重要,那么它可能會(huì)有幫助。但實(shí)際上,這些要求很少同時(shí)出現(xiàn)。我們可以在腳本中看到此方法,通常是因?yàn)檫@些腳本很舊。

總結(jié)

  • 創(chuàng)建新節(jié)點(diǎn)的方法:
    • ?document.createElement(tag)? —— 用給定的標(biāo)簽創(chuàng)建一個(gè)元素節(jié)點(diǎn),
    • ?document.createTextNode(value)? —— 創(chuàng)建一個(gè)文本節(jié)點(diǎn)(很少使用),
    • ?elem.cloneNode(deep)? —— 克隆元素,如果 ?deep==true? 則與其后代一起克隆。
  • 插入和移除節(jié)點(diǎn)的方法:
    • ?node.append(...nodes or strings)? —— 在 ?node? 末尾插入,
    • ?node.prepend(...nodes or strings)? —— 在 ?node? 開(kāi)頭插入,
    • ?node.before(...nodes or strings)? —— 在 ?node? 之前插入,
    • ?node.after(...nodes or strings)? —— 在 ?node? 之后插入,
    • ?node.replaceWith(...nodes or strings)? —— 替換 ?node?。
    • ?node.remove()? —— 移除 ?node?。

    文本字符串被“作為文本”插入。

  • 這里還有“舊式”的方法:
    • ?parent.appendChild(node)?
    • ?parent.insertBefore(node, nextSibling)?
    • ?parent.removeChild(node)?
    • ?parent.replaceChild(newElem, node)?

    這些方法都返回 ?node?。

  • 在 ?html? 中給定一些 HTML,?elem.insertAdjacentHTML(where, html)? 會(huì)根據(jù) ?where? 的值來(lái)插入它:
    • ?"beforebegin"? —— 將 ?html? 插入到 ?elem? 前面,
    • ?"afterbegin"? —— 將 ?html? 插入到 ?elem? 的開(kāi)頭,
    • ?"beforeend"? —— 將 ?html? 插入到 ?elem? 的末尾,
    • ?"afterend"? —— 將 ?html? 插入到 ?elem? 后面。

另外,還有類似的方法,elem.insertAdjacentText 和 elem.insertAdjacentElement,它們會(huì)插入文本字符串和元素,但很少使用。

  • 要在頁(yè)面加載完成之前將 HTML 附加到頁(yè)面:
    • ?document.write(html)?

    頁(yè)面加載完成后,這樣的調(diào)用將會(huì)擦除文檔。多見(jiàn)于舊腳本。

任務(wù)


createTextNode vs innerHTML vs textContent

重要程度: 5

我們有一個(gè)空的 DOM 元素 elem 和一個(gè)字符串 text。

下面這 3 個(gè)命令中的哪些命令會(huì)執(zhí)行完全相同的操作?

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

解決方案

回答:1 和 3。

這兩個(gè)命令都會(huì)將 text “作為文本”添加到 elem 中。

這是一個(gè)例子:

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

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

清除元素

重要程度: 5

創(chuàng)建一個(gè)函數(shù) ?clear(elem)? 用來(lái)移除元素里的內(nèi)容。

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

<script>
  function clear(elem) { /* 你的代碼 */ }

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

解決方案

首先,讓我們看看 錯(cuò)誤 的做法:

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

這是行不通的,因?yàn)檎{(diào)用 remove() 會(huì)從首端開(kāi)始移除 elem.childNodes 集合中的元素,因此,元素每次都從索引 0 開(kāi)始。但是 i 在增加,所以元素就被跳過(guò)了。

用 for..of 循環(huán)的結(jié)果也跟上面一樣。

正確的做法是:

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

還有一種更簡(jiǎn)單的方法,也可以達(dá)到我們所要的效果:

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

為什么留下 "aaa"?

重要程度: 1

在下面這個(gè)示例中,我們調(diào)用 table.remove() 從文檔中刪除表格。

但如果運(yùn)行它,你就會(huì)看到文本 "aaa" 并沒(méi)有被刪除。

這是為什么?

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

<script>
  alert(table); // 表格,就是它應(yīng)有的樣子

  table.remove();
  // 為什么 "aaa" 還存在于文檔中?
</script>

解決方案

這個(gè)題目中的 HTML 是錯(cuò)的。這就是造成怪異現(xiàn)象的原因。

瀏覽器必須自動(dòng)修復(fù)它。但 <table> 內(nèi)可能會(huì)沒(méi)有文本:根據(jù)規(guī)范,只允許特定于表格的標(biāo)簽。所以瀏覽器將 "aaa" 展示在了 <table> 前面。

當(dāng)我們刪除表格后,文本 "aaa" 仍然存在的原因就很明顯了吧。

通過(guò)使用瀏覽器開(kāi)發(fā)者工具查看 DOM,就可以輕松地回答這個(gè)問(wèn)題。從瀏覽器開(kāi)發(fā)者工具中我們可以看到,"aaa" 在 <table> 前面。

HTML 標(biāo)準(zhǔn)規(guī)范詳細(xì)描述了如何處理錯(cuò)誤的 HTML,并且瀏覽器的這種行為是正確的。


創(chuàng)建一個(gè)列表

重要程度: 4

編寫一個(gè)接口,根據(jù)用戶輸入創(chuàng)建一個(gè)列表(list)。

對(duì)于每個(gè)列表項(xiàng):

  1. 使用 ?prompt? 向用戶詢問(wèn)列表項(xiàng)的內(nèi)容。
  2. 使用用戶輸入的內(nèi)容創(chuàng)建 ?<li>?,并添加到 ?<ul>?。
  3. 重復(fù)以上步驟,直到用戶取消輸入(按下 ?Esc? 鍵,或輸入一個(gè)空內(nèi)容)。

所有元素應(yīng)該都是動(dòng)態(tài)創(chuàng)建的。

如果用戶輸入了 HTML 標(biāo)簽,那么這些內(nèi)容應(yīng)該被視為文本進(jìn)行后續(xù)處理。


解決方案

請(qǐng)注意使用 textContent 對(duì) <li> 的內(nèi)容進(jìn)行賦值的用法。

使用沙箱打開(kāi)解決方案。


從對(duì)象創(chuàng)建樹(shù)

重要程度: 5

編寫一個(gè)函數(shù) createTree,從嵌套對(duì)象創(chuàng)建一個(gè)嵌套的 ul/li 列表(list)。

例如:

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

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

語(yǔ)法:

let container = document.getElementById('container');
createTree(container, data); // 將樹(shù)創(chuàng)建在 container 中

結(jié)果(樹(shù))看起來(lái)像這樣:


選擇下面兩種方式中的一種,來(lái)完成這個(gè)任務(wù):

  1. 為樹(shù)創(chuàng)建 HTML,然后將它們賦值給 ?container.innerHTML?。
  2. 創(chuàng)建節(jié)點(diǎn)樹(shù),并使用 DOM 方法將它們附加(append)上去。

如果這兩種方式你都做,那就太好了。

P.S. 樹(shù)上不應(yīng)該有“多余”的元素,例如空的 <ul></ul> 葉子節(jié)點(diǎn)。


解決方案

遍歷對(duì)象的最簡(jiǎn)單的方法是使用遞歸。

  1. 使用 innerHTML 的解決方案。
  2. 使用 DOM 的解決方案

在樹(shù)中顯示后代

重要程度: 5

這里有一棵由嵌套的 ul/li 組成的樹(shù)。

編寫代碼,為每個(gè) <li> 添加其后代數(shù)量。跳過(guò)葉子節(jié)點(diǎn)(沒(méi)有子代的節(jié)點(diǎn))。

結(jié)果:



解決方案

為了將文本附加到每個(gè) <li> 中,我們可以改變文本節(jié)點(diǎn)的 data。

使用沙箱打開(kāi)解決方案。


創(chuàng)建一個(gè)日歷

重要程度: 4

編寫一個(gè)函數(shù) createCalendar(elem, year, month)

對(duì)該函數(shù)的調(diào)用,應(yīng)該使用給定的 year/month 創(chuàng)建一個(gè)日歷,并將創(chuàng)建的日歷放入 elem 中。

創(chuàng)建的日歷應(yīng)該是一個(gè)表格(table),其中每一周用 <tr> 表示,每一天用 <td> 表示。表格頂部應(yīng)該是帶有星期名的 <th>:第一天應(yīng)該是 Monday,依此類推,直到 Sunday。

例如,createCalendar(cal, 2012, 9) 應(yīng)該在元素 cal 中生成如下所示的日歷:


P.S. 在這個(gè)任務(wù)中,生成一個(gè)日歷就可以了,不需要有點(diǎn)擊交互的功能。


解決方案

我們將表格創(chuàng)建為字符串:"<table>...</table>",然后將其賦值給 innerHTML。

算法如下:

  1. 使用 ?<th>? 創(chuàng)建帶有星期名的表頭。
  2. 創(chuàng)建日期對(duì)象 ?d = new Date(year, month-1)?。它是 ?month? 的第一天(考慮到 JavaScript 中的月份從 ?0? 開(kāi)始,而不是從 ?1? 開(kāi)始)。
  3. 直到月份的第一天 ?d.getDay()?,前面的幾個(gè)單元格是空的。讓我們用 ?<td></td>? 填充它們。
  4. 天數(shù)增長(zhǎng) ?d?:?d.setDate(d.getDate()+1)?。如果 ?d.getMonth()? 還沒(méi)到下一個(gè)月,那么就將新的單元格 ?<td>? 添加到日歷中。如果那天是星期日,就添加一個(gè)新行 ?“</tr><tr>”?。
  5. 如果該月結(jié)束,但表格的行尚未填滿,就用空的 ?<td>? 補(bǔ)齊。

使用沙箱打開(kāi)解決方案。


使用 setInterval 的彩色時(shí)鐘

重要程度: 4

創(chuàng)建一個(gè)像這樣的彩色時(shí)鐘:


使用 HTML/CSS 進(jìn)行樣式設(shè)計(jì),JavaScript 僅用來(lái)更新元素中的時(shí)間。


解決方案

首先,讓我們編寫 HTML/CSS。

時(shí)間的每個(gè)組件都有其自己的 <span>,那將會(huì)看起來(lái)很棒:

<div id="clock">
  <span class="hour">hh</span>:<span class="min">mm</span>:<span class="sec">ss</span>
</div>

另外,我們需要使用 CSS 為它們著色。

函數(shù) update 會(huì)刷新時(shí)鐘,由 setInterval 每秒調(diào)用一次:

function update() {
  let clock = document.getElementById('clock');
  let date = new Date(); // (*)
  let hours = date.getHours();
  if (hours < 10) hours = '0' + hours;
  clock.children[0].innerHTML = hours;

  let minutes = date.getMinutes();
  if (minutes < 10) minutes = '0' + minutes;
  clock.children[1].innerHTML = minutes;

  let seconds = date.getSeconds();
  if (seconds < 10) seconds = '0' + seconds;
  clock.children[2].innerHTML = seconds;
}

在 (*) 行中,我們每次都檢查當(dāng)前時(shí)間。setInterval 調(diào)用并不可靠:它們可能會(huì)發(fā)生延遲現(xiàn)象。

時(shí)鐘管理函數(shù):

let timerId;

function clockStart() { // 運(yùn)行時(shí)鐘
  if (!timerId) { // 僅當(dāng)時(shí)鐘停止時(shí) setInterval
    timerId = setInterval(update, 1000);
  }
  update(); // (*)
}

function clockStop() {
  clearInterval(timerId);
  timerId = null; // (**)
}

請(qǐng)注意,update() 不僅在 clockStart() 中被調(diào)度,而且還立即在 (*) 行運(yùn)行。否則,訪問(wèn)者將不得不等到 setInterval 第一次執(zhí)行。在那之前,時(shí)鐘將是空的。

此外,在 clockStart() 中僅當(dāng)時(shí)鐘未運(yùn)行時(shí)才進(jìn)行 setInterval 也是至關(guān)重要的。否則多次點(diǎn)擊 Start 按鈕會(huì)設(shè)置多個(gè)并發(fā)的間隔。更糟糕的是 —— 我們只會(huì)保留最后一個(gè)時(shí)間間隔的 timerID,失去對(duì)所有其他時(shí)間間隔的引用。那我們就再也無(wú)法停止時(shí)鐘了!請(qǐng)注意,當(dāng)時(shí)鐘停止時(shí),我們需要在 (**) 行這樣清除 timerID,以便可以通過(guò)執(zhí)行 clockStart() 再次啟動(dòng)時(shí)鐘。

使用沙箱打開(kāi)解決方案。


將 HTML 插入到列表中

重要程度: 5

編寫代碼,將 <li>2</li><li>3</li>,插入到兩個(gè) <li> 之間:

<ul id="ul">
  <li id="one">1</li>
  <li id="two">4</li>
</ul>

解決方案

當(dāng)我們需要在某處插入 HTML 時(shí),insertAdjacentHTML 是最適合的方案。

解決方法:

one.insertAdjacentHTML('afterend', '<li>2</li><li>3</li>');

對(duì)表格進(jìn)行排序

重要程度: 5

下面是一個(gè)表格:

<table>
<thead>
  <tr>
    <th>Name</th><th>Surname</th><th>Age</th>
  </tr>
</thead>
<tbody>
  <tr>
    <td>John</td><td>Smith</td><td>10</td>
  </tr>
  <tr>
    <td>Pete</td><td>Brown</td><td>15</td>
  </tr>
  <tr>
    <td>Ann</td><td>Lee</td><td>5</td>
  </tr>
  <tr>
    <td>...</td><td>...</td><td>...</td>
  </tr>
</tbody>
</table>

可能會(huì)有更多行。

編寫代碼,按 ?"name"? 列對(duì)其進(jìn)行排序。


解決方案

這個(gè)解決方案雖然很短,但可能看起來(lái)有點(diǎn)難理解,因此,在這里我提供了一些擴(kuò)展性的注釋:

let sortedRows = Array.from(table.tBodies[0].rows) // 1
  .sort((rowA, rowB) => rowA.cells[0].innerHTML.localeCompare(rowB.cells[0].innerHTML));

table.tBodies[0].append(...sortedRows); // (3)

對(duì)此算法一步一步進(jìn)行講解:

  1. 從 ?<tbody>? 獲取所有 ?<tr>?。
  2. 然后將它們按第一個(gè) ?<td>?(?name? 字段)中的內(nèi)容進(jìn)行比較。
  3. 然后使用 ?.append(...sortedRows)? 按正確的順序插入節(jié)點(diǎn)。

我們不必刪除行元素,只需要“重新插入”,它們就會(huì)自動(dòng)離開(kāi)原來(lái)的位置。

P.S. 在我們的例子中,表格中有一個(gè)明確的 <tbody>,但即使 HTML 中的表格沒(méi)有 <tbody>,DOM 結(jié)構(gòu)也總是具有它。

使用沙箱打開(kāi)解決方案。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)