想象南方老祠堂里那本越写越厚的族谱。族长手里总有一沓记录着家族名讳的手写草稿——草稿只是文字,还算不上家族本身。每逢祭祖大典,族长要做的第一件事,就是把草稿逐字念出来,拆成一句句能听懂的指令,再按辈分长幼,把每一位家人摆进一张活生生的座次树里。这张座次树,就是网页世界里的 DOM 树。HTML 源码是那沓草稿,浏览器是那位认真的族长,而 JavaScript 则是家族里最勤快的执事管家:他翻着座次树找到某人、给家族添丁、把某位成员除名、替成员改名换姓。你每天看到的点击展开、列表刷新、弹窗关闭,本质都是管家在对着这棵家族树做“找、改、添、删”四件事。这篇文章,就从族长如何“把草稿变成树”讲起,再教你学会管家翻族谱、写族谱的全部手艺。
学懂 DOM 树构建与节点操作,等于拿到了前端动态页面的总钥匙——无论日后用 jQuery、Vue 还是 React,底层技术都是这份“翻族谱”的本事。
可视化演示
动态演示DOM树构建与节点操作的核心机制,通过入门/进阶/实战/优化四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
DOM树构建与节点操作:一DOM 一、DOM树的本质与文档解析流 二、DOM节点的类型体系与继承 核心机制可视化 DOM树构建与节点操作整体架构与关键组件
构建原理流程
想象村口祠堂正在筹备一场百年大祭。族长手里是一沓手写的族谱草稿,上面密密麻麻记着“某公配某氏,生三子,长子居东厢……”。可草稿终究只是文字,不是宴席。族长必须先把草稿念出声来,拆成一句句能听懂的指令,再按辈分长幼,把每一位族人安排进祠堂的座次树里。这一步一步“念稿—拆分—入座”的过程,就是浏览器拿到 HTML 源码后搭建 DOM 树的全过程。
当服务器把 HTML 字符串交给浏览器后,浏览器的 HTML 解析器会进行两轮加工。第一轮叫词法分析(tokenization):把字符流切成一个个有名字的“标记”(Token),比如 <div class="card"> 被切成一个开始标签 Token,</div> 被切成一个结束标签 Token。第二轮叫语法分析(tree construction):解析器维护一个“当前父节点”栈,遇到开始标签就生成一个新节点挂到栈顶元素下,遇到结束标签就弹栈,最终把所有节点拼成一棵以 document 为根的树。
这棵组装出来的树就是 DOM(Document Object Model,文档对象模型),它常驻内存,与屏幕上的页面一一对应。整个构建是流式的:浏览器边下载边解析,看到没有 async/defer 的 <script> 标签时还会暂停解析、去执行脚本,因为脚本可能改写后续节点的结构——这也是为什么把 <script> 放 </body> 之前能减少阻塞的原因。树建成后,JavaScript 通过 document 对象拿到树的入口,剩下的增删改查都由 DOM API 完成。
家族聚会从来不是一秒变出来的:先有草稿,再有念稿拆分,再按辈分入座。理解了这条流水线,你就能解释很多浏览器的“怪脾气”——为什么动态插入脚本影响性能,为什么把 script 放页面底部更顺畅。因为族长念稿念到一半,是不允许管家打断改座次的;等整棵树搭好,管家才有充分的操作空间。
- 词法分析(分词)把 HTML 字符串按尖括号和引号切分成最小单位 Token,每个 Token 记录标签名和属性表,相当于族长把草稿念成一句句指令。
- 语法分析(建树)按标签的嵌套配对关系,把每一个 Token 变成一个节点并挂到当前父节点下,形成层次分明的父子关系。
- 文档根节点收束所有节点最终以 document 为根连成整棵树,JavaScript 就从这棵树的根部开始遍历和操作。
节点类型识别
翻开族谱你会发现,里面不只有“人”,还有“神主牌位”“写在牌位旁的小字注释”“整本族谱的封皮”。DOM 树也一样:除了最常打交道的元素节点(族人本人),还有文本节点(族人说的闲话、牌位上的注释文字)、属性节点(族人身上的特征标签)和文档节点(整本族谱本身)。认人先认身份,操作节点也要先认类型。
DOM 规定每个节点都有一个 nodeType 数字作为“身份腰牌”:Document 文档节点是 9,整棵树的根;元素节点是 1,对应一个 HTML 标签;文本节点是 3,对应标签之间的文字;属性节点是 2,通常通过 getAttribute/setAttribute 间接操作,很少直接创建。nodeName 则返回节点名字:元素节点返回大写标签名如 DIV,文本节点返回 #text。
在日常开发里,判断节点类型的场景非常多。比如遍历子节点时,childNodes 里会混入文本节点,而 children 只包含元素节点;如果你用 nodeType === 1 做过滤,就能精准地只操作元素。文本节点没有 id、class、style 这些元素专属能力,它的内容存在 nodeValue 或 data 里,改它对页面布局几乎没有影响。
就像族长看腰牌决定“这位能不能上主桌”,前端代码也靠 nodeType 这块腰牌决定“这个节点能不能加 class、改样式”。搞混了文本节点和元素节点,会出现“明明改了却不生效”的怪事——因为你一直在给一团“备注文字”下达只有“族人”才能执行的命令。
- 文档节点(9)整棵树的根,对应浏览器里的 document 对象,是访问一切节点的入口,相当于族谱的封皮与总目录。
- 元素节点(1)HTML 标签实例化成的成员,拥有标签名、属性、子节点,是操作的主力对象,对应家族里一个个有名有姓的人。
- 文本节点(3)标签与标签之间的文字内容,没有子节点,内容放在 data 属性里,对应牌位上那一行行小字备注。
查询元素方法
祭典前夕,管家要找人布置任务。最快的办法是直接喊大名——getElementById('老张'),全家族只有一个老张,一嗓子出去人就到了。如果只记得姓氏,喊一声“姓李的!”——getElementsByTagName('li'),呼啦啦站起来一队人。如果条件更复杂,比如“穿红衣服、坐在第二排的姑姑”,就得用一句完整的话描述——querySelector('.card .title')。
DOM 查询 API 分几大家族。getElementById 按 id 精确查找,速度最快,返回单个元素;getElementsByTagName 和 getElementsByClassName 按标签名、类名查找,返回 HTMLCollection(动态集合);querySelector 接受任意 CSS 选择器,返回第一个匹配元素;querySelectorAll 返回所有匹配元素组成的 NodeList(静态集合)。
“动态”与“静态”的区别是高频考点:动态集合就像一个“实时直播窗口”,每次读取它都会去文档里重新数一遍;静态集合则是查询那一瞬间的“合照”,之后文档怎么变,照片里的人都不变。所以如果你边遍历边往页面里加同标签元素,动态集合会越数越多——这一点在误区部分还会细聊。
管家找人从“喊名字”学到“说条件”,越来越省力。现代代码里 querySelector 出现频率最高,因为它把“找什么样的人”浓缩成一句话。但记住:喊名字(getElementById)永远最快,条件简单时别扛着选择器这个重武器满街跑。
- 精确查找按 id 属性直接定位唯一元素,引擎内部有哈希索引,几乎不遍历整棵树,对应管家扯着嗓子喊全名。
- 标签集合收集文档中所有同名标签组成动态集合,每次访问都与实时文档同步,对应“姓李的”一声招呼站起来的一队人。
- 选择器匹配用 CSS 选择器描述条件组合,返回第一个匹配节点,对应“穿红衣服坐第二排的姑姑”这种复合寻人法。
创建插入节点
家族要添丁,绝不能从天上掉下一个活人。管家先要去登记处“报户口”——document.createElement('div') 在内存里造出一个叫"div"的新成员;然后决定把他挂到哪一房之下——parent.appendChild(child) 把他记到父节点的子列表末尾;如果想插在某位哥哥前面,用 parent.insertBefore(newChild, refChild) 精准落座。
这三个动作对应 DOM 最核心的三个 API。createElement 创建的节点一开始是“游离状态”:它存在于内存,但不在文档树里,页面看不到它,直到被 appendChild 或 insertBefore 挂载。appendChild 把新节点追加为父节点的最后一个孩子;insertBefore 接受两个参数,把新节点插到参考节点之前。若参考节点传 null,则效果等同于 appendChild。
批量添加几十个节点时,如果每加一个都直接挂到页面上,浏览器就会一次又一次地重新计算布局,非常浪费。这时用 document.createDocumentFragment() 创建一个“草稿本”(文档碎片),把新节点先全部登记在草稿本里,最后一次性把草稿本挂到父节点下,页面只重排一次——这就是性能优化的经典手法。
添丁讲究“先报户口、再选位置、批量时用草稿本”。家族不会为每一个新人都重新摆一次全族宴席,管家也绝不会为每一条数据都刷新一遍页面——把“批量新增”攒成“一次入册”,正是前端工程师从小白走向熟练的标记。
- 创建节点在内存中造出一个游离的新元素,此时它不属于任何父节点,页面不可见,相当于刚报完户口还没入族谱的人。
- 尾部追加把新节点挂到父节点子列表的末尾,父节点子元素数量加一,对应家族记谱本末页添一行。
- 指定位置插入把新节点放到参考节点之前,参考节点及之后的所有兄弟顺势后移,对应插队入座但绝不打乱后面秩序。
- 文档碎片缓冲碎片是一个脱离文档树的临时容器,批量攒好再一次性挂载,减少页面重排次数。
删除替换节点
有添丁就有除名。家族规矩讲究“谁的孩子谁管教”:要从族谱里划掉某个人,必须由他的父辈来执行。DOM 里对应 parent.removeChild(child)——只有父节点能删除子节点。若是要换人继位,parent.replaceChild(newChild, oldChild) 让新人坐在旧人的同一个位置上,连座位号都不用变。
新手常直觉地用 node.remove()(现代浏览器支持)或更经典的 node.parentNode.removeChild(node)。前者简洁,后者是兼容性最稳的写法。replaceChild 相当于“原地替换”:旧节点从树中断开,新节点占用它在 children 中的原下标,父元素的子节点数量保持不变。
想清空一个容器下所有子节点,不要用 container.innerHTML = '' 这种粗暴方式(它会额外走一遍 HTML 解析),更稳妥的是循环删除:while (container.firstChild) container.removeChild(container.firstChild); 每次移除第一个孩子,直到没有孩子为止。注意 removeChild 会返回被删除节点的引用,如果之后还要复用它,可以把返回值接住。
删除和替换都强调“从父节点发起”,因为 DOM 树靠 parent 指针维系血缘;断了一条链,整棵树就散了。还要记得循环里“边走边删”的坑——队伍在前移,你的计数还停在原地,会跳过本该处理的成员。
- 移除子节点父节点调用该方法删除指定孩子,被删节点的 parent 指针变为 null,彻底脱离家族树。
- 替换子节点新节点原位顶替旧节点,数组中旧节点的下标被新节点占用,兄弟关系无缝衔接。
- 循环清空反复删除第一个孩子直到没有孩子,最后子节点数量归零,容器变回“空房间”。
属性内容操作
在家族里,改个名字、换件衣裳、理个发型,都是日常。DOM 的属性与内容操作也是这么三件事:setAttribute('class', 'active') 相当于给成员别上一枚"active"徽章;classList.add/remove/toggle 则是穿衣、脱衣、翻衣领的优雅动作;textContent 只改文字备注,innerHTML 则把整间屋子推倒重建。
textContent 读取时会把节点及其所有后代里的文本节点内容拼接起来;写入时先清空所有子节点,再创建一个文本节点存下字符串——传进去的 <b>hi</b> 会被当成普通文字,不会生成 b 元素。innerHTML 则完全相反:赋值时浏览器会对字符串做一次完整的 HTML 解析,把它变成一段子树挂进去。
这两个 API 是最容易踩 XSS 的入口。如果用 innerHTML 插入用户输入的内容,用户提交的 <img onerror="alert(1)"> 会被解析成真实标签并执行脚本;而 textContent 会把整段文字原样显示,天然安全。所以原则是:改文字用 textContent,确认内容可信、确需结构时才用 innerHTML。
管家改家族档案,必须分清“改字”和“盖房子”。只说一句话,就改牌位上的备注;要换格局,才拆屋重建,而且一定要确认建材没有夹带危险品(脚本)。这个分寸感,就是新手与老手的一道分水岭。
- 读取拼接文本把节点下所有后代文本节点的 data 按顺序拼接成一个字符串,不含任何标签,对应把牌位上所有小字连起来读一遍。
- 写入纯文本清空原有子节点,只生成一个文本节点来存放字符串,尖括号等特殊字符原样显示。
- 解析 HTML 子树innerHTML 把字符串当作 HTML 源码走一遍词法+语法分析,生成一段子树再挂载,对应按图纸拆旧屋盖新房。
节点遍历关系
管家要按辈分巡访整个家族,从任何一位成员出发都能走遍全族:向上问 parentNode 找到父亲,向下问 children 找到儿女们,左右问 nextSibling / previousSibling 找到兄弟。沿着这几条通道,整棵家族树畅通无阻——这就是 DOM 遍历。
常用的关系属性里,parentNode 返回父节点;children 只统计元素子节点,而 childNodes 会把文本节点也算进去;firstElementChild / lastElementChild 取第一个、最后一个元素孩子;nextElementSibling / previousElementSibling 取相邻的兄弟元素。注意带 Element 的版本会跳过文本节点,遍历时能省去大量 nodeType 过滤。
遍历时最怕“边走边改地图”。如果你在遍历 children 的同时删除成员,下标会错位,漏掉本应处理的节点;如果遍历的是动态集合(HTMLCollection),新增节点还会让集合越变越长,甚至死循环。稳妥做法是先把集合转成静态数组(如 Array.from(children) 或 [...children])再遍历修改。
走亲访友要先懂“谁是我爸、谁是我儿、谁是我兄弟”。前端代码里最常见的操作就是“从按钮往上摸到表单,再往下摸到列表项,再往旁找到同级的输入框”——这套在家族树里摸爬滚打的本事,是 DOM 操作的底层地图。
- 向上找父每个节点最多只有一个父节点,通过 parentNode 属性一步到达,对应“这是我爹”的直系认定。
- 向下找子children 返回所有元素子节点的集合,逐个访问即可递归遍历整棵子树,对应“这是我的儿女们”。
- 横向找兄弟同父节点的相邻子节点互为兄弟,通过 nextSibling / previousSibling 左右移动,对应按长幼排序的辈分名单。
AI 应用场景
电商订单列表的批量渲染
阶段:数据驱动渲染(前端展示层)
解决的问题:后端返回商品数组,传统做法是拼 HTML 字符串再整块 innerHTML,既慢又容易 XSS。用 DocumentFragment + createElement 把每条商品数据变成一个节点,攒好一次性挂载,页面只重排一次,性能提升明显。
具体案例:某电商后台管理页的“今日订单”表格,接口返回 200 条订单。前端先用 fragment 批量创建 200 个 tr 节点并填充 textContent,再一次性 appendChild 到 tbody;用户滚动到底部时再增量追加下一批,全程无整页刷新。
表单实时校验提示
阶段:交互反馈(用户输入即时校验)
解决的问题:传统表单要提交后才能知道哪里填错,体验很差。用 querySelector 定位输入框,classList.toggle('input-error') 即时切换错误样式,再用 textContent 更新错误文案,不刷新页面就能边填边提示。
具体案例:注册页密码框失焦时,JS 用 getElementById('pwd') 拿到输入框,检查长度后用 classList 控制红色边框样式,同时把“至少 8 位”等提示写入旁边 span 的 textContent;符合条件则 classList 换回绿色成功样式。
单页应用的无刷新视图切换
阶段:前端路由渲染
解决的问题:传统多页面每次跳转都整页刷新,白屏体验差。SPA 用 replaceChild 把“上页面视图”替换成“下页面视图”,容器节点不变,只换子树,实现无刷新导航。
具体案例:后台系统的侧边栏菜单点击后,路由解析出目标视图的根节点,mainContainer.replaceChild(newView, oldView) 完成切换,离开页面时再用 removeChild 清理不再需要的节点,避免内存泄漏。
Python 演示
import html.parser class Node: """模拟 DOM 树中的一个节点""" def __init__(self, tag, attrs=None): self.tag = tag self.attrs = dict(attrs or []) self.children = [] self.parent = None def append_child(self, child): child.parent = self self.children.append(child) return child def remove_child(self, child): if child in self.children: self.children.remove(child) child.parent = None def get_element_by_id(self, target_id): if self.attrs.get("id") == target_id: return self for child in self.children: result = child.get_element_by_id(target_id) if result is not None: return result return None def query_selector_all(self, tag): result = [] if self.tag == tag: result.append(self) for child in self.children: result.extend(child.query_selector_all(tag)) return result def display(self, depth=0): line = " " * depth + "<" + self.tag for key, value in self.attrs.items(): line += ' %s="%s"' % (key, value) line += ">" print(line) for child in self.children: child.display(depth + 1) class TinyHTMLParser(html.parser.HTMLParser): """把 HTML 字符串解析成 Node 树""" def __init__(self): super().__init__() self.root = None self.stack = [] def handle_starttag(self, tag, attrs): node = Node(tag, attrs) if not self.stack: self.root = node else: self.stack[-1].append_child(node) self.stack.append(node) def handle_endtag(self, tag): if self.stack and self.stack[-1].tag == tag: self.stack.pop() if __name__ == "__main__": sample = ( '<html><body>' '<header id="top">主页</header>' '<main><h1>标题</h1><p>段落</p></main>' '<footer>底部</footer>' '</body></html>' ) parser = TinyHTMLParser() parser.feed(sample) print("=== 解析出的 DOM 树 ===") parser.root.display() main_node = parser.root.query_selector_all("main")[0] button = Node("button", {"id": "btn", "class": "btn-blue"}) main_node.append_child(button) print("\n=== appendChild 添加 button 之后 ===") parser.root.display() found = parser.root.get_element_by_id("btn") print("\n=== getElementById 找到 ===") print(found.tag, found.attrs) footer = parser.root.query_selector_all("footer")[0] footer.parent.remove_child(footer) print("\n=== removeChild 移除 footer 之后 ===") parser.root.display()
self.children = []每个节点持有一个孩子列表,对应 DOM 的 childNodes,是树形结构的核心。child.parent = self挂载孩子的同时登记 parent 指针,有了它才能“谁的孩子谁管教”。self.stack[-1].append_child(node)把新节点挂到栈顶父节点下,模拟浏览器语法分析的建树过程。parser.root.display()从根节点递归打印整棵家族树,相当于在控制台查看 DOM 树形结构。main_node.append_child(button)对应页面里 appendChild 动态添加节点,新节点立刻成为树的一部分。footer.parent.remove_child(footer)删除必须由父节点发起,对应 DOM 的 removeChild 语义。result.extend(child.query_selector_all(tag))深度优先收集所有匹配标签的节点,对应 querySelectorAll 的遍历逻辑。
常见误区
误区我一直用 innerHTML 给元素塞文本,换成 textContent 效果也差不多吧,反正页面上都显示出来了。
正解差远了。textContent 把内容当纯文本,<b>加粗</b> 会带着尖括号原样显示;innerHTML 会解析 HTML 标签,生成真实的加粗元素。更危险的是,用 innerHTML 插入用户输入等于引狼入室——XSS 攻击最爱从这里进来。规矩是:改文字用 textContent,确需生成结构且内容可信时才用 innerHTML。
为何常见:新手看到两个 API 都能“改内容”,只观察到“结果都显示在页面上”,没留意底层一个是“当字写”,一个是“当图纸盖房子”。
误区我调用 getElementsByTagName('li') 得到一份列表,之后页面怎么变,这份列表应该都不变吧?
正解恰恰相反,它返回的是动态集合 HTMLCollection,每次访问都实时同步文档。你拿到的不是“照片”,而是“实时直播”。想要快照要用 querySelectorAll 返回的 NodeList(静态集合)。如果边遍历边给容器添加 li,动态集合的长度会一直增长,稍不留神就是死循环。
为何常见:名字里带“Elements“太像”查询结果集合“,很容易让人以为是查询那一瞬间固化的数组;直到有人在循环里被它坑出死循环,才真正明白”动态”二字的含义。
误区我想把同一个按钮同时放到两个容器里,用 appendChild 复制两次不就行了吗?
正解appendChild 不会复制节点,它会把原节点从原位置“连根拔起”,再挂到新位置——同一个节点在整棵树里只能有一个父节点。想复制得先 cloneNode(true) 生成深拷贝,那才会出现两个独立的按钮。
为何常见:生活里“把文件复制到另一个文件夹”的直觉太强了,而 DOM 的语义更接近“搬家”而不是“复印”,所以第一次用的人几乎都会踩这个坑。
误区删除节点不就是 node 自己调用一个删除方法吗?为什么我写了 node.removeChild(node) 却报错说 removeChild 未定义?
正解经典 API 是 parentNode.removeChild(child),删除权在父节点手里;node.remove() 是较新浏览器提供的便捷方法,老旧环境不支持。报错通常是因为调用对象搞反了——你在子节点上调用了父节点才拥有的方法,好比让儿子去开除他爹,规矩上说不通。
为何常见:面向对象的直觉是“对象管理自己”,但 DOM 的删除语义刻意强调“父子关系”,新手容易把方法的归属对象搞颠倒。
面试题
Q1. 要读取某个元素内的纯文本(不含 HTML 标签),最合适的属性是什么?
- A.textContent 属性← 正确答案
- B.innerHTML 属性
- C.outerHTML 属性
- D.value 属性
答案:A。textContent 专门负责拼接所有后代文本节点的内容,完全不解析标签;innerHTML 会返回包含标签的字符串;outerHTML 连当前元素自身标签一起返回;value 是表单控件的专用属性。这就像读牌位文字选“读字”而非“连房子一起看”。
Q2. document.getElementById('app') 的返回值是什么?
- A.一个包含所有元素的数组
- B.第一个匹配的元素节点← 正确答案
- C.匹配元素的字符串描述
- D.一个布尔值表示是否存在
答案:B。getElementById 按 id 精确查找,id 在整个文档中唯一,所以它直接返回那一个元素节点,而不是数组。找不到时返回 null,也不是布尔值。这就像管家喊全名,回来的是“那个人”而不是“一份名单”。
Q3. 下面哪个方法可以在内存中创建新的元素节点?
- A.document.createDiv('div')
- B.document.createElement('div')← 正确答案
- C.document.newElement('div')
- D.document.makeElement('div')
答案:B。标准 API 是 document.createElement(tagName),它返回一个脱离文档树的游离节点,调用后还需要 appendChild 等挂载方法才能上树。其他三个方法都不存在,属于臆造 API。这就像“报户口”只有一个正规窗口,其他窗口都是假的。
Q4. 把页面上已存在的节点 A 用 appendChild 挂到新父节点下,会发生什么?
- A.在页面上复制一份 A 节点
- B.原位置的 A 被移动到新位置← 正确答案
- C.页面上同时出现两个相同节点
- D.抛出一个运行时类型异常
答案:B。appendChild 的语义是“移动”而非“复制”:同一节点在文档树中只能有一个父节点,所以它会被先从原位置移除,再挂到新父节点的末尾。想复制必须用 cloneNode(true)。这就像把同一个人从东厢房搬到西厢房,他不可能同时出现在两间屋里。
Q5. querySelectorAll 与 getElementsByTagName 返回的集合,最重要的区别是什么?
- A.前者是动态集合后者是静态集合
- B.前者是静态集合后者是动态集合← 正确答案
- C.两者都是动态集合
- D.两者都是静态集合
答案:B。querySelectorAll 返回静态 NodeList,是查询瞬间的“合影照片”;getElementsByTagName 返回动态 HTMLCollection,是实时同步文档的“直播窗口”。文档变化后,动态集合会自动反映变化,静态集合不会。这个区别在遍历中添加元素时会导致完全不同的行为。
Q6. 下列哪些写法可以移除页面中的节点 node?
- A.node.parentNode.removeChild(node)← 正确答案
- B.node.remove()← 正确答案
- C.node.deleteNode()
- D.parent.innerHTML = ''← 正确答案
答案:ABD。A 是经典写法,由父节点执行删除;B 是 modern API,节点自己调用即可;C 的 deleteNode 方法并不存在;D 虽然粗暴,但如果 parent 是 node 的父节点,清空 innerHTML 会连带把 node 一起移除,只是会误伤兄弟节点。前两种精准,后一种属于“拆整屋”。
Q7. 使用 DocumentFragment 批量添加节点的主要好处是什么?
- A.可以跨浏览器访问任意节点
- B.减少多次操作引发的性能开销← 正确答案
- C.让新节点自带样式效果
- D.让代码执行速度变慢便于调试
答案:B。DocumentFragment 是一个游离的轻量容器,往里面添加节点不会触发页面的布局计算;最后一次性挂到父节点下,页面只重排一次。这就像族谱登记先用草稿本攒着,最后一页一页誊清,而不是每添一人就重翻一遍全册。
Q8. 关于 innerHTML 与 textContent,下列说法正确的是?
- A.两者设置内容的效果完全一致
- B.innerHTML 赋值会重新解析 HTML 标签← 正确答案
- C.textContent 会解析字符串中的标签
- D.innerHTML 只能读取不能写入
答案:B。innerHTML 赋值时浏览器会对字符串走词法、语法分析,把 <p>hi</p> 变成真实的 p 元素子树;textContent 则把字符串当纯文本,尖括号原样显示,绝不解析标签。两者都可读可写。用 innerHTML 插入不可信内容会引入 XSS 风险,这是它和 textContent 最本质的分歧。
Q9. 关于 DOM 树的构建过程,下列描述正确的是?
- A.先对 HTML 字符流做词法分析← 正确答案
- B.Token 直接渲染成屏幕像素不生成节点
- C.按嵌套关系把 Token 组装成节点树← 正确答案
- D.文档解析完成后 DOM 树永远不能改变
答案:AC。浏览器先把 HTML 字符流切成 Token(词法分析),再按标签嵌套关系把 Token 组装成节点树(语法分析),A 和 C 正确。B 错在 Token 之后还要生成节点、布局、绘制才能上屏;D 错在 DOM 树建成后依然可被 JavaScript 增删改查,这正是“动态页面”的根基。整个过程好比族长念稿、拆分、再按辈分入座。
Q10. 执行以下代码时会发生什么?
- A.正常遍历固定次数后结束
- B.items 是动态集合导致死循环← 正确答案
- C.第一次循环后就抛出异常
- D.只创建了一个新 li 就停止
答案:B。getElementsByTagName 返回的是动态 HTMLCollection,每轮循环调用 items.length 都会重新统计,而循环体里又在不断新增 li,导致 length 永远追不上 i,形成死循环。解决方案是先把集合转成静态数组或先缓存初始长度。这就像边数人头边往队伍里加人,永远数不到头。