從昨天文章的 FB 貼文留言學到新東西。

身為 IE 出身的開發者,用 innerHTML 讀寫元素 HTML 原始碼,用 innerText 讀寫純文字版內文已經是本能反應。讀者 Chester Fung 提醒我一件事,最好用 textContent 取代 innerText,不然遇到隱藏元素文字會消失。一查,這才後知後覺學到二者差異。

依據 MDN 的說明

Node.innerText 是一個代表節點及其後代之「已渲染」(rendered)文字內容的屬性。Node.innerText 近似於使用者利用游標選取成高亮後複製至剪貼簿之元素的內容。此功能最初由 Internet Explorer 提供,並在被所有主要瀏覽器採納之後,於 2016 年正式成為 HTML 標準。
Node.textContent 屬性是一個相似的選擇,但兩者之間仍有非常不同的差異。

而 innerText 與 textContent 的差異如下:

  • textContent 包含 <script><style>,innerText 只包含使用者會讀取到的部分
  • textContent 包含所有子孫元素,innerText 可察覺樣式,忽略隱藏元素
  • innerText 會觸發 Reflow 重繪頁面以確保得到最新樣式結果,效能成本稍高

我設計了以下實驗對照二者差異,確認 innerText 在不同情境下的行為:

如上圖,共有七組測試,由 1, 2, 3 可知,刪節點或超出範圍看不到並不影響 innerText 讀取,其結果與 textContext 相同(滑鼠選取複製得到的也是完整內容)。

由測試 4,text-transform: uppercase 改變大小寫後,innerText 會讀到全大寫內容。

測試 5 當內含元素 display: none,innerText 讀不到東西,但故意在其中放了 <script>console.log('Hi');</script>console.log('Hi'); 會被 textContent 算成 td 的內部文字。

測試 6 加了 <br />,innerText 會解析成 '\n' 換行符號,textContext 則無視。

測試 7 將 World! 包成 SPAN 設 display: none,在 innerText 會消失,但 textContent 不受影響。

線上展示

【結論】

如果元素內容有透過 CSS 隱藏部分內容、修改文字大小寫... 時,而你想擷取使用者看到的文字樣貌(註:不包含超出範圍隱藏的效果),可用 innerText。若要取未受樣式影響的原始文字內容,則用 textContent,但如果在元素內有穿插 <script><style> 等非可見內容,textContent 會將區塊內的文字也算進去,可能產非非預期的結果。

附上完整程式範例:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>innerText, textContent</title>
    <style>
        .container {
            border-collapse: collapse;
            font-family: Verdana, Geneva, Tahoma, sans-serif;
            font-size: 10pt;
        }
        td { border: 1px solid gray; padding: 3px 6px; }
        thead { text-align: center; background-color: #eee; }
        tbody tr:nth-child(even) { background-color: #ffa2; }
        tbody td:nth-child(1) { text-align: center; }
        tbody td:nth-child(3) { color: #00fd; }
        tbody td:nth-child(2) div {
            width: 70px;
            white-space: nowrap;
            border: 1px solid gray;
        }
    </style>
</head>

<body>
    <table class="container">
        <thead>
            <tr>
                <td>No</td>
                <td style="width: 120px">Element</td>
                <td>td.innerHTML</td>
                <td>td.innerText</td>
                <td>td.textContent</td>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>1</td>
                <td><div style="overflow: hidden; text-overflow: ellipsis">Hello, World!</div></td>
                <td></td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td>2</td>
                <td><div  style="overflow: hidden">Hello, World!</div></td>
                <td></td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td>3</td>
                <td><div  style="overflow-x: scroll">Hello, World!</div></td>
                <td></td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td>4</td>
                <td><div  style="text-transform: uppercase">Hello, World!</div></td>
                <td></td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td>5</td>
                <td><div  style="display: none;">Hello, World!<script>console.log('Hi');</script></div></td>
                <td></td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td>6</td>
                <td><div>Hello,<br /> World!</div></td>
                <td></td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td>7</td>
                <td><div>Hello,<span style="display: none"> World!</span></div></td>
                <td></td>
                <td></td>
                <td></td>
            </tr>
        </tbody>
    </table>
    <script>
        const formatHtml = html => {
            return html.replace(/(&lt;div.*?&gt;)/, '$1<div style="color:brown">')
                .replace(/(&lt;\/div&gt;)/, '</div>$1');
        };
        document.querySelectorAll('tbody tr').forEach(d => {
            const tds = [...d.querySelectorAll('td')];
            tds[2].textContent = tds[1].innerHTML;
            tds[2].innerHTML = formatHtml(tds[2].innerHTML);
            tds[3].innerHTML = `<pre>${tds[1].innerText}</pre>`;
            tds[4].innerHTML = `<pre>${tds[1].textContent}</pre>`;
        });
    </script>
</body>

</html>

An explanation and an experiment on the difference between innerText and textContent in JavaScript.


Comments

# by Latishaaaaaa

想請教黑暗大,.textContent() 跟 .text() 又差在哪裡呢?在stackoverflow看到的說法是 The textContent property is "inhertied" from the Node interface of the DOM Core specification. The text property is "inherited" from the HTML5 HTMLAnchorElement interface and is specified as "must return the same value as the textContent IDL attribute". The two are probably retained to converge different browser behaviour, the text property for script elements is defined slightly differently. 看起來主要是為了融合不同的瀏覽器行為。除此之外還有其他的嗎...? 那是否代表用兩種產生的結果其實差別不大呢?

# by Latishaaaaaa

(補一下說法) 也有看到說 html 文字如"<b>This is bold text</b>" 分別使用.textContent() 跟 .text()設定當作文字時,前者會直接顯示<b>This is bold text</b>,後者則會顯示特效後文字(粗體)。但若是不考慮傳入html文字的純文字狀況下,兩者是否就沒有差別呢?

# by Jeffrey

to Latishaaaaaa,.text()設定會顯示特效後文字(粗體),這幾乎是 innerHTML 的效果了。經驗中我沒看過有人用 HTML5 text(),也想不出非它不可的應用情境,我打算忽略這個 API,減輕腦力負擔,哈。

# by Latishaaaaaa

感謝黑暗大的回覆!那麼我主力還是放在 innerHTML 與 textContent 好了。

# by toradoraa

今天在找搜尋 table 功能的bug時,發現有未包含搜尋關鍵字的 row 也被渲染出來,原來是隱藏的文字textContent仍會被計算!獲益良多

Post a comment