Skip to content

2-14swipe插件及综合案例-通讯录(二) ​

本页对应 第二章WebAPIs,共 6 段视频,约 1 小时 3 分。

学习目标 ​

  1. 能使用 substring 结合字符串长度减一,截取姓名最后一个字符并渲染为通讯录头像。
  2. 能遍历通讯录列表项并绑定滑动手势,实现左滑显示删除按钮、非左滑移除 active 类,且保证同一时间仅一项处于滑出状态。
  3. 能对新增联系人表单进行非空验证,使用 return 阻止无效提交,并写出等价的简化判断逻辑。
  4. 能按“数据驱动视图”完成新增:根据表单生成对象、追加到数组、调用渲染函数、清空表单,并在渲染函数中调用滑动初始化以恢复滑动效果。
  5. 能使用事件委托为删除按钮注册点击事件,通过自定义属性 data-index 获取索引,结合 confirm 确认框与 splice 删除数组数据并重新渲染页面。
  6. 能说明 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) 当成所有浏览器都完全一致的强制刷新。

本节笔记没有配图,因此不涉及图片 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),两者在“前进/后退一步”的效果上是对应的。

怎么做:准备两个按钮,分别选择它们并注册点击事件:

history对象常见方法

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),使用时给按钮绑定点击事件并调用对应方法。

课件图示 ​

课件第16页:1.5 navigator对象 l navigator是对象,该对象下记录了浏览

课件第12页:1.3 location对象 l location (地址) 它拆分并保存了 U

课件第18页:1.5 histroy对象 l history (历史)是对象,主要管理历史记录

课件第6页:1.1 BOM l BOM (Browser Object Model ) 是浏

课件第13页:总结 总结 属性/方法 说明 href 属性,获取完整的 URL 地址,赋值时用

课件第14页:案例 5秒钟之后跳转页面 需求:用户点击可以跳转,如果不点击,则5秒之后自动跳转

课件第20页:1.6 本地存储(今日重点) 本地存储:将数据存储在本地浏览器中 常见的使用场景

课件第21页:1.6 本地存储分类- localStorage(重点) l 作用: 数据可以长

代码示例 ​

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综合案例-通讯录截取字符串substring07:00
2综合案例-通讯录滑动业务13:13
3综合案例-通讯录新增业务验证表单09:26
4综合案例-通讯录新增业务完成11:54
5综合案例-通讯录删除业务上13:21
6综合案例-通讯录删除业务下08:12

小结 ​

  1. substring(start, end) 的第二个参数可省略;取「最后一个字符」的通用写法是 str.substring(str.length - 1),与字符个数无关。
  2. location.href 既能读取完整地址,赋值时又能跳转;search 取 ? 之后、hash 取 # 之后,reload(true) 是强制刷新。
  3. navigator.userAgent 常用于判断 Android / iPhone 并做移动站跳转;history.go(1) / go(-1) 等价于前进 / 后退。
  4. 本案例的新增与删除都遵循数据驱动视图:先改数组,再调用 render() 重绘,绝不直接操作 DOM 节点。
  5. render() 必须封装成函数并可重复调用——新增、删除、初始化都复用它,这是后续框架思维的起点。
  6. 渲染后 DOM 会重建,所有依赖具体节点的手势/事件都必须在 render() 内重新初始化,否则「渲染一次,功能失效一次」。
  7. 会变的元素(新增/删除的条目)不绑事件,绑到不变的父容器上——事件委托既减少绑定次数,又天然支持动态节点。
  8. data-index 自定义属性是「DOM 元素」与「数组下标」之间的桥梁,配合 e.target.dataset.index 精确定位要操作的数据。
  9. 删除前用 confirm 做二次确认,其返回 true / false 必须参与 if 判断;同时表单校验失败的分支一定要 return,避免后续逻辑误执行。