整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          用原生 JS 實現 innerHTML 功能

          知道瀏覽器和服務端是通過 HTTP 協議進行數據傳輸的,而 HTTP 協議又是純文本協議,那么瀏覽器在得到服務端傳輸過來的 HTML 字符串,是如何解析成真實的 DOM 元素的呢,也就是我們常說的生成 DOM Tree,最近了解到狀態機這樣一個概念,于是就萌生一個想法,實現一個 innerHTML 功能的函數,也算是小小的實踐一下。

          函數原型

          我們實現一個如下的函數,參數是 DOM 元素和 HTML 字符串,將 HTML 字符串轉換成真實的 DOM 元素且 append 在參數一傳入的 DOM 元素中。

          function html(element, htmlString) {
           // 1. 詞法分析
           // 2. 語法分析
           // 3. 解釋執行
          }
          復制代碼
          

          在上面的注釋我已經注明,這個步驟我們分成三個部分,分別是詞法分析、語法分析和解釋執行。

          詞法分析

          詞法分析是特別重要且核心的一部分,具體任務就是:把字符流變成 token 流。

          詞法分析通常有兩種方案,一種是狀態機,一種是正則表達式,它們是等效的,選擇你喜歡的就好。我們這里選擇狀態機。

          首先我們需要確定 token 種類,我們這里不考慮太復雜的情況,因為我們只對原理進行學習,不可能像瀏覽器那樣有強大的容錯能力。除了不考慮容錯之外,對于自閉合節點、注釋、CDATA 節點暫時均不做考慮。

          接下來步入主題,假設我們有如下節點信息,我們會分出哪些 token 來呢。

          <p class="a" data="js">測試元素</p>
          復制代碼
          

          對于上述節點信息,我們可以拆分出如下 token

          • 開始標簽:<p
          • 屬性標簽:class="a"
          • 文本節點:測試元素
          • 結束標簽:</p>

          狀態機的原理,將整個 HTML 字符串進行遍歷,每次讀取一個字符,都要進行一次決策(下一個字符處于哪個狀態),而且這個決策是和當前狀態有關的,這樣一來,讀取的過程就會得到一個又一個完整的 token,記錄到我們最終需要的 tokens 中。

          萬事開頭難,我們首先要確定起初可能處于哪種狀態,也就是確定一個 start 函數,在這之前,對詞法分析類進行簡單的封裝,具體如下

          function HTMLLexicalParser(htmlString, tokenHandler) {
           this.token = [];
           this.tokens = [];
           this.htmlString = htmlString
           this.tokenHandler = tokenHandler
          }
          復制代碼
          

          簡單解釋下上面的每個屬性

          • token:token 的每個字符
          • tokens:存儲一個個已經得到的 token
          • htmlString:待處理字符串
          • tokenHandler:token 處理函數,我們每得到一個 token 時,就已經可以進行流式解析

          我們可以很容易的知道,字符串要么以普通文本開頭,要么以<開頭,因此 start 代碼如下

          HTMLLexicalParser.prototype.start = function(c) {
           if(c === '<') {
           this.token.push(c)
           return this.tagState
           } else {
           return this.textState(c)
           }
          }
          復制代碼
          

          start處理的比較簡單,如果是<字符,表示開始標簽或結束標簽,因此我們需要下一個字符信息才能確定到底是哪一類 token,所以返回tagState函數去進行再判斷,否則我們就認為是文本節點,返回文本狀態函數。

          接下來分別展開tagState和textState函數。tagState根據下一個字符,判斷進入開始標簽狀態還是結束標簽狀態,如果是/表示是結束標簽,否則是開始標簽,textState用來處理每一個文本節點字符,遇到<表示得到一個完整的文本節點 token,代碼如下

          HTMLLexicalParser.prototype.tagState = function(c) {
           this.token.push(c)
           if(c === '/') {
           return this.endTagState
           } else {
           return this.startTagState
           }
          }
          HTMLLexicalParser.prototype.textState = function(c) {
           if(c === '<') {
           this.emitToken('text', this.token.join(''))
           this.token = []
           return this.start(c)
           } else {
           this.token.push(c)
           return this.textState
           }
          }
          復制代碼
          

          這里初次見面的函數是emitToken、startTagState和endTagState。

          emitToken用來將產生的完整 token 存儲在 tokens 中,參數是 token 類型和值。

          startTagState用來處理開始標簽,這里有三種情形

          • 如果接下來的字符是字母,則認定依舊處于開始標簽態
          • 遇到空格,則認定開始標簽態結束,接下來是處理屬性了
          • 遇到>,同樣認定為開始標簽態結束,但接下來是處理新的節點信息

          endTagState用來處理結束標簽,結束標簽不存在屬性,因此只有兩種情形

          • 如果接下來的字符是字母,則認定依舊處于結束標簽態
          • 遇到>,同樣認定為結束標簽態結束,但接下來是處理新的節點信息

          邏輯上面說的比較清楚了,代碼也比較簡單,看看就好啦

          HTMLLexicalParser.prototype.emitToken = function(type, value) {
           var res = {
           type,
           value
           }
           this.tokens.push(res)
           // 流式處理
           this.tokenHandler && this.tokenHandler(res)
          }
          HTMLLexicalParser.prototype.startTagState = function(c) {
           if(c.match(/[a-zA-Z]/)) {
           this.token.push(c.toLowerCase())
           return this.startTagState
           }
           if(c === ' ') {
           this.emitToken('startTag', this.token.join(''))
           this.token = []
           return this.attrState
           }
           if(c === '>') {
           this.emitToken('startTag', this.token.join(''))
           this.token = []
           return this.start
           }
          }
          HTMLLexicalParser.prototype.endTagState = function(c) {
           if(c.match(/[a-zA-Z]/)) {
           this.token.push(c.toLowerCase())
           return this.endTagState
           }
           if(c === '>') {
           this.token.push(c)
           this.emitToken('endTag', this.token.join(''))
           this.token = []
           return this.start
           }
          }
          復制代碼
          

          最后只有屬性標簽需要處理了,也就是上面看到的attrState函數,也處理三種情形

          • 如果是字母、單引號、雙引號、等號,則認定為依舊處于屬性標簽態
          • 如果遇到空格,則表示屬性標簽態結束,接下來進入新的屬性標簽態
          • 如果遇到>,則認定為屬性標簽態結束,接下來開始新的節點信息

          代碼如下

          HTMLLexicalParser.prototype.attrState = function(c) {
           if(c.match(/[a-zA-Z'"=]/)) {
           this.token.push(c)
           return this.attrState
           }
           if(c === ' ') {
           this.emitToken('attr', this.token.join(''))
           this.token = []
           return this.attrState
           }
           if(c === '>') {
           this.emitToken('attr', this.token.join(''))
           this.token = []
           return this.start
           }
          }
          復制代碼
          

          最后我們提供一個parse解析函數,和可能用到的getOutPut函數來獲取結果即可,就不啰嗦了,上代碼

          HTMLLexicalParser.prototype.parse = function() {
           var state = this.start;
           for(var c of this.htmlString.split('')) {
           state = state.bind(this)(c)
           }
          }
          HTMLLexicalParser.prototype.getOutPut = function() {
           return this.tokens
          }
          復制代碼
          

          接下來簡單測試一下,對于<p class="a" data="js">測試并列元素的</p><p class="a" data="js">測試并列元素的</p>HTML 字符串,輸出結果為

          看上去結果很 nice,接下來進入語法分析步驟

          語法分析

          首先們需要考慮到的情況有兩種,一種是有多個根元素的,一種是只有一個根元素的。

          我們的節點有兩種類型,文本節點和正常節點,因此聲明兩個數據結構。

          function Element(tagName) {
           this.tagName = tagName
           this.attr = {}
           this.childNodes = []
          }
          function Text(value) {
           this.value = value || ''
          }
          復制代碼
          

          目標:將元素建立起父子關系,因為真實的 DOM 結構就是父子關系,這里我一開始實踐的時候,將 childNodes 屬性的處理放在了 startTag token 中,還給 Element 增加了 isEnd 屬性,實屬愚蠢,不但復雜化了,而且還很難實現。仔細思考 DOM 結構,token 也是有順序的,合理利用棧數據結構,這個問題就變的簡單了,將 childNodes 處理放在 endTag 中處理。具體邏輯如下

          • 如果是 startTag token,直接 push 一個新 element
          • 如果是 endTag token,則表示當前節點處理完成,此時出棧一個節點,同時將該節點歸入棧頂元素節點的 childNodes 屬性,這里需要做個判斷,如果出棧之后??樟耍硎菊麄€節點處理完成,考慮到可能有平行元素,將元素 push 到 stacks。
          • 如果是 attr token,直接寫入棧頂元素的 attr 屬性
          • 如果是 text token,由于文本節點的特殊性,不存在有子節點、屬性等,就認定為處理完成。這里需要做個判斷,因為文本節點可能是根級別的,判斷是否存在棧頂元素,如果存在直接壓入棧頂元素的 childNodes 屬性,不存在 push 到 stacks。

          代碼如下

          function HTMLSyntacticalParser() {
           this.stack = []
           this.stacks = []
          }
          HTMLSyntacticalParser.prototype.getOutPut = function() {
           return this.stacks
          }
          // 一開始搞復雜了,合理利用基本數據結構真是一件很酷炫的事
          HTMLSyntacticalParser.prototype.receiveInput = function(token) {
           var stack = this.stack
           if(token.type === 'startTag') {
           stack.push(new Element(token.value.substring(1)))
           } else if(token.type === 'attr') {
           var t = token.value.split('='), key = t[0], value = t[1].replace(/'|"/g, '')
           stack[stack.length - 1].attr[key] = value
           } else if(token.type === 'text') {
           if(stack.length) {
           stack[stack.length - 1].childNodes.push(new Text(token.value))
           } else {
           this.stacks.push(new Text(token.value))
           }
           } else if(token.type === 'endTag') {
           var parsedTag = stack.pop()
           if(stack.length) {
           stack[stack.length - 1].childNodes.push(parsedTag)
           } else {
           this.stacks.push(parsedTag)
           }
           }
          }
          復制代碼
          

          簡單測試如下:

          沒啥大問題哈

          解釋執行

          對于上述語法分析的結果,可以理解成 vdom 結構了,接下來就是映射成真實的 DOM,這里其實比較簡單,用下遞歸即可,直接上代碼吧

          function vdomToDom(array) {
           var res = []
           for(let item of array) {
           res.push(handleDom(item))
           }
           return res
          }
          function handleDom(item) {
           if(item instanceof Element) {
           var element = document.createElement(item.tagName)
           for(let key in item.attr) {
           element.setAttribute(key, item.attr[key])
           }
           if(item.childNodes.length) {
           for(let i = 0; i < item.childNodes.length; i++) {
           element.appendChild(handleDom(item.childNodes[i]))
           }
           }
           return element
           } else if(item instanceof Text) {
           return document.createTextNode(item.value)
           }
          }
          復制代碼
          

          實現函數

          上面三步驟完成后,來到了最后一步,實現最開始提出的函數

          function html(element, htmlString) {
           // parseHTML
           var syntacticalParser = new HTMLSyntacticalParser()
           var lexicalParser = new HTMLLexicalParser(htmlString, syntacticalParser.receiveInput.bind(syntacticalParser))
           lexicalParser.parse()
           var dom = vdomToDom(syntacticalParser.getOutPut())
           var fragment = document.createDocumentFragment()
           dom.forEach(item => {
           fragment.appendChild(item)
           })
           element.appendChild(fragment)
          }
          復制代碼
          

          三個不同情況的測試用例簡單測試下

          html(document.getElementById('app'), '<p class="a" data="js">測試并列元素的</p><p class="a" data="js">測試并列元素的</p>')
          html(document.getElementById('app'), '測試<div>你好呀,我測試一下沒有深層元素的</div>')
          html(document.getElementById('app'), '<div class="div"><p class="p">測試一下嵌套很深的<span class="span">p的子元素</span></p><span>p同級別</span></div>')
          復制代碼
          

          聲明:簡單測試下都沒啥問題,本次實踐的目的是對 DOM 這一塊通過詞法分析和語法分析生成 DOM Tree 有一個基本的認識,所以細節問題肯定還是存在很多的。

          總結

          其實在了解了原理之后,這一塊代碼寫下來,并沒有太大的難度,但卻讓我很興奮,有兩個成果吧

          • 了解并初步實踐了一下狀態機
          • 數據結構的魅力

          ocument.write 是直接寫入到頁面的內容流,如果在寫之前沒有調用 document.open, 瀏覽器會自動調用 open。每次寫完關閉之后重新調用該函數,會導致頁面被重寫。

          innerHTML 則是 DOM 頁面元素的一個屬性,代表該元素的 html 內容。你可以精確到某一個具體的元素來進行更改。如果想修改 document 的內容,則需要修改。

          document.documentElement.innerElement

          innerHTML 將內容寫入某個 DOM 節點,不會導致頁面全部重繪。

          innerHTML 很多情況下都優于 document.write,其原因在于其允許更精確的控制要刷新頁面的那一個部分。

          內容是《Web前端開發之Javascript視頻》的課件,請配合大師哥《Javascript》視頻課程學習。

          Element.innerHTML屬性:

          操作元素內HTML內容,即可設置或獲取使用HTML代碼表示的元素的后代;

          在讀取時,該屬性返回與調用元素的所有子節點(包括元素、注釋和文本節點)對應的HTML代碼字會串,如:

          <div id="mydiv">
              <h2>零點程序員</h2>
              <ul id="myList">
                  <li>HTML</li>
                  <li class="current">CSS</li>
                  <li>JavaScript</li>
              </ul>
          </div>
          <script>
          var mydiv = document.getElementById("mydiv");
          console.log(mydiv.innerHTML);
          </script>

          注:不同瀏覽器返回的文本格式有可能不同,比如,部分低版本會把所有標簽轉換為大寫;另外,瀏覽器會按照原先文檔的格式返回,包括空格和縮進;

          在寫入時,會將給定的字符串解析為DOM子樹,將用這個DOM子樹替換調用元素原先的所有子節點;如果設置的值是文本沒有HTML標簽,其結果就是純文本,也就是文本節點,此時,如果該文本節點包含字符&、<或>, innerHTML將這些字符分別返回為&、<和>;

          mydiv.innerHTML = "零點程序員 & zeronetwork 主講><b>\"王唯\"</b>";
          console.log(mydiv.innerHTML);
          mydiv.innerHTML = "零點程序員";

          設置元素的innerHTML屬性將會刪除該元素的所有后代,因此,如果要保留原來的內容,可以在innerHTML屬性的基礎上,可以使用+=進行賦值,也就達到了追加內容的效果;

          mydiv.innerHTML += "<b>大師哥王唯</b>";

          如果設置innerHTML屬性時,使用了不合法的HTML代碼,瀏覽器會自動修正,但要避免出現這種問題;

          另外,不允許document對象使用該屬性,如果使用了,會靜默失?。?/p>

          設置了innerHTML屬性后,可以像訪問文檔中的其他節點一樣訪問新創建的節點;

          console.log(mydiv.childNodes);

          從本質上來看,設置innerHTML屬性,瀏覽器會把給定的值被解析為HTML或者XML,結果就是一個DocumentFragment對象,其中保存著代表元素的DOM節點,然后再append到元素中;

          innerHTML也有一些限制,在多數瀏覽器中,通過innerHTML插入的<script> 元素不會被執行,因為有可能會產生潛在的安全問題;

          var content = document.getElementById("content");
          content.innerHTML = "<script>alert('wangwei');<\/script>";

          即使如此,使用innerHTML屬性也不能消除潛在的風險,比如,繞過<script>標簽,把腳本綁定到相關的事件中;

          mydiv.innerHTML = "<img src='nourl' onerror='alert(\"加載圖片出錯啦\")'>";

          通過innerHTML寫入<style>元素就可以運行;如:

          mydiv.innerHTML = "<style>body{background-color:purple;}</style>";
          // 放在head中
          document.head.innerHTML += "<style>body{background-color:purple;}</style>";
          console.log(document.head.innerHTML);

          在設置innerHTML屬性時,雖然元素的所有子元素被替換,其仍被保存在內存中,如果事先有變量在引用這些子元素,在設置innerHTML后,這些變量仍將保持對原始子元素的引用;

          var mydiv = document.getElementById("mydiv");
          var h2 = mydiv.querySelector("h2");
          mydiv.innerHTML = "新內容";
          console.log(h2);
          mydiv.appendChild(h2);

          并不是所有元素都有innerHTML屬性,不支持的有<col> <colgroup> <frameset> <head> <html> <style> <table> <tbody> <thead> <tfoot> <title> <tr>

          無論什么時候插入外界的HTML內容時,都應該對HTML進行無害化處理,IE8提供了window.toStaticHTML()方法,接受一個HTM字符串,返回一個經過無害化處理后的版本;

          var mydiv = document.getElementById("mydiv");
          var text = "<a href='#' onclick='alert(\"hi\")'>zeronetwork</a>";
          // mydiv.innerHTML = text;
          var sanitized = window.toStaticHTML(text);  // [?s?n?ta?zd]
          console.log(sanitized);  // 非IE會拋出異常
          mydiv.innerHTML = sanitized;

          小示例:

          使用innerHTML創建一種機制用于將消息記錄到頁面中的一個元素中;

          <style>
          .box{width: 600px;height: 300px;
          border:1px solid black; padding: 2em; overflow: hidden scroll;}
          </style>
          <div class="box">
              <h2>日志:</h2>
              <div class="log"></div>
          </div>
          <script>
          function log(msg){
              var logEle = document.querySelector(".log");
              var time = new Date().toLocaleTimeString();
              logEle.innerHTML += time + ": " + msg + "<br/>"; 
          }
          // log("打印一些數據");
          // 定義一個事件處理程序
          function logEvent(event){
              var msg = "Event <strong>" + event.type + "</strong> at <em>" + 
                  event.clientX + "," + event.clientY + "</em>";
              log(msg);
          }
          // 綁定事件處理程序
          var boxEle = document.querySelector(".box");
          boxEle.addEventListener("mousedown", logEvent);
          boxEle.addEventListener("mouseup", logEvent);
          boxEle.addEventListener("click", logEvent);
          boxEle.addEventListener("mouseenter", logEvent);
          boxEle.addEventListener("mouseleave", logEvent);
          </script>

          Element.outerHTML屬性:

          與innerHTML屬性基本一致,不同點是,innerHTML是訪問和設置元素的所有子節點,而outerHTML屬性不僅包括它的所有子節點,也包括它本身;

          console.log(mydiv.outerHTML);
          mydiv.outerHTML = "<p><h2>零點網絡</h2></p>";

          如果元素沒有父元素,即如果它是文檔的根元素,在設置其outerHTML屬性將拋出異常,如:

          document.documentElement.outerHTML = "content"; // 異常

          這個屬性應用的機會非常少;

          HTMLElement.innerText屬性:

          可以操作元素中包含的所有文本,最初是由IE實現的,后來被納入標準中;

          var mydiv = document.getElementById("mydiv");
          console.log(mydiv.innerText);
          mydiv.innerText = "零點程序員";
          console.log(mydiv.innerText);

          輸出一個文檔樹時,無論文本位于文檔樹中的什么位置,會按照由淺入深的順序,將子文檔樹中的所有文本拼接起來;

          <div id="content">
              <p>零點網絡<strong>zerontwork</strong>是一家從事IT教育的公司</p>
              <ul>
                  <li>HTML</li>
                  <li>CSS</li>
                  <li>Javascript</li>
              </ul>
          </div>
          <script>
          var content = document.getElementById("content");
          console.log(content.innerText);
          // 返回
          // 零點網絡zerontwork是一家從事IT教育的公司
          //
          // HTML/
          // CSS
          // Javascript
          </script>

          由于不同瀏覽器處理空白字符的方式不同,因此輸出的文本可能會也可能不會包含原始的HTML代碼中的縮進;

          使用innerText屬性設置內容時,會移除原先所有的子節點,將永遠只會生成當前節點的一個子文本節點;如果設置的內容包括HTML標簽,會自動被轉碼,也就是說,會對所有出現在文本中的HTML語法字符進行編碼(>、<、”、&);

          mydiv.innerText = "<h2>wangwei</h2>";  // < > 會被轉義

          因為在訪問innerText屬性時,其會過濾掉html標簽,所以可以利用它的這個特點,快速過濾掉元素的HTML標簽,即把innerText設置為innerText;

          content.innerText = content.innerText;
          console.log(content.innerText);

          如果在設置innerHTML屬性時,賦給的就是純文本字符串,那它就與innerText屬性作用一樣了;

          var mydiv = document.getElementById("mydiv");
          mydiv.innerText = "零點網絡 zeronetwork";
          mydiv.innerHTML = "零點網絡 zeronetwork";
          mydiv.innerText = "零點網絡\nzeronetwork";  // 有br
          mydiv.innerHTML = "零點網絡\nzeronetwork";  // 無br,但源碼格式有換行

          因為innerHTML是解析html標簽的,而\n不是標簽,所以當作空格被忽略了;但在innerText中,瀏覽器遇到\n,就會執行換行,所以把它解析為<br>;

          在實際使用中,如果要過濾html標簽,可以使用正則,如:

          // 去除html標簽可以使用正則
          content.innerHTML = content.innerHTML.replace(/<.+?>/img,"");
          console.log(content.innerText);  // 沒有格式<br>
          console.log(content.innerHTML);  // 沒有格式<br>

          HTMLElement.outerText屬性:

          與innerText一樣,只不過替換的是元素(包括子節點)本身;其是一個非標準屬性;

          var mydiv = document.getElementById("mydiv");
          console.log(mydiv.innerText);
          console.log(mydiv.outerText); // 返回值與innerText一致

          在讀取文本值時,outerText和innerText的結果完全一樣;

          但在寫模式下,outerText就完全不同了,其本身都會被新的文本節點都替代,從文檔中被刪除,但其仍然被保存在內存中,如果有變量引用,還可以再利用;

          mydiv.outerText = "零點程序員";
          console.log(mydiv);  // 依然保留著原有的引用

          FF不支持outerText屬性,如:

          mydiv.outerText = "零點程序員";  // 在FF中失效
          // 在FF中返回undefined,如果有上一行,會打印出“零點程序員”,但這和內置的outerText沒有關系
          console.log(mydiv.outerText);

          在實際使用中,只會用到innerHTML和innerText,其他兩個一般不用,也沒有多大的實際意義;

          Node.textContent屬性:

          DOM3規定了一個屬性textContent,該屬性被定義在Node接口中,它的作用類似innerText屬性,返回一個節點及其后代的所有文本內容;

          var mydiv = document.getElementById("mydiv");
          console.log(mydiv.innerText);
          console.log(mydiv.textContent); // 返回值與innerText基本一致,但格式不一樣

          如果設置textContent屬性,會刪除該元素的所有子節點,并被替換為包含指定字符串的一個單獨的文本節點;

          var mydiv = document.getElementById("mydiv");
          mydiv.textContent = "大師哥王唯";
          mydiv.textContent = "<h3>大師哥王唯</h3>";  // 會被轉碼
          console.log(mydiv.textContent);
          console.log(mydiv.childNodes);  // NodeList [text]

          如果節點是文本節點,此屬性可用于取代 nodeValue 屬性,如;

          var h2 = document.querySelector("h2").firstChild; // 取得文本節點
          console.log(h2.textContent); // zeronetwork
          console.log(h2.nodeValue);   // zeronetwork
          h2.nodeValue = "零點程序員";
          console.log(h2.textContent); // 零點程序員
          console.log(h2.nodeValue);   // 零點程序員

          可以看出,兩者是聯動的;

          如果事先有變量引用著它的后代節點,即使節點使用該方法移除所有后代節點,但被引用的后代節點依然存在,可以被再次利用;

          var content = document.getElementById("content");
          var h2 = content.querySelector("h2");  // content中的h2
          content.textContent = "王唯";
          console.log(content.textContent);
          console.log(h2);  // <h2>zeronetwork</h2>
          console.log(h2.parentElement);  // null
          var mydiv = document.getElementById("mydiv");
          mydiv.appendChild(h2);

          與innerText屬性的區別:

          兩者的返回的內容并不完全一樣,比如在輸出的格式上其與innerText是不同的,其會保留代碼中的空白符;同時,innerText針對表格,會試圖保留表格的格式;

          var mytable = document.getElementById("mytable");
          console.log(mytable.innerText);
          console.log(mytable.textContent);

          textContent屬性會返回元素的所有內容,包括其中的樣式和腳本代碼,而innerText只返回能呈現在頁面上的元素;

          // 在mydiv中添加<style>和<script>標簽
          var mydiv = document.getElementById("mydiv");
          console.log(mydiv.innerText); // 不包括<style>和<script>
          // 包括<style>和<script>標簽內的內容,但該標簽被過濾了
          console.log(mydiv.textContent);

          既然innerText只返回能呈現在頁面上的元素,所以它的返回值會受CSS樣式的影響,不會返回被CSS隱藏的元素的文本;

          <!-- textContent返回值沒有變化,但innerText不包括"HTML"和"CSS" -->
          <ul id="mylist">
              <li style="visibility: hidden;">HTML</li>
              <li style="display: none;">CSS</li>
              <li>JavaScript</li>
          </ul>

          textContent屬性能返回文本節點的文本內容,而innerText會返回undefined;如果是文本節點調用textContent屬性,其返回值與nodeValue一致;

          innerHTML有可能會引發安全問題,但textConent卻不會;

          mydiv.innerHTML = "<img src='nourl' onerror='alert(\"加載圖片出錯啦\")'>";
          mydiv.textContent = "<img src='nourl' onerror='alert(\"加載圖片出錯啦\")'>";
          console.log(mydiv.childNodes);  // index.html:20 NodeList [text]

          第一行的onerror會被執行,第二行不會執行,并且其被解析為文本節點,如此,textContent不會引發安全問題;

          所有主流的瀏覽器都支持textContent屬性,但IE8及以下不支持,可以包裝一個兼容的函數:

          function getInnerText(element){
              return (typeof element.textContent == "string") ? element.textContent : element.innerText;
          }
          function setInnerText(element, text){
              if(typeof element.textContent == "string")
                  element.textContent = text;
              else
                  element.innerText = text;
          }
          document.write(getInnerText(content));
          setInnerText(content, "零點程序員");

          或者直接定義在Node.prototype中:

          if(Object.defineProperty 
              && Object.getOwnPropertyDescriptor
              && !Object.getOwnPropertyDescriptor(Node.prototype, "textContent")){
              (function(){
                  var innerText = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "innerText");
                  Object.defineProperty(Node.prototype, "textContent",{
                      get: function(){
                          return innerText.get.call(this);
                      },
                      set: function(s){
                          return innerText.set.call(this, s);
                      }
                  });
              })();
          }

          <script>元素中的文本:

          內聯的<script>元素有一個text屬性用來獲取它們的文本;

          <script>
              console.log("function");
              function func(){return true;}
          </script>
          <script>
          var script = document.getElementsByTagName("script")[0];
          console.log(script.innerText);
          console.log(script.textContent);
          console.log(script.text);  // 三者輸出一致
          </script>

          如果將<script>元素的type屬性設置為”text/x-custom-data”,就表明了腳本為不可執行的Javascript代碼,如此,Javascript解析器將忽略該腳本,這也使得<script>元素可以被用來嵌入任意文本內容;

          <script type="text/x-custom-data">
              console.log("function");
              function func(){return true;}
          </script>
          <script>
          var script = document.getElementsByTagName("script")[0];
          console.log(script.innerText);
          console.log(script.textContent);
          console.log(script.text);  // 三者輸出一致
          </script>
          <script type="text/x-custom-data">
          <div style="border:1px solid red; width:300px;">
              <h2>視頻教程</h2>
          </div>
          </script>
          <script>
          var script = document.getElementsByTagName("script")[0];
          var mydiv = document.getElementById("mydiv");
          mydiv.innerHTML = script.text;
          </script>

          Element.insertAdjacentHTML(position, text)方法:

          該方法會將任意的HTML字符串text解析為Element元素,并將結果節點插入到DOM樹中的指定的元素”相鄰”的position位置;該方法最早是在IE4中出現的;它接收兩個參數:插入位置和要插入的HTML文本;

          第一個參數position的可能值:

          • beforebegin:在當前元素之前插入一個緊鄰的同輩元素;
          • afterbegin:在當前元素之下插入一個新的子元素或在第一個子元素之前插入新的子元素;
          • beforeend:在當前元素之下插入一個新的子元素或在最后一個子元素之后插入新的子元素;
          • afterend:在當前元素之后插入一個緊鄰的同輩元素;

          第二個參數text為HTML字符串,如果瀏覽器無法解析,會拋出錯誤,如;

          var mydiv = document.getElementById("mydiv");
          mydiv.insertAdjacentHTML("beforebegin","<p>前一個同輩元素</p>");
          mydiv.insertAdjacentHTML("afterbegin","<p>作為第一個子元素</p>");
          mydiv.insertAdjacentHTML("beforeend","<p>最后一個子元素</p>");
          mydiv.insertAdjacentHTML("afterend","<p>后一個同輩元素</p>");

          insertAdjacentHTML()方法同innerHTML屬性一樣,會遇到安全問題,在使用該屬性插入HTML內容時,需要轉義之后才能使用;

          另外,如果元素沒有子元素的時候,其和innerHTML就非常相像了;

          var newdiv = document.createElement("div");
          newdiv.insertAdjacentHTML("afterbegin", "<p>零點程序員</p>");
          // 同以下
          newdiv.innerHTML = "<p>零點程序員</p>";
          document.body.appendChild(newdiv);

          需要注意的是,如果position為beforebegin或afterend,那該元素必須具有一個parent元素;

          var newdiv = document.createElement("div");
          // 異常:The element has no parent,此時newdiv并沒有被添加到DOM樹中,它并沒有父節點,但是如果把下面行互換一下,就可以了;
          newdiv.insertAdjacentHTML("afterend", "<p>零點程序員</p>");
          document.body.appendChild(newdiv);

          基于insertAdjacentHTML()方法定義一個更符合語義邏輯的一個對象,如:

          // Insert.before()、Insert.after()、Insert.atStart()和Insert.atEnd()
          var Insert = {
              before: function(e,h) {
                  if(e.parentElement)
                      e.insertAdjacentHTML("beforebegin", h);
              },
              after: function(e,h) {
                  if(e.parentElement)
                      e.insertAdjacentHTML("afterend", h);
              },
              atStart: function(e,h) {e.insertAdjacentHTML("afterbegin", h);},
              atEnd: function(e,h) {e.insertAdjacentHTML("beforeend", h);}
          };
          var mydiv = document.getElementById("mydiv");
          Insert.before(mydiv, "<h2>zeronetwork</h2>");
          // 或者
          // 假定where值為before、after、innerfirst和innerlast
          function insertHTML(el, where, html){
              if(!el) return false;
              var _where = "beforeend";
              switch(where){
                  case "before":
                      _where = "beforebegin";
                      break;
                  case "after":
                      _where = "afterend";
                      break;
                  case "innerfirst":
                      _where = "afterbegin";
                      break;
                  case "innerlast":
                      _where = "beforeend";
                      break;
                  default:
                      _where = "beforeend";
                      break;
              }
              if(_where == "beforebegin" || _where == "afterend"){
                  if(!el.parentElement)
                      return false;
              }
              el.insertAdjacentHTML(_where, html);
          }
          var mydiv = document.getElementById("mydiv");
          insertHTML(mydiv, "innerfirst", "<h2>zeronetwork</h2>");

          小示例,添加商品:

          <div class="container">
              <div class="formdiv">
                  <label>商品:</label><input type="text" id="product" /><br/>
                  <label>價格:</label><input type="text" id="price" /><br/>
                  <label>數量:</label><input type="text" id="quantity" /><br/>
                  <button id="btnAdd">添加</button>
              </div>
              <table class="table">
                  <thead>
                      <tr>
                          <th>序號</th><th>商品</th><th>價格</th><th>數量</th><th>金額</th>
                      </tr>
                  </thead>
                  <tbody id="data"></tbody>
              </table>
          </div>
          <script>
          var id=1;
          var btnAdd = document.getElementById("btnAdd");
          btnAdd.addEventListener("click",function(e){
              var content = document.getElementById("data");
              
              var product = document.getElementById("product").value;
              var price = document.getElementById("price").value;
              var quantity = document.getElementById("quantity").value;
              var total = price * quantity;
              var newEntry = "<tr>" + 
                  "<td>" + id + "</td>" + 
                  "<td>" + product + "</td>" + 
                  "<td>" + price + "</td>" + 
                  "<td>" + quantity + "</td>" + 
                  "<td>" + total + "</td>" + 
                  "</tr>";
              content.insertAdjacentHTML('afterbegin', newEntry);
              id++;
          },false);
          </script>

          Element.insertAdjacentText(position, text)方法:

          該方法與insertAdjacentHTML()類似,只不過插入的是純文本內容,它的作用是將一個給定的文本text插入到相對于被調用的元素的給定position位置;

          position的值insertAdjacentHTML()中的position是一樣的;

          var mydiv = document.getElementById("mydiv");
          mydiv.insertAdjacentText("afterbegin","王唯");
          mydiv.insertAdjacentText("afterend","zeronetwork");

          如果text是html字符串,也會被當作純文本進行處理,如:

          // 頁面輸出:<h2>王唯</h2>
          mydiv.insertAdjacentText("afterbegin","<h2>王唯</h2>");

          Element. insertAdjacentElement(position, element)方法:

          將一個給定的元素節點插入到相對于被調用的元素的給定的position位置;與insertAdjacentHTML()方法類似,只不過插入的是一個節點對象;該方法會返回一個Element對象;

          var mydiv = document.getElementById("mydiv");
          var div = document.createElement("div");
          div.innerHTML = "<h2>zeronetwork</h2>";
          div.style.width = "200px";
          div.style.height = "100px";
          div.style.backgroundColor = "lightgray";
          var newdiv = mydiv.insertAdjacentElement("beforeend", div);
          console.log(div === newdiv); // true

          github上有人分享了一個包裝的方法,就是利用以上原生的方法;

          // 把一個節點插入到DOM樹中的一個位置
          function dominsert(parent, child, position){
              var pos = position || 'beforeend';
              if(typeof child === 'string')
                  dominsert.html(parent, child, pos);
              else
                  dominsert.element(parent, child, pos);
          }
          // 使用原生的insertAdjacentHTML()方法
          dominsert.html = function(parent, child, position){
              parent.insertAdjacentHTML(position, child);
          };
          // 使用原生的insertAdjacentElement()或insertBefore()方法
          dominsert.element = function(parent, child, position){
              if(parent.insertAdjacentElement)
                  parent.insertAdjacentElement(position, child);
              else{
                  switch (position){
                      case "beforebegin":
                          parent.parentNode.insertBefore(child, parent);
                          break;
                      case "afterbegin":
                          parent.insertBefore(child, parent.firstChild);
                          break;
                      case "beforeend":
                          parent.appendChild(child);
                          break;
                      case "afterend":
                          parent.parentNode.insertBefore(child, parent.nextSibling);
                          break;
                  }
              }
          };
          var mydiv = document.getElementById("mydiv");
          dominsert(mydiv,"<span>web前端</span>");
          dominsert(mydiv, "<b>零點程序員</b>", 'beforebegin');
          console.log(mydiv);

          內存和性能問題:

          使用以上的方法替換子節點可能會導致瀏覽器的內存占用問題,尤其是在IE中,問題更加明顯;

          如果被刪除的子樹中的元素設置了事件處理程序或者引用了一個Javascript對象作為屬性,被刪除的元素與事件處理程序或引用的JS對象之間的綁定關系在內存中并沒有一并刪除;如果這種情況頻繁出現,頁面占用的內存數量就會明顯增加;因此,在使用innerHTML、outerHTML屬性和insertAdjacentHTML()方法時,最好手工先移除要被替換的元素的所有事件處理程序和JS對象屬性;

          不要反復地使用innerHTML插入HTML;

          var arr = ["HTML","CSS","JavaScript"];
          var ul = document.getElementById("myList");
          for(var i=0,len=arr.length; i < len; i++){
              ul.innerHTML += "<li>" + arr[i] + "</li>"; 
          }

          ,最好的做法是:單獨構建字符串變量,再一次性的把結果賦給innerHTML;

          console.time("insert");
          var lisHTML = "";
          for(var i=0,len=arr.length; i<len;i++){
              lisHTML += "<li>" + arr[i] + "</li>";
          }
          ul.innerHTML = lisHTML;
          console.timeEnd("insert");

          adjacent三個方法與insertBefore()、appendChild()和innerHTML的比較;

          在某些時候,這些方法屬性都可以達到同樣的目的,但在實際開發中,要針對當時的情況,選擇一個合適的方法,沒有哪個方法就一定比另外的方法更好,只有相對的合適;

          同時,這三個方法的性能雖然不一樣,但相差不大,幾乎可以忽略;

          insertAdjacentHTML()與innerHTML屬性的性能:

          insertAdjacentHTML()方法不會重新解析它正在使用的元素,因此它不會破壞元素內的現有元素,這就避免了額外的序列化步驟,但使用innerHTML時,特別是在原有的基礎上追加元素時,都會對原有的元素重新序列化,因此,前者比后者效率更快;

          appendChild()與insertAdjacentHTML()方法的性能;

          // time 10ms
          console.time("append");
          for(var i=0; i<1000; i++)
              mydiv.appendChild(document.createElement("div"));
          console.timeEnd("append");
          // tim 30ms
          console.time("adjacent");
          for(var i=0; i<1000; i++)
              mydiv.insertAdjacentHTML("beforeend","<div></div>");
          console.timeEnd("adjacent");

          可以看到appendChild()方法比insertAdjacentHTML()方法快很多,但是改進以上代碼后,為其添加有文本內容的元素,如;

          // time 30ms多
          console.time("append");
          for(var i=0; i<1000; i++){
              var div = document.createElement("div");
              var h2 = document.createElement("h2");
              h2.appendChild(document.createTextNode("零點程序員"));
              div.appendChild(h2);
              var p = document.createElement("p");
              p.appendChild(document.createTextNode("由大師哥王唯主講"));
              div.appendChild(p);
              mydiv.appendChild(div);
          }
          console.timeEnd("append");
          // time 40ms多
          console.time("adjacent");
          for(var i=0; i<1000; i++)
              mydiv.insertAdjacentHTML("beforeend","<div><h2>零點程序員</h2><p>由大師哥王唯主講</p></div>");
          console.timeEnd("adjacent");

          可以看到,兩者相差10ms,幾乎可以忽略不計;

          比較appendChild()與insertAdjacentElement方法的性能;

          如:把測試appendChild()方法中的mydiv.appendChild(div)改成mydiv.insertAdjacentElement("beforeend", div);即可;

          發現兩者幾乎相同;

          比較insertBefore()與以上兩者的性能;

          如:把測試appendChild()方法中的mydiv.appendChild(div),改成mydiv.insertBefore(div, mydiv.lastChild);,結束也大同小異;

          小實例,排序表格;

          基于表格指定列中單元格的值來進行排序;

          <table id="mytable"  border="1">
              <thead>
                  <tr>
                      <th>ID</th><th>Name</th><th>Sex</th>
                  </tr>
              </thead>
              <tbody>
              <tr>
                  <td>1</td><td>wangwei</td><td>女</td>
              </tr>
              <tr>
                  <td>2</td><td>jingjing</td><td>男</td>
              </tr>
              <tr>
                  <td>3</td><td>juanjuan</td><td>女</td>
              </tr>
          </tbody>
          </table>
          <script>
          // 根據指定表格每行第n個單元格的值,對第一個<tbody>中的行進行排序
          // 如果存在comparator函數則使用它,否則按字母表順序比較
          function sortrows(table, n, comparator){
              var tbody = table.tBodies[0];  // 第一個<tbody>,可能是隱式創建的
              var rows = tbody.getElementsByTagName("tr");  // tbody中所有行
              rows = Array.prototype.slice.call(rows, 0);  // 變成數組
              // 基于第n個<td>元素的值進行排序
              rows.sort(function(row1, row2){
                  var cell1 = row1.getElementsByTagName("td")[n];  // 獲得第n個單元格
                  var cell2 = row2.getElementsByTagName("td")[n];  // 同上
                  var val1 = cell1.textContent || cell1.innerText; // 獲得文本內容
                  var val2 = cell2.textContent || cell2.innerText;
                  if(comparator) return comparator(val1,val2);  // 進行排序
                  if(val1 < val2) return -1;
                  else if(val1 > val2) return 1;
                  else return 0;
              });
              // rows中已經排好序,在tbody中按它們的順序把行添加到最后
              // 這將自動把它們從當前位置移走,并不是刪除,而是移動
              for(var i=0; i<rows.length; i++){
                  tbody.appendChild(rows[i]);
              }
          }
          // 查找元素的<th>元素,讓它們可單擊,可以按該列排序
          function makeSortable(table){
              var headers = table.getElementsByTagName("th");
              for(var i=0; i<headers.length; i++){
                  (function(n){
                      headers[i].onclick = function() {
                          sortrows(table, n);
                      };
                  }(i));
              }
          }
          var mytable = document.getElementById("mytable");
          makeSortable(mytable);
          </script>

          小實例,生成目錄表:

          <style>
          #TOC{border:solid black 1px; margin:10px; padding: 10px;}
          .TOCEntry{}
          .TOCEntry a{text-decoration: none;}
          .TOCLevel1{font-size: 2em;}
          .TOCLevel2{font-size: 1.5em; margin-left: 1em;}
          .TOCSectNum::after{content: ": ";}
          </style>
          <script>
          // 當執行這個函數時會去文檔中查找id為"TOC"的元素;
          // 如果這個元素不存在,就創建一個元素
          // 生成的TOC目錄應當具有自己的CSS樣式,整個目錄區域的樣式className設置為"TOCEntry";
          // 為不同層級的目錄標題定義不同的樣式,<h1>標簽生成的標題className為"TOCLevel1",
          // <h2>標簽生成的標題className為”TOCLevel2“,以此類推;段編號的樣式為"TOCSectNum"
          function createToc(){
              // 查找TOC容器元素,如果不存在,則在文檔開頭處創建一個
              var toc = document.getElementById("TOC");
              if(!toc){
                  toc = document.createElement("div");
                  toc.id = "TOC";
                  document.body.insertBefore(toc, document.body.firstChild);
              }
              // 查找所有的標題元素
              var headings;
              if(document.querySelectorAll)
                  headings = document.querySelectorAll("h1,h2,h3,h4,h5,h6");
              else
                  headings = findHeadings(document.body, []);
              // 遞歸遍歷document的body,查找標題元素
              function findHeadings(orrt, sects){
                  for(var c = root.firstChild; c!=null; c=c.nextSibling){
                      if(c.nodeType !== 1) continue;
                      if(c.tagName.length == 2 && c.tagName.charAt(0) == "H")
                          sects.push(c);
                      else
                          findHeadings(c, sects);
                  }
                  return sects;
              }
              // 初始化一個數組來保存跟蹤章節號
              var sectionNumbers = [0,0,0,0,0,0];
              // 循環找到所有標題元素
              for(var h=0; h<headings.length; h++){
                  var heading = headings[h];
                  // 跳過在TOC容器中的標題元素
                  if(heading.parentNode == toc) continue;
                  // 獲取標題的級別
                  var level = parseInt(heading.tagName.charAt(1));
                  if(isNaN(level) || level < 1 || level > 6) continue;
                  // 對于該標題級別增加sectionNumbers對應的數字
                  // 并重置所有標題比它級別低的數字為零
                  sectionNumbers[level-1]++;
                  for(var i=level; i<6; i++) sectionNumbers[i] = 0;
                  // 將所有標題級的章節號組合產生一個章節號,如2.3.1
                  var sectionNumber = sectionNumbers.slice(0, level).join(".");
                  // 為標題級別增加章節號
                  // 把數字放在<span>中,使得其可以秀樣式修飾
                  var span = document.createElement("span");
                  span.className = "TOCSectNum";
                  span.innerHTML = sectionNumber;
                  heading.insertBefore(span, heading.firstChild);
                  // 用命名的錨點將標題包起來,以便為它增加鏈接
                  var anchor = document.createElement("a");
                  anchor.name = "TOC" + sectionNumber;
                  heading.parentNode.insertBefore(anchor, heading);
                  anchor.appendChild(heading);
                  // 為該節創建一個鏈接
                  var link = document.createElement("a");
                  link.href = "#TOC" + sectionNumber; // 鏈接目標地址
                  link.innerHTML = heading.innerHTML; // 鏈接文本與實際標題一致
                  // 將鏈接放在一個div中,div用基于級別名字的樣式修飾
                  var entry = document.createElement("div");
                  entry.className = "TOCEntry TOCLevel" + level;
                  entry.appendChild(link);
                  // 該div添加到TOC容器中
                  toc.appendChild(entry);
              }
          };
          window.onload = function(){createToc();}
          </script>

          Web前端開發之Javascript


          主站蜘蛛池模板: 国产伦精品一区二区三区精品| 成人区人妻精品一区二区不卡| 国产精品香蕉一区二区三区| 韩国福利一区二区三区高清视频| 无码人妻一区二区三区免费视频| 一区二区三区视频观看| 久久一区二区免费播放 | 精品香蕉一区二区三区| 无码少妇一区二区浪潮免费| 国产日韩综合一区二区性色AV| 亚洲AV无码一区二三区| 久久一区二区三区精品| 无码中文字幕乱码一区| 久久久久国产一区二区| 人妻体内射精一区二区三区| 免费一本色道久久一区| 国产精品视频一区二区三区不卡 | 久久精品无码一区二区app | 日韩精品无码一区二区三区| 无码人妻一区二区三区兔费| 亚洲第一区精品日韩在线播放| 最美女人体内射精一区二区| 亚洲国产激情在线一区| 一区二区三区在线免费| 亚洲一区无码中文字幕乱码| 在线播放精品一区二区啪视频| 国产一区麻豆剧传媒果冻精品| 亚洲一区电影在线观看| 成人区人妻精品一区二区不卡网站| 国模私拍一区二区三区| 精品国产一区二区三区香蕉事 | 亚洲国产精品一区第二页 | 精品国产日韩亚洲一区| 伊人色综合一区二区三区影院视频| 国产人妖视频一区二区破除| 在线观看亚洲一区二区| 成人在线一区二区| 亚洲大尺度无码无码专线一区| 国产成人精品无人区一区| 日韩欧美一区二区三区免费观看| 丰满岳乱妇一区二区三区|