主题
2-14swipe插件及综合案例-通讯录(二)
本页对应 第二章WebAPIs,共 6 段视频,约 1 小时 3 分。
学习目标
- 能使用
substring结合字符串长度减一,截取姓名最后一个字符并渲染为通讯录头像。 - 能遍历通讯录列表项并绑定滑动手势,实现左滑显示删除按钮、非左滑移除
active类,且保证同一时间仅一项处于滑出状态。 - 能对新增联系人表单进行非空验证,使用
return阻止无效提交,并写出等价的简化判断逻辑。 - 能按“数据驱动视图”完成新增:根据表单生成对象、追加到数组、调用渲染函数、清空表单,并在渲染函数中调用滑动初始化以恢复滑动效果。
- 能使用事件委托为删除按钮注册点击事件,通过自定义属性
data-index获取索引,结合confirm确认框与splice删除数组数据并重新渲染页面。 - 能说明
location、navigator、history对象的常用属性或方法,并指出其在浏览器对象模型中的基本用途。
知识点
本节说明
本节笔记主题是 location 对象。需要先说明:提供的转写稿主体是“通讯录案例”的字符串截取、滑动、新增、删除等调试过程,并没有直接讲解 location 的推导与调试;因此下面不把无关案例硬套到 location 上,避免编造。笔记中“啥希值”是转写同音字,应纠正为“哈希值”,即 hash 值。
location 对象是什么
为什么需要:浏览器地址栏里的 URL 包含协议、域名、路径、参数、哈希等信息。如果只用字符串手写拼接,既麻烦又容易出错。location 把当前页面 URL 拆成多个可读属性,并提供跳转、刷新等方法。
怎么想:把 location 理解成“当前地址栏的对象化表示”。想拿完整地址用 href;想拿问号参数用 search;想拿井号后面内容用 hash;想刷新页面用 reload()。
怎么做:先打印 location 的常用属性观察结构,再在表单、锚点、按钮事件中使用。
易错点:location 是浏览器提供的对象,不是自己声明的变量;属性名不要写成 Location,大小写敏感。
href 属性:完整地址与跳转
为什么需要:有时需要读取当前页面完整 URL,有时需要点击按钮后跳到另一个地址。
怎么想:location.href 读取时是完整 URL;给它赋值时,浏览器会按新地址跳转。
怎么做:读取用 console.log(location.href);跳转用 location.href = 'http://www.itcast.cn'。笔记代码中跳转语句被注释掉,调试时可先打印,确认无误再取消注释。
易错点:赋值不是“修改字符串”,而是触发页面导航;如果地址写错,会跳到错误页面或 404。调试跳转前先保存当前地址。
search 属性:问号后面的参数
为什么需要:搜索页、列表页常通过 URL 传参,例如 ?search=笔记本,需要读取用户搜索关键词。
怎么想:search 只取 ? 后面部分,包含 ? 本身。笔记示例打印结果为 ?search=笔记本。
怎么做:表单中 input name="search",提交后地址栏出现参数,再用 console.log(location.search) 查看。实际取参数时还要进一步解析,不能直接当纯关键词使用。
易错点:search 包含 ?;没有参数时可能是空字符串;参数值可能被编码,不能想当然认为一定是中文原文。
hash 属性:井号后面的哈希值
为什么需要:单页应用、锚点导航常用 #/music、#/download 表示不同视图或位置。
怎么想:hash 取 # 后面部分,包含 #。点击 <a href="#/music">音乐</a> 后,地址栏变化,location.hash 随之变化。
怎么做:页面放两个锚点链接,点击后打印 console.log(location.hash),观察输出是 #/music 或 #/download。
易错点:hash 不是 search;不要混淆 ? 和 #;哈希变化通常不会让页面重新请求服务器,但会触发地址栏变化。
reload 方法:刷新页面
为什么需要:数据更新后有时需要重新加载当前页面;调试时也常手动刷新。
怎么想:location.reload() 刷新当前页面;笔记中 location.reload(true) 表示强制刷新,类似 Ctrl+F5 的思路。
怎么做:给刷新按钮注册点击事件,在回调中调用 location.reload();笔记代码保留两种写法,其中 true 被注释说明为强制刷新。
易错点:刷新会丢失未保存的表单内容;reload(true) 在部分现代浏览器中参数可能被忽略,不能把它当成绝对可靠的强制刷新保证。调试时先确认页面状态,再执行刷新。
完整代码
下面保留本节笔记代码,便于对照调试:
html
<body>
<form>
<input type="text" name="search"> <button>搜索</button>
</form>
<a href="#/music">音乐</a>
<a href="#/download">下载</a>
<button class="reload">刷新页面</button>
<script>
// location 对象
// 1. href属性 (重点) 得到完整地址,赋值则是跳转到新地址
console.log(location.href)
// location.href = 'http://www.itcast.cn'
// 2. search属性 得到 ? 后面的地址
console.log(location.search) // ?search=笔记本
// 3. hash属性 得到 # 后面的地址
console.log(location.hash)
// 4. reload 方法 刷新页面
const btn = document.querySelector('.reload')
btn.addEventListener('click', function () {
// location.reload() // 页面刷新
location.reload(true) // 强制页面刷新 ctrl+f5
})
</script>
</body>调试顺序与易错点汇总
建议按“先读后写”的顺序调试:先打印 href、search、hash,观察地址栏变化;再测试 href 赋值跳转;最后测试 reload。搜索表单提交后注意页面会刷新,控制台输出可能一闪而过,可先阻止默认提交或手动在地址栏加参数观察。核心记忆:href 管完整地址和跳转,search 管 ? 后参数,hash 管 # 后哈希,reload() 管刷新。不要把 search 与 hash 混用,也不要把 reload(true) 当成所有浏览器都完全一致的强制刷新。
navigator 是什么:先认识“浏览器信息对象”
本节笔记没有配图,因此不涉及图片 alt 调整。转写稿主要讲通讯录案例,与 navigator 没有直接对应关系,下面只依据本节笔记展开,不把无关内容硬套进来。
为什么需要:页面有时要根据浏览器自身信息做判断,例如判断当前是 Android 还是 iPhone,再决定是否跳转到移动站点。navigator 就是浏览器提供的对象,用来记录浏览器自身的相关信息。
怎么想:先把它当成一个普通对象来理解,不要当成函数;需要信息时,从它下面取属性。笔记里明确说“navigator 是对象,该对象下记录了浏览器自身的相关信息”,本节重点使用 navigator.userAgent。
怎么做:在控制台或代码中读取 navigator.userAgent,得到一串浏览器信息字符串,再交给正则表达式判断平台。
易错点:navigator 不是数组,也不是当前页面地址对象;不要写 navigator()。另外,userAgent 可以被伪造,不能把它当成绝对可靠的安全判断依据。
用 userAgent 检测浏览器版本及平台
为什么需要:同一套页面在 PC 和手机上的布局、交互可能不同。通过 userAgent 可以先识别 Android、iPhone 等平台,再执行跳转或加载不同逻辑。
怎么想:核心不是“直接读版本号”,而是“从一长串浏览器信息里匹配关键字”。笔记使用 match 配合正则:能匹配到就说明命中对应平台。
怎么做:先取 const userAgent = navigator.userAgent,再分别匹配 Android 和 iPhone:
javascript
const android = userAgent.match(/(Android);?[\s\/]+([\d.]+)?/)
const iphone = userAgent.match(/(iPhone\sOS)\s([\d_]+)/)android 或 iphone 有结果时,说明当前设备命中移动端平台,于是执行 location.href = 'http://m.itcast.cn'。
易错点:正则区分大小写,Android、iPhone OS 的写法要与实际字符串对应;iPhone 的 OS 前有空白,版本号中常见下划线;不要只判断 Android 而漏掉 iPhone。匹配结果可能是数组或 null,不要直接对 null 取下标。
正则拆解:知道每一段在匹配什么
为什么需要:不拆正则,代码只能照抄;一旦设备字符串变化,就不知道改哪里。
怎么想:把正则分成“平台关键字 + 分隔符 + 版本号”三部分看。
怎么做:/(Android);?[\s\/]+([\d.]+)?/ 中,(Android) 匹配平台名,;? 表示分号可有可无,[\s\/]+ 表示空白或斜杠,([\d.]+)? 表示数字和点组成的版本号,末尾 ? 表示版本号可缺省。/(iPhone\sOS)\s([\d_]+)/ 中,iPhone\sOS 匹配“iPhone OS”,后面的 \s 匹配空白,([\d_]+) 匹配数字和下划线组成的版本信息。
易错点:不要漏掉转义符 \/;[\d.] 里的点表示字面点,不是任意字符;? 的位置不同,含义不同。
立即执行函数与跳转:把检测封装起来
为什么需要:检测代码通常只运行一次,不希望把临时变量暴露到全局,避免污染全局命名空间。
怎么想:用立即执行函数把逻辑包起来,执行完就结束;命中移动端后直接修改 location.href 完成跳转。
怎么做:保留笔记代码:
javascript
// 检测 userAgent(浏览器信息)
(function () {
const userAgent = navigator.userAgent
// 验证是否为Android或iPhone
const android = userAgent.match(/(Android);?[\s\/]+([\d.]+)?/)
const iphone = userAgent.match(/(iPhone\sOS)\s([\d_]+)/)
// 如果是Android或iPhone,则跳转至移动站点
if (android || iphone) {
location.href = 'http://m.itcast.cn'
}
})();易错点:立即执行函数末尾的 () 不能漏;location.href 会触发页面跳转,测试时要确认目标地址;如果移动站点仍然命中同一判断,要检查是否会造成重复跳转。实际项目中还应考虑是否已经处于移动站,避免无意义跳转。
调试与验证:看匹配结果,不靠猜
为什么需要:正则是否命中,只看代码不容易确认,必须打印中间结果。
怎么想:先验证 userAgent 是否取到,再分别打印 android、iphone 的匹配结果;命中时是数组,不命中时是 null。
怎么做:可在测试时临时加 console.log(userAgent)、console.log(android)、console.log(iphone),观察输出后再决定是否跳转。调试完成后删除临时日志。
易错点:不要在生产代码里长期保留调试输出;不要为了测试随意改 navigator.userAgent;不同浏览器、不同设备的 userAgent 字符串可能不同,判断逻辑要允许未命中。
history对象是什么
history(历史)是浏览器提供的一个对象,主要用来管理当前窗口的历史记录。它和浏览器地址栏左侧的前进、后退操作相对应。比如用户点过多个页面后,调用 history.back() 就相当于点击浏览器“后退”,调用 history.forward() 就相当于点击“前进”。
需要先说明:你给的转写稿主要讲的是通讯录综合案例中的字符串截取、滑动、新增和删除,并没有 history 对象的推导内容。因此下面不把通讯录逻辑硬套到 history 上,只依据本节笔记和代码讲解,避免编造。转写稿里出现的“缩引号”应纠正为“索引号”;本节代码注释里的 histroy 也应纠正为 history。
为什么需要history对象
为什么需要:有些页面希望自己提供“后退”“前进”按钮,而不是只依赖浏览器工具栏。比如 OA 办公系统中,页面内部可能有自己的操作按钮,需要让用户在不离开当前页面的情况下完成历史跳转。
怎么想:先把 history 看成“浏览器历史记录的管理器”。它不负责保存业务数据,也不负责渲染页面,只负责让页面执行前进、后退、跳转历史等动作。
怎么做:在 JavaScript 中可以直接使用 history,因为它通常作为 window 对象的属性存在。实际写代码时写 history.back()、history.forward()、history.go(1) 即可。
易错点:不要拼成 histroy;history 不是数组,不能像数组那样遍历历史记录;它管理的是浏览器历史,不是你自己写的业务数据。
使用场景
为什么需要:history 对象在实际开发中一般比较少用,但并不是没有场景。笔记明确指出,它会在一些 OA 办公系统中见到。
怎么想:如果页面需要模拟浏览器地址栏的前进、后退体验,就可以考虑它;如果只是普通列表增删改查,通常不需要。
怎么做:先判断产品是否真的需要“页面内前进/后退”按钮。若需要,再给按钮绑定点击事件,在事件中调用 history 的方法。
易错点:不要为了用 API 而用 API。没有历史记录时,点前进或后退可能“没反应”,这不是代码一定错了,而是当前窗口没有可跳转的历史。

常见方法与代码演示
为什么需要:知道概念不够,真正落地时要能通过按钮触发历史操作。
怎么想:history.back() 表示后退一步,history.forward() 表示前进一步;history.go(n) 更通用,正数表示前进,负数表示后退。因此 history.go(1) 可以理解为前进一页,history.go(-1) 可以理解为后退一页。代码中虽然注释了 history.forward() 和 history.back(),但实际用了 history.go(1) 和 history.go(-1),两者在“前进/后退一步”的效果上是对应的。
怎么做:准备两个按钮,分别选择它们并注册点击事件:

html
<body>
<button class="back">←后退</button>
<button class="forward">前进→</button>
<script>
// histroy对象
// 1.前进
const forward = document.querySelector('.forward')
forward.addEventListener('click', function () {
// history.forward()
history.go(1)
})
// 2.后退
const back = document.querySelector('.back')
back.addEventListener('click', function () {
// history.back()
history.go(-1)
})
</script>
</body>易错点:第一,选择器要和按钮类名一致,否则点击事件绑不上;第二,测试时不要只打开一个空白页就点按钮,最好先访问两个页面形成历史记录;第三,go(1) 和 go(-1) 的参数正负不要写反;第四,代码注释中的 histroy 应改为 history,避免以后复制时把错误拼写带进正式代码。
调试与理解
为什么需要:history 的效果依赖浏览器历史,单看代码不容易判断是否生效。
怎么想:调试时先确认按钮事件是否触发,再确认当前窗口是否存在可前进或可后退的历史记录。可以在事件里临时加 console.log('点击了') 验证事件绑定;如果日志有输出但页面没变化,再检查历史记录条件。
怎么做:在浏览器中先打开页面 A,再进入页面 B,然后在 B 中点击“后退”按钮,观察是否回到 A;再点击“前进”按钮,观察是否回到 B。这样能直观看到 history.go(-1) 和 history.go(1) 的效果。
易错点:不要用 history 保存业务状态;不要把“无历史记录时无反应”误判为 API 失效;不要在跨域、隐私模式等复杂环境里做过度假设。掌握本节的重点即可:history 管理历史记录,常见操作是后退、前进和 go(n),使用时给按钮绑定点击事件并调用对应方法。
课件图示








代码示例
1. location 对象:href / search / hash / reload
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>location 对象</title>
</head>
<body>
<form>
<input type="text" name="search"> <button>搜索</button>
</form>
<a href="#/music">音乐</a>
<a href="#/download">下载</a>
<button class="reload">刷新页面</button>
<script>
// location 对象
// 1. href属性 (重点) 得到完整地址,赋值则是跳转到新地址
console.log(location.href)
// location.href = 'http://www.itcast.cn'
// 2. search属性 得到 ? 后面的地址
console.log(location.search) // ?search=笔记本
// 3. hash属性 得到 # 后面的地址
console.log(location.hash)
// 4. reload 方法 刷新页面
const btn = document.querySelector('.reload')
btn.addEventListener('click', function () {
// location.reload() // 页面刷新
location.reload(true) // 强制页面刷新 ctrl+f5
})
</script>
</body>
</html>关键行与易错点
location是对象,它把 URL 拆成 href、search、hash 等部分分别保存。search拿到的是 带?的整串,如?search=笔记本,不是只有笔记本;要拿到参数值需要再截取。hash拿到的是 带#的整串,如#/music,点击锚点链接后才会变化。location.href = '地址'是赋值即跳转,并且会往历史记录里写一条(所以才能配合下面的 history 前进后退)。reload(true)是强制刷新(相当于 ctrl+f5),不带参数是普通刷新;注意页面刷新后输入框、变量全部重置。
2. navigator 检测浏览器 + history 前进后退
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>navigator 与 history</title>
</head>
<body>
<button class="back">←后退</button>
<button class="forward">前进→</button>
<a href="#/music">音乐</a>
<script>
// 检测 userAgent(浏览器信息)
(function () {
const userAgent = navigator.userAgent
// 验证是否为Android或iPhone
const android = userAgent.match(/(Android);?[\s\/]+([\d.]+)?/)
const iphone = userAgent.match(/(iPhone\sOS)\s([\d_]+)/)
// 如果是Android或iPhone,则跳转至移动站点
if (android || iphone) {
location.href = 'http://m.itcast.cn'
}
})();
// history 对象
// 1.前进
const forward = document.querySelector('.forward')
forward.addEventListener('click', function () {
// history.forward()
history.go(1)
})
// 2.后退
const back = document.querySelector('.back')
back.addEventListener('click', function () {
// history.back()
history.go(-1)
})
</script>
</body>
</html>关键行与易错点
navigator记录的是浏览器自身信息,userAgent只能读、不能改,用来做“判断平台再跳移动站”这类需求。- 外面包了一层
(function () { ... })();立即执行函数,页面一打开就检测,不需要被调用。 userAgent.match(...)匹配到返回数组(真值),匹配不到返回null,所以if (android || iphone)可以直接判断。- 这段跳转在电脑浏览器里不会触发,但一旦在手机上打开就会真的跳到
m.itcast.cn,本地测试时建议先注释掉location.href那一行。 history.go(1)等价于history.forward(),history.go(-1)等价于history.back();前提是确实有历史记录,刚打开的页面点“后退”不会有任何反应。- history 管的是浏览器的历史记录(前进、后退),实际开发中比较少用,多见于 OA 办公系统。
3. 通讯录:substring 取头像 + 新增 + 删除(数据驱动视图)
js
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>通讯录</title>
<style>
.list { width: 320px; }
.item {
position: relative;
display: flex;
align-items: center;
padding: 10px 60px 10px 10px;
border-bottom: 1px solid #eee;
}
.avatar {
width: 40px; height: 40px; line-height: 40px; text-align: center;
border-radius: 50%; background: #4a90e2; color: #fff; margin-right: 10px;
}
.del {
position: absolute; right: 0; top: 0;
width: 50px; height: 100%; line-height: 60px;
background: #e64340; color: #fff; text-align: center;
font-style: normal; cursor: pointer;
}
</style>
</head>
<body>
<div class="list">
<form class="add-form">
<input type="text" class="name" placeholder="姓名">
<input type="text" class="tel" placeholder="手机号">
<button type="button" id="add">添加联系人</button>
</form>
<div class="address-book"></div>
</div>
<script>
// 数据
const arr = [
{ name: '周杰伦', tel: '13012341234' },
{ name: '刘德华', tel: '13112341234' },
{ name: '岳云鹏', tel: '13212341234' }
]
const book = document.querySelector('.address-book')
const nameInput = document.querySelector('.name')
const telInput = document.querySelector('.tel')
const addBtn = document.querySelector('#add')
// 渲染函数(数据驱动视图)
function render() {
let html = ''
arr.forEach((item, index) => {
html += `
<div class="item">
<div class="avatar">${item.name.substring(item.name.length - 1)}</div>
<div>
<p>${item.name}</p>
<p>${item.tel}</p>
</div>
<i class="del" data-index="${index}">删除</i>
</div>`
})
book.innerHTML = html
// initSwiper() // 每次渲染完都要重新给 item 绑定滑动效果
}
render()
// 新增:先验证表单,再操作数据,最后重新渲染
addBtn.addEventListener('click', function () {
if (!nameInput.value || !telInput.value) {
alert('姓名和电话不能为空')
return
}
arr.push({ name: nameInput.value, tel: telInput.value })
render()
nameInput.value = ''
telInput.value = ''
})
// 删除:事件委托 + confirm + splice
book.addEventListener('click', function (e) {
if (e.target.tagName === 'I') {
const index = e.target.dataset.index
const isDel = confirm('您确定要删除这个电话吗?')
if (isDel) {
arr.splice(index, 1)
render()
}
}
})
</script>
</body>
</html>关键行与易错点
\${item.name.substring(item.name.length - 1)}:substring第一个参数是起始索引,第二个参数可省略(省略则截取到末尾)。想取最后一个字符就写length - 1,三个字、四个字的名字都不用改代码;substring(0, 1)才是取第一个字符。- 数据驱动视图:新增、删除都不直接操作 DOM,而是先改
arr,再调用render()按最新数组重画页面。 - 表单验证用简写
if (!nameInput.value || !telInput.value):空字符串取反为true,进 if 后必须return结束点击事件,否则后面照样会新增一条空数据。 arr.push({ name: ..., tel: ... })可以直接把对象塞进数组,不必先声明变量。- 删除必须用事件委托:事件绑在不变的父盒子
.address-book上,e.target.tagName === 'I'(大写)才执行;如果改用querySelectorAll('.del')一个个绑,后来新增的那条数据的删除按钮绑不上事件。 data-index="\${index}"与e.target.dataset.index对应,靠它才知道删第几条;arr.splice(index, 1)第二个参数是删除个数。confirm()点确定返回true、点取消返回false,用来防误删。- 最大的坑:
render()用innerHTML整体重画,之前绑定的事件和滑动效果全部丢失,所以滑动函数initSwiper()必须写进render()里调用(代码中注释的位置),这也是把滑动业务封装成函数的原因。
常见错误
1. 用 substring 取姓名最后一个字时写死索引
- 现象:三字姓名显示正常,换成「迪丽热巴」这类四字姓名后取到的是「巴」前一个字,或取到空字符串。
- 原因:把起始索引写成了固定数字(如
substring(2)),而substring的起始索引是字符位置,与姓名长度无关。 - 修法:用长度动态计算——
name.substring(name.length - 1),永远从「长度减一」处开始截取到最后。
2. 滑动效果只对首屏已有条目生效,新增后「拉不动」
- 现象:初始 5 条都能左滑出删除按钮,新增一条后,新条目怎么滑都没反应,甚至旧条目也失效。
- 原因:
initSwiper()只在页面加载时调用一次,它遍历的是当时的item集合;render()重新生成 DOM 后,新节点没有绑定手势。 - 修法:把
initSwiper()的调用放进render()内部,每次渲染完成后重新为全部item绑定手势。
3. 删除按钮用 querySelectorAll 直接批量绑定点击事件
- 现象:页面初始条目可以删除,新增出来的条目点删除毫无反应。
- 原因:事件绑定发生在新增数据之前,后来通过
render()生成的节点不在绑定时的集合里。 - 修法:改为事件委托——绑定到不变化的父容器
addressBook上,用e.target.tagName === 'I'判断是否点中删除按钮。
4. 未判断 active 元素就调用 remove,控制台报错
- 现象:首次左滑时控制台抛
Cannot read properties of null (reading 'classList'),滑动效果中断。 - 原因:页面初始没有任何元素带
active类,querySelector('.active')返回null,直接.classList.remove()必然报错。 - 修法:先取再判,利用逻辑与短路——
const active = document.querySelector('.active'); active && active.classList.remove('active')。
5. 删除时忘记 return,或把 return 写成 break
- 现象:姓名为空时提示框弹出后,页面依然新增了一条空数据。
- 原因:只做了
alert提示,没有终止事件处理函数,后续的push+render继续执行。 - 修法:校验失败分支末尾必须
return,把push与render写在if判断之后。
6. confirm 返回值未参与判断,取消也被删除
- 现象:点击「取消」后数据仍然被删掉了。
- 原因:只写了
confirm('确定删除吗')却没有接收返回值,删除代码在if之外照常执行。 - 修法:
const con = confirm('...'); if (con) { arr.splice(...); render() },让删除逻辑完全包裹在判断内。
练习
练习 1(字符串截取) 封装函数 getLastChar(str),返回任意长度字符串的最后一个字符;再分别用 'pink'、'迪丽热巴'、'' 三种输入测试,并说明空字符串时返回值是什么、为什么。
练习 2(数据驱动视图) 在通讯录案例基础上,把「新增联系人」改成同时校验:姓名不能为空、手机号不能为空、手机号必须为 11 位数字。校验失败用 alert 提示并终止,成功后追加到数组、重新渲染、清空表单。写出完整的点击回调,并标注每一处 return 的作用。
练习 3(事件委托 + 自定义属性) 在渲染模板中给每个删除按钮 <i> 添加 data-index 属性,然后在父容器上注册一次点击事件,点击时打印出「你即将删除第 N 条,姓名是 XXX」。要求:点击条目空白区域不触发任何输出。
练习 4(滑动互斥) 补全 initSwiper() 中的左滑逻辑:左滑时先移除其他条目已存在的 active 类,再给当前条目添加;右滑时移除自身的 active。说明为什么移除「其他条目」时必须先判空。
练习 5(删除确认) 为删除业务加上 confirm 二次确认,并在确认删除后调用 splice 与 render()。再思考一个问题:如果 splice 之后忘记调用 render(),数组和页面会分别是什么状态?请用一句话描述这种「数据与视图不一致」的表现。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 综合案例-通讯录截取字符串substring | 07:00 |
| 2 | 综合案例-通讯录滑动业务 | 13:13 |
| 3 | 综合案例-通讯录新增业务验证表单 | 09:26 |
| 4 | 综合案例-通讯录新增业务完成 | 11:54 |
| 5 | 综合案例-通讯录删除业务上 | 13:21 |
| 6 | 综合案例-通讯录删除业务下 | 08:12 |
小结
substring(start, end)的第二个参数可省略;取「最后一个字符」的通用写法是str.substring(str.length - 1),与字符个数无关。location.href既能读取完整地址,赋值时又能跳转;search取?之后、hash取#之后,reload(true)是强制刷新。navigator.userAgent常用于判断 Android / iPhone 并做移动站跳转;history.go(1)/go(-1)等价于前进 / 后退。- 本案例的新增与删除都遵循数据驱动视图:先改数组,再调用
render()重绘,绝不直接操作 DOM 节点。 render()必须封装成函数并可重复调用——新增、删除、初始化都复用它,这是后续框架思维的起点。- 渲染后 DOM 会重建,所有依赖具体节点的手势/事件都必须在
render()内重新初始化,否则「渲染一次,功能失效一次」。 - 会变的元素(新增/删除的条目)不绑事件,绑到不变的父容器上——事件委托既减少绑定次数,又天然支持动态节点。
data-index自定义属性是「DOM 元素」与「数组下标」之间的桥梁,配合e.target.dataset.index精确定位要操作的数据。- 删除前用
confirm做二次确认,其返回true/false必须参与if判断;同时表单校验失败的分支一定要return,避免后续逻辑误执行。