整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

用原生 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对象使用该属性,如果使用了,会静默失败;

设置了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