news 2026/10/17 11:56:42

JavaScript DOM操作实战:查询、遍历、增删改与事件委托

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript DOM操作实战:查询、遍历、增删改与事件委托

1. 这篇内容解决什么问题:从“会写JS”到“会操作网页”

很多人学JavaScript学到循环、函数、数组就觉得差不多了,但一回到真实项目里,马上懵掉:数据拿到了,怎么把它放进页面里?用户点了按钮,怎么让某个列表项消失?表单勾了几个选项,怎么判断到底选了哪些?这些场景绕不开同一个核心能力——DOM操作。这篇内容围绕“JavaScript基础12-day14”展开,覆盖了DOM查询、节点遍历、全选反选、节点增删改、动态添加删除记录、Window对象方法这几个高频主题,正好是“会写JS”和“会操作网页”之间最关键的一步。

我把这一篇定位为“从0到1的DOM实战起点”,适合两类人:一类是刚学完JavaScript基础语法、想往实际页面开发过渡的初学者,另一类是做过一些静态页面、但对DOM操作逻辑一直稀里糊涂、每次写增删改都靠复制粘贴的开发者。阅读前你只需要掌握基本的变量、函数、数组和简单的循环,不需要任何框架知识,原生JavaScript就能跑通全部内容。

这篇文章里我不打算只贴代码,我希望把每条API背后的“为什么这样用”讲清楚。比如为什么获取元素要分getElementById和querySelector,为什么遍历子节点时childNodes会经常出问题,为什么给动态创建的元素绑定事件要用事件委托。用实际场景把这些知识点串起来,这才是从基础语法走向真实开发的关键一步。

2. DOM查询:获取元素节点的几种方式和它们的使用边界

2.1 五种查询API的适用场景与取舍

DOM查询是后续所有操作的前提。JavaScript里获取元素节点的方式有不少,我把最常用的列成一个表,方便对照:

查询方式写法示例返回结果典型使用场景
按id获取document.getElementById('goodsList')单个元素或null页面里唯一的容器、父节点
按类名获取document.getElementsByClassName('item')HTMLCollection(动态集合)多个同类型元素批量操作
按标签名获取document.getElementsByTagName('li')HTMLCollection(动态集合)标签结构规范的同类元素
选择器获取单个document.querySelector('#box .title')第一个匹配的元素或null结构层级较深的定位
选择器获取多个document.querySelectorAll('.item')NodeList(静态集合)配合forEach遍历操作

这里有个特别值得注意的点:getElementsByClassName返回的是动态集合,而querySelectorAll返回的是静态集合。怎么理解动态和静态?举个例子——你用getElementsByClassName('item')获取了一个集合,然后往页面里新增了一个带item类名的元素,这个集合会自动把新元素算进去,长度自动变化;但如果你用querySelectorAll('.item')获取集合之后再新增元素,这个NodeList的长度不会变,新元素不会被自动加进去。这个差异在“全选练习”和“动态添加记录”两个场景里影响很大,后面我还会专门提到。

2.2 querySelector和getElementById怎么选

很多初学者特别喜欢querySelector,因为它写法灵活,一个方法走天下。但我要说的是:项目里按id获取时,我更推荐getElementById。原因有两个:一是写法上更明确,读代码的人一眼就能看出你在按id找元素;二是实测性能上getElementById比querySelector快,尤其在循环里频繁查询时差距更明显。当然对于一般页面,这点性能差异可忽略,但“按id就该用getElementById”这个习惯能让代码更有语义。

反过来,如果需要根据类名+结构层级找元素,比如获取表格里某一行的某个单元格,这时querySelector就比多次调用getElementsByClassName方便得多。这两种方法不是替代关系,是互补关系。建议你记住一个简单原则:能直接拿到id就用getElementById,需要按层级、按结构定位就用querySelector。

2.3 查询不到元素时的一个隐蔽问题

查DOM时很容易忽略的一个问题:脚本位置。如果script标签放在页面头部,而你要获取的DOM元素在body里,这时document.getElementById返回的是null,因为浏览器从上往下解析HTML,执行到这个script时目标元素还没解析出来。解决方式有三种:把script放到body末尾、在DOMContentLoaded事件里执行代码、或者使用defer属性。这个不细讲了,但这个坑一定要知道,不然照着教程写半天,所有DOM都是null,心态直接崩。

3. 遍历子节点:childNodes与children的差别,以及为什么firstChild经常掉链子

3.1 childNodes和children到底差在哪

获取了父节点之后,接下来常见的操作是获取它的子节点。这里有两个方法很容易混:childNodes和children。光看名字,很多人以为它们差不多,实际差别很大。

childNodes获取的是父节点的所有子节点,注意“所有”这个词——它不仅包含元素节点,还包含文本节点(比如换行符、空格)和注释节点。写HTML时我们习惯性地换行缩进,这些换行符在DOM解析时会被当成文本节点。所以当你用childNodes拿到一个列表,里面经常出现一堆#text节点,索引明明想取第一个子元素,结果取到的是一个换行符文本。

children则只返回元素子节点,也就是nodeType为1的那些节点。文本节点、注释节点统统被过滤掉。绝大多数业务场景里,我们操作DOM就是操作元素节点,所以children使用频率远高于childNodes。

3.2 firstChild、firstElementChild的典型“翻车现场”

因为文本节点的存在,firstChild经常返回一个文本节点而不是元素节点。比如:

const list = document.getElementById('list'); // list内部的HTML是 // <li>商品一</li><li>商品二</li> // 但HTML源码里可能写成 // <li>商品一</li> // <li>商品二</li> const first = list.firstChild; // 这个first可能是文本节点(换行符),不是li元素

于是很多初学者调试半天,发现firstChild拿到的东西和预期完全不符。解决办法就是使用firstElementChild,它专门获取第一个元素子节点。同理,lastChild对应lastElementChild,nextSibling对应nextElementSibling,previousSibling对应previousElementSibling。

我个人的建议非常简单粗暴:操作子节点和兄弟节点时,默认全部用带Element后缀的那一套。只有当你明确想要包含文本节点时,才回头用不带后缀的版本。这样做至少能避开80%的“明明有子元素但取不到”的奇怪问题。

3.3 从父节点出发的逆向查询

除了从父到子,DOM还支持从子到父、从子到兄弟的逆向查询,比如parentNode获取父节点,parentElement获取父元素(在大多数情况下两者相同)。这个在后续“添加删除记录”里很有用——点击删除按钮时,需要找到当前按钮所在的整行节点,通常就是先拿到按钮,再通过parentNode向上找到tr或者容器节点。

4. 全选练习:一个checkbox联动需求里的完整逻辑拆解

4.1 需求先想清楚:到底有哪些联动规则

标题里的“全选练习”,几乎每个做前端的人都写过。看起来简单,实际上把边界条件想清楚不容易。先看需求:页面上有一个商品列表,每一行前面有一个checkbox,列表顶部还有一个“全选”按钮(或全选checkbox)。要求:

  1. 点击全选checkbox,所有子项都选中;
  2. 再点一次全选,所有子项都取消;
  3. 手动勾选/取消某个子项后,全选checkbox的状态要同步变化——所有子项都被选中时,全选自动勾上;只要有一个没选中,全选就取消勾选;
  4. 更完整的场景还要支持反选:把当前选中的变成未选中,未选中的变成选中。

这几条规则里,最容易出问题的是第3条——全选框和子项之间的双向同步。很多初学者只实现了“全选影响子项”,忘了“子项反推全选状态”,结果手动取消一个子项后,全选框还亮着,逻辑就露馅了。

4.2 双向同步的实现思路

先搭HTML结构:

<div id="goodsPanel"> <label><input type="checkbox" id="checkAll"> 全选</label> <ul id="goodsList"> <li><input type="checkbox" class="itemCheckbox"> 商品A</li> <li><input type="checkbox" class="itemCheckbox"> 商品B</li> <li><input type="checkbox" class="itemCheckbox"> 商品C</li> </ul> <button id="btnReverse">反选</button> </div>

全选box的change事件处理逻辑:

document.getElementById('checkAll').addEventListener('change', function() { const checkboxes = document.querySelectorAll('.itemCheckbox'); const isChecked = this.checked; checkboxes.forEach(function(cb) { cb.checked = isChecked; }); });

子项反推全选状态的核心逻辑:

const itemCheckboxes = document.querySelectorAll('.itemCheckbox'); itemCheckboxes.forEach(function(cb) { cb.addEventListener('change', function() { const allChecked = Array.from(itemCheckboxes).every(function(item) { return item.checked; }); document.getElementById('checkAll').checked = allChecked; }); });

这里的技巧在于Array.from(itemCheckboxes).every(...)——把NodeList转成真正的数组,再用every判断“是否所有子项都选中”。注意不能直接用itemCheckboxes.every,因为NodeList本身没有every方法,虽然部分浏览器实现了forEach,但every仍然不是普遍可靠的方法,转一次数组最稳。

4.3 反选逻辑和“事件触发时机”问题

反选逻辑本身不复杂:

document.getElementById('btnReverse').addEventListener('click', function() { const checkboxes = document.querySelectorAll('.itemCheckbox'); checkboxes.forEach(function(cb) { cb.checked = !cb.checked; }); // 反选之后需要重新计算全选框状态 const allChecked = Array.from(checkboxes).every(cb => cb.checked); document.getElementById('checkAll').checked = allChecked; });

但很多人在这个练习里会遇到一个问题:反选之后全选框状态没有同步。因为你是用JS直接改了checkbox的checked属性,这并不会触发change事件,子项各自绑定的事件处理器根本没机会执行。这是DOM操作里一个非常经典的认知点:用JS修改元素状态时,不会自动触发对应事件,所以必须在这次修改之后手动补一次状态同步。

这一点在做“动态添加删除记录”时同样会出现,比如新增一行记录、默认勾选一个新checkbox,这一行自身的联动事件怎么绑定?直接用addEventListener绑定一个新节点没问题,但如果列表数据量大了,每个子项都单独绑事件,性能和后续维护都比较麻烦,所以接下来我要重点说事件委托。

5. DOM增删改:创建节点、插入节点、删除节点的正确姿势

5.1 创建节点的四种方式对比

DOM增删改,核心方法其实不多,但每个方法的“脾气”不太一样。先看创建环节,常见的有document.createElement、document.createTextNode、element.innerHTML以及element.insertAdjacentHTML。

document.createElement('li')创建一个空的元素节点,适合需要精细控制属性的场景;createTextNode创建纯文本节点,适合防止用户输入内容被解析成HTML的安全场景;innerHTML是“快捷方式”,把一大段HTML字符串直接塞进去,写法最省事;insertAdjacentHTML则是把HTML字符串插入到指定位置的邻近处。

我平时怎么选?如果只是往一个容器里塞一块带模板性质的静态HTML,用innerHTML或insertAdjacentHTML最合适。如果元素还需要绑定事件、设置数据、后续单独操作,用createElement结构化创建更顺手。涉及用户输入的内容,绝对不要直接拼到innerHTML里,那样会产生XSS风险,应该使用textContent或createTextNode。

5.2 插入节点的三个方法

创建完节点之后,需要把它放进页面。常用这三个方法:

方法作用使用场景
appendChild(child)把节点追加为父元素的最后一个子节点在列表末尾新增一行
insertBefore(newChild, referenceNode)把新节点插入到某个子节点之前在列表顶部插入
replaceChild(newChild, oldChild)用新节点替换旧节点编辑修改某个已有项

实操里有个常见误区:一个节点只能有一个父节点。如果你把一个已经存在于页面里的节点appendChild到另一个父节点下,浏览器不会产生副本,而是把原节点“移动”过去——旧位置上的节点消失,新位置出现它。这个特性有时可以利用(比如“移动列表项”),但如果不理解,往往会出现“我明明把这个节点挂到新容器,旧容器里的怎么不见了”的困惑。

5.3 批量插入大量节点的性能技巧

如果一次性要往页面里插入100行表格记录,逐条appendChild虽然能跑但效率不高,而且会造成多次“重排”影响性能。更优的方案是用DocumentFragment(文档片段)。你可以把新创建的节点全部appendChild进这个Fragment,最后把Fragment一次性挂载到真正的DOM容器里。由于Fragment本身并不在文档里,它不会触发页面重排,等一次性挂进去时才发生一次真实插入,性能更好。

const fragment = document.createDocumentFragment(); for (let i = 0; i < 100; i++) { const tr = document.createElement('tr'); tr.innerHTML = '<td>第' + i + '行</td>'; fragment.appendChild(tr); } document.getElementById('tableBody').appendChild(fragment);

这一段代码在“动态添加删除记录”时非常实用,建议亲手试一遍。

5.4 删除节点的推荐写法

删除节点常用的是parentNode.removeChild(childNode),先找到要删除节点的父节点,再调用方法。实际项目里更顺手的写法是:

function removeNode(node) { if (node && node.parentNode) { node.parentNode.removeChild(node); } }

自己封装这么一层,调用时只需要removeNode(某个元素)即可,不需要每次手动求父节点。有些环境支持element.remove(),直接自身删除,但老浏览器兼容性欠佳,封装一层更稳,也能统一处理空引用的情况——这是很多初学者容易漏掉的边界判断。

6. 动态添加删除记录:一个完整的表格增删改Demo

6.1 整体需求与页面结构

现在把前面这些知识点串起来做一个完整小项目:一个简单的库存记录表格,支持新增记录、删除记录、修改记录状态。这个例子非常接近真实后台管理系统里的常见操作。

页面结构大致是:

<div id="app"> <h2>库存记录管理</h2> <form id="addForm"> <input type="text" id="goodsName" placeholder="商品名称"> <input type="number" id="goodsCount" placeholder="数量"> <button type="submit">添加记录</button> </form> <table> <thead> <tr> <th>选择</th> <th>商品名称</th> <th>数量</th> <th>操作</th> </tr> </thead> <tbody id="tableBody"> <!-- 动态生成的行 --> </tbody> </table> <button id="batchDelete">批量删除选中记录</button> </div>

6.2 新增记录:从表单取值到插入表格

新增记录的第一步是从表单里取数据。这里有个关键点:阻止表单默认提交行为。如果form的submit事件不调用event.preventDefault(),浏览器会刷新页面或者跳到action指定的地址,所有动态添加效果都看不到。

const addForm = document.getElementById('addForm'); addForm.addEventListener('submit', function(event) { event.preventDefault(); const nameInput = document.getElementById('goodsName'); const countInput = document.getElementById('goodsCount'); const name = nameInput.value.trim(); const count = countInput.value.trim(); if (name === '' || count === '') { alert('商品名称和数量不能为空'); return; } addRecordToTable(name, count); nameInput.value = ''; countInput.value = ''; });

addRecordToTable里通过createElement创建一行tr,依次添加四个td:checkbox、商品名、数量、操作按钮。删除按钮上加一个自定义属性记录当前行的索引或者直接绑定点击事件。但更推荐的做法是用事件委托,下面专门讲。

6.3 删除和编辑:事件委托为什么好用

如果每一行的“删除”按钮都单独绑一个click事件,数据少还好,数据多了之后事件数量跟着膨胀,而且新增出来的行又得手动绑事件,非常麻烦。事件委托的思路是:把事件绑定在父容器上,利用事件冒泡机制,通过判断事件源的标签或类名来处理对应逻辑。

document.getElementById('tableBody').addEventListener('click', function(event) { const target = event.target; if (target.classList.contains('deleteBtn')) { const tr = target.parentNode.parentNode; tr.parentNode.removeChild(tr); } });

这里target.parentNode.parentNode是常规的“向上找两层”写法:按钮所在的是td,td的父节点就是tr。这样即使之后动态新增100行、1000行,也不需要给每个新按钮单独绑事件,因为事件已经统一委托给了tableBody。

同样,全选逻辑也可以委托:在表头的全选checkbox上监听change,动态计算每个checkbox的状态。事件委托尤其适合“同一个容器下大量同类节点、且节点会动态增删”的场景。

6.4 批量删除:从勾选状态收集到逐行删除

批量删除用到了“收集当前勾选了哪些checkbox”的逻辑。需要遍历所有itemCheckbox,筛选出checked === true的那些,然后向上定位行并删除。

document.getElementById('batchDelete').addEventListener('click', function() { const checkboxes = Array.from(document.querySelectorAll('.itemCheckbox')); const checkedBoxes = checkboxes.filter(cb => cb.checked); if (checkedBoxes.length === 0) { alert('没有选中任何记录'); return; } const isConfirm = confirm('确定删除选中的' + checkedBoxes.length + '条记录吗?'); if (!isConfirm) { return; } checkedBoxes.forEach(cb => { const tr = cb.closest('tr'); if (tr) { tr.parentNode.removeChild(tr); } }); updateCheckAllState(); });

这里用到了closest('tr'),它从当前元素向上查找最近的匹配选择器的祖先元素,比手动parentNode.parentNode更稳健。建议多使用closest,尤其是嵌套层级发生变化时,不容易写错层级。

6.5 新增行和全选状态怎么保持一致

新增一条记录时,如果当前处于“全选”状态,新加的行要不要也跟着选中?从业务逻辑上看,更合理的做法是:添加新记录后重新计算全选状态,而不是默认全选。我在项目里一般这样处理:

function addRecordToTable(name, count) { // ...创建tr并append进tableBody updateCheckAllState(); }

updateCheckAllState里判断所有子项是否都勾选,再决定全选框的checked值。这样无论用户如何操作,全选框始终反映真实状态。

7. Window对象方法:setTimeout、setInterval、confirm、alert的实战地位

7.1 浏览器环境下的全局对象

DOM操作刚入门时,很多人会忽视Window对象,但它在实际开发里几乎无处不在。简单说,Window是浏览器环境里的全局对象,document、alert、confirm、setTimeout这些都是它身上的属性或方法。因为它在全局作用域里,所以调用时可以省略window.前缀,直接写alert('...')。

7.2 setTimeout和setInterval的常见坑

setTimeout用于延时执行一次任务,setInterval用于每隔指定时间重复执行。它们的第一个参数可以是函数引用,也可以是字符串形式的代码。不要传字符串,一方面有安全风险,另一方面作用域行为也容易出问题。应该 всегда传函数引用:

setTimeout(function() { console.log('3秒后执行'); }, 3000); const timer = setInterval(function() { console.log('每隔2秒执行一次'); }, 2000); // 需要停止时 clearInterval(timer);

另一个常见坑是:setInterval并不会在指定时间结束后“立即执行第一次”,而是从当前时间起等待第一个间隔周期。如果你希望第一次就立即执行一次,可以手动先调用一次函数,再启动setInterval。还有一种需求是每隔一段时间执行,但每次执行需要等上一次完成(比如轮询接口),这种情况下用“递归setTimeout”更合适:

function poll() { // 模拟异步请求 setTimeout(function() { console.log('本次请求完成'); setTimeout(poll, 2000); }, 1000); }

这种写法能保证上一次任务完全结束后才开始下一次计时,不会出现setInterval里的任务堆积问题。

7.3 用户交互弹窗:alert、confirm、prompt的恰当使用

弹窗系列里,alert用于简单提示,confirm用于让用户确认,prompt用于让用户输入内容。它们有一个共同特性:阻塞。也就是说弹窗期间,JavaScript执行暂时停下来,用户做了操作之后才继续往下走。这个特性有时方便,比如在删除前用confirm阻塞式等待用户确认,很适合前面对批量删除的处理。但也正因为阻塞,实际大型项目里很少用这些原生弹窗做复杂交互,因为样式不可定制、体验也相对粗糙,一般会换成自定义弹层组件或者UI库里的封装方法。这篇内容里的Demo使用原生confirm图个方便,但要知道真实项目里有更好的方案。

7.4 requestAnimationFrame:动画场景下比setInterval更好用的方法

如果做页面动画,比如进度条、滚动效果、小球运动,其实setInterval不是最优选择。requestAnimationFrame会根据浏览器刷新频率(通常是60帧每秒)在合适的时机执行回调,动画更流畅,而且能规避定时器被后台标签页节流的问题。思路就是“帧回调里自己计算位置,并继续请求下一帧”:

function animate() { // 更新元素位置 requestAnimationFrame(animate); } requestAnimationFrame(animate);

对于DOM增删改这个主题来说,requestAnimationFrame不算必需,但既然聊到Window对象方法,提前了解一下能帮你建立“不同场景选不同API”的思维习惯。

8. 实际项目里最容易踩的坑:事件、集合、性能的细节总结

8.1 动态集合与静态集合的“幽灵差异”

回到开头提到的getElementsByClassName返回动态集合的问题。做全选练习和动态添加记录时,如果写成这样:

const checkboxes = document.getElementsByClassName('itemCheckbox'); for (let i = 0; i < checkboxes.length; i++) { checkboxes[i].checked = true; // 全选 }

在浏览器里第一轮全选通常没问题,但如果你在循环里删除某个元素或者往容器里新增元素,checkboxes.length会实时变化,容易导致循环次数错乱或索引越界。批量删除场景里尤其危险:你删除了一行,动态集合立刻少了这个节点,如果你还用原来的索引继续访问,很可能碰到undefined。

所以我的经验法则是:遍历时,先用Array.from或者querySelectorAll把节点列表转成静态快照再去操作。尤其是涉及增删操作的场景,静态快照能让循环逻辑稳定得多。

8.2 事件绑定记住了,但每次新增行都要重新绑?

事件委托可以一劳永逸解决这个问题。我再强调一次:只要容器节点不变,容器内子元素的事件就尽量绑定在容器上,判断event.target是否匹配目标。这样不管你是新增、删除还是替换行,事件逻辑都不需要额外维护。这也是面试里常被问到的“动态列表事件绑定”的推荐答案。

8.3 大量DOM操作时的防卡顿思路

一次性渲染几百条记录,如果逐行拼接字符串再赋值到innerHTML,其实速度反而不慢,因为只触发了一次重排;但如果你逐条appendChild,就会反复触发布局计算。我的建议是:小数据量无所谓,随便写;数据量预计比较大时,记得用DocumentFragment,或者先拼接完整HTML字符串一次性innerHTML赋值。两种方式都能明显减少页面的卡顿感。

8.4 内容里夹杂用户输入时的安全问题

在写“动态添加记录”这样的Demo时,用户输入的商品名如果直接拼进innerHTML,虽然本地调试一般看不出问题,但万一用户输入了类似<img src=x onerror=...>的内容,就会变成可执行代码。真实项目里,对用户输入的内容优先用textContent动态创建文本节点,或者在使用innerHTML之前对特殊字符做转义。安全问题不是危言耸听,XSS攻击一直是前端必须防范的重点,养成从输入源头做处理的习惯,比后期修洞要省事得多。

8.5 循环里的this和闭包陷阱

给多个元素绑事件时,很多人写过这样的代码:

for (var i = 0; i < buttons.length; i++) { buttons[i].onclick = function() { alert('第' + i + '个按钮'); }; }

结果点击任何一个按钮,输出的都是第N个按钮,因为循环结束后i已经变成了最终值。这是一个极其经典的坑。解决办法有三种:把var i改成let i(块级作用域生效)、在循环内用一个立即执行函数包一层、或者在对象上存一份当前索引。现在写代码推荐直接用let,这个问题基本上从语法层面就解决了。但如果你在维护老代码,这个坑还是经常出现,值得记住。

9. 从这些练习里沉淀下来的操作习惯与排查技巧

做完了全选练习和动态添加删除记录这两个场景,我觉得最值得带走的不是某个具体API,而是一套“F12调试”时很好用的排查习惯:

第一,先确认元素有没有拿到。操作不生效时,先打印一下目标元素,如果输出null或undefined,直接看脚本位置、id是否正确、选择器是否匹配。九成问题在这一步就定位了。

第二,确认事件到底有没有触发。在事件处理函数第一行加个console.log,如果点击之后控制台没输出,那就是事件没绑定上或绑错了元素;如果输出了但页面没变化,再往下排查具体操作逻辑。

第三,确认节点关系是否符合预期。操作节点之前打印它的parentNode、children,看看层级关系是不是自己设想的那样。很多“删除不掉”的问题,本质是parentNode找错了层级,多用console.dir看对象的真实结构。

第四,确认修改状态之后的事件同步。这是全选练习和动态添加记录里最容易翻车的环节。手动改了checked属性、塞了动态添加的记录之后,凡是依赖这些状态的其他UI都要记得同步更新。

这几个习惯听起来简单,实际能大幅度减少“改半天不知道哪里出问题”的无效时间。

10. 一点个人心得:DOM能力是“练”出来的,不是“看”出来的

最后分享一点我在带新人时反复说的话:DOM操作这块知识,光看教程背API效果很差,一定要自己动手把全选练习和动态添加删除记录这两个经典场景完整写一遍,最好是从空文件开始写,不要复制粘贴。写完之后再自己加需求,比如加上“编辑行内数据”“按名称筛选”“排序”等功能,每一次扩展都是对节点查询、事件委托、集合特性的再检验。

我见过不少初学者在理解了代码逻辑之后,以为已经掌握了,但真正写的时候还是会在childNodes和children、动态集合和静态集合、事件触发时机这些细节上卡壳。这些都不是“笨”,而是没有亲手踩过这些坑。所以哪怕这篇内容讲得再细,还是建议你打开编辑器、建一个HTML文件,把今天提到的代码一行一行敲出来,再故意改几个地方看报错和表现。踩过坑之后,这些知识点才真正变成你自己的实操经验。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 11:55:50

Kafka接入AI应用:实时数据管道与消息队列实战指南

1. 为什么是Kafka&#xff1a;AI应用的数据入口选型思考1.1 先搞清楚一个事实&#xff1a;AI真正常缺的不是模型&#xff0c;而是实时数据管道我接AI相关项目已经有几年了&#xff0c;早先大家一哄而上调API、微调模型&#xff0c;最后发现跑起来效果差、响应慢、业务落不了地&…

作者头像 李华
网站建设 2026/10/17 11:54:43

美赛A题非线性建模与算法嵌入核心考点解析

简介&#xff1a;本资源为2021年美国大学生数学建模竞赛&#xff08;MCM&#xff09;特等奖&#xff08;Outstanding Winner&#xff09;论文合辑&#xff0c;面向数学建模参赛者、高校数理类专业学生及科研入门者&#xff0c;提供高水准赛题建模范式与跨学科方法论参考。合辑聚…

作者头像 李华
网站建设 2026/10/17 11:54:36

SAP MTO成本核算全解析:销售订单控制与生产订单结算配置指南

简介&#xff1a;一份面向SAP顾问、制造业成本会计及ERP实施人员的SAP MTO&#xff08;Make-to-Order&#xff09;生产模式专题讲解资料。内容从MTO基础概述切入&#xff0c;完整覆盖不采用/采用销售订单控制两种路径&#xff0c;分别讲解其理论逻辑、后台配置要点&#xff0c;…

作者头像 李华
网站建设 2026/10/17 11:54:29

FedML横向联邦学习实践:从FedAvg到Non-IID场景落地

接触过联邦学习的人应该都有这种感觉&#xff1a;算法论文看着很美好&#xff0c;真到了手写落地的时候&#xff0c;“数据不离开本地”这一句话背后&#xff0c;是通信调度、参数聚合、设备管理、容错恢复一堆烂摊子。我最早自己实现 FedAvg&#xff0c;用 socket 传参数&…

作者头像 李华
网站建设 2026/10/17 11:53:30

AI编码代理双重改造:上下文工程+Agent Harness实战指南

1. 先搞明白&#xff1a;AI编码代理为什么需要一场“Context Harness”的双重改造先说个结论&#xff1a;我现在让AI编码代理干活&#xff0c;效率至少是去年同期的5到8倍&#xff0c;个别重复性任务的提升甚至超过10倍。但这个数字不是我换了更强的模型换来的&#xff0c;而是…

作者头像 李华