第17章 JavaScript算法与事件进阶

三连决胜:任务11/12/13 | 课型:实习课 | 建议课时:4节

🏠 返回总目录 / 第17章

🎯 学习目标

知识目标

  • 理解eval的安全风险与替代方案
  • 掌握递归下降解析器的分层设计
  • 掌握Pointer Events拖拽
  • 掌握背景定位与animationend事件
  • 掌握事件冒泡与委托

能力目标

  • 能实现支持四则运算/乘方/括号的安全计算器
  • 能实现Pointer Events拖拽元素
  • 能完成图片切片过渡动画
  • 能用事件委托优化列表交互

德育目标

  • 树立网络安全意识:eval是定时炸弹
  • 培养算法分层设计的工程思维

教学重点

  • 任务11递归下降四层结构
  • 任务12的setPointerCapture
  • 任务13的animationend收尾

教学难点

  • 递归实现运算符优先级与右结合
  • 切片动画的时序衔接

💻 实例演示3个实例 · 可在线运行

▶ 竞赛任务11:安全计算器

支持+-*/%^()与负号的递归下降解析计算器,拒绝eval。

新窗口运行 下载源码

操作步骤

  1. 打开demo1.html,计算默认表达式验证优先级
  2. 测试2^3^2验证右结合(应为512)
  3. 测试-(3+4)*2验证负号与括号
  4. 输入"2+"观察错误提示
  5. 阅读四层函数结构,画出调用关系图

效果观察点

  • 低优先级函数先调用高优先级函数
  • parsePow递归实现右结合
  • try/catch给出友好错误
查看核心代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Task 11 - JS算术(安全版)</title>
<style>
  body { font-family: Arial, "Microsoft YaHei", sans-serif; background: #29303b;
         display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
  .panel { background: #fff; padding: 30px; border-radius: 12px; width: 420px; }
  input { width: 100%; padding: 12px; font-size: 18px; border: 2px solid #123a52; border-radius: 8px; box-sizing: border-box; }
  button { margin-top: 14px; width: 100%; padding: 12px; font-size: 17px; border: none;
           border-radius: 8px; background: #0f8c7f; color: #fff; cursor: pointer; }
  .result { margin-top: 14px; font-size: 20px; color: #123a52; }
  .hint { font-size: 13px; color: #6b8a99; margin-top: 10px; }
</style>
</head>
<body>
  <div class="panel">
    <input id="txt" value="2+3*4^2/(1+1)" placeholder="输入表达式,如 2+3*4">
    <button id="btn">计 算</button>
    <div class="result">计算结果为:<span></span></div>
    <div class="hint">支持 + - * / % ^ 括号 负数 小数 | 无eval,递归下降解析</div>
  </div>
<script>
  function calculate(input) {
    const tokens = input.match(/\d+\.?\d*|[-+*/%^()]/g);
    if (!tokens) throw new Error('表达式为空');
    let pos = 0;
    const peek = () => tokens[pos];
    const next = () => tokens[pos++];
    function parseAdd() {
      let val = parseMul();
      while (peek() === '+' || peek() === '-') {
        const op = next(); const rhs = parseMul();
        val = op === '+' ? val + rhs : val - rhs;
      }
      return val;
    }
    function parseMul() {
      let val = parsePow();
      while (peek() === '*' || peek() === '/' || peek() === '%') {
        const op = next(); const rhs = parsePow();
        if (op === '*') val *= rhs; else if (op === '/') val /= rhs; else val %= rhs;
      }
      return val;
    }
    function parsePow() {
      const base = parseAtom();
      if (peek() === '^') { next(); return base ** parsePow(); }
      return base;
    }
    function parseAtom() {
      if (peek() === '-') { next(); return -parseAtom(); }
      if (peek() === '(') {
        next(); const val = parseAdd();
        if (next() !== ')') throw new Error('缺少右括号');
        return val;
      }
      return parseFloat(next());
    }
    const result = parseAdd();
    if (pos !== tokens.length) throw new Error('表达式不完整');
    return result;
  }
  document.getElementById('btn').addEventListener('click', () => {
    const expr = document.getElementById('txt').value;
    const out = document.querySelector('span');
    try { out.textContent = calculate(expr); }
    catch (err) { out.textContent = '错误:' + err.message; }
  });
</script>
</body>
</html>

▶ 竞赛任务12:JS拖动

Pointer Events实现流畅拖拽方块,兼容鼠标与触屏。

新窗口运行 下载源码

操作步骤

  1. 打开demo2.html,拖动方块到任意位置
  2. 快速甩动鼠标,观察方块不"脱手"(捕获的作用)
  3. 用开发者工具模拟触屏设备拖动
  4. 注释掉setPointerCapture,快速移动观察脱手现象
  5. 挑战:为拖动添加边界限制

效果观察点

  • offset修正抓取点与左上角的差值
  • Pointer Events一套代码兼容鼠标触屏
  • touch-action:none防止触屏滚动干扰
查看核心代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Task 12 - JS Draggable</title>
<style>
  body { margin: 0; height: 100vh; background: #29303b; font-family: Arial, sans-serif; overflow: hidden; }
  #box { width: 120px; height: 120px; background: linear-gradient(135deg, #38d6c4, #0f8c7f);
         border-radius: 12px; position: absolute; top: 100px; left: 100px; cursor: grab;
         display: flex; align-items: center; justify-content: center; color: #fff;
         font-weight: bold; user-select: none; touch-action: none; }
  #box:active { cursor: grabbing; }
</style>
</head>
<body>
  <div id="box">拖我</div>
<script>
  const box = document.getElementById('box');
  let dragging = false, offsetX = 0, offsetY = 0;
  box.addEventListener('pointerdown', e => {
    dragging = true;
    offsetX = e.clientX - box.offsetLeft;
    offsetY = e.clientY - box.offsetTop;
    box.setPointerCapture(e.pointerId);   // 捕获指针:移出元素也能跟踪
  });
  box.addEventListener('pointermove', e => {
    if (!dragging) return;
    box.style.left = (e.clientX - offsetX) + 'px';
    box.style.top = (e.clientY - offsetY) + 'px';
  });
  box.addEventListener('pointerup', () => dragging = false);
</script>
</body>
</html>

▶ 竞赛任务13:图片切片过渡

5×3切片网格依次飞入的过渡动画——背景定位+animationend(CSS渐变模拟图片)。

新窗口运行 下载源码

操作步骤

  1. 打开demo3.html,点击"播放过渡动画"
  2. 观察15个切片按序飞入拼成完整图案
  3. 选中某切片查看background-position的负值含义
  4. 把延时i*80改为随机,观察洗牌式进入
  5. 对照评分点:表格/切片尺寸/背景定位/依次动画

效果观察点

  • background-size:500px 300px让15片共享一张"大图"
  • 负background-position把每片移到对应区域
  • transitionend/animationend用于收尾
查看核心代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Task 13 - 切片过渡</title>
<style>
  body { margin: 0; min-height: 100vh; background: #123a52; display: flex;
         flex-direction: column; align-items: center; justify-content: center; font-family: Arial, sans-serif; }
  table { border-collapse: collapse; }
  td { width: 100px; height: 100px; padding: 0; }
  .slice { width: 100%; height: 100%;
           background: linear-gradient(135deg, #38d6c4, #1b5a7a, #c77728);
           background-size: 500px 300px; opacity: 0; transform: translateY(-40px);
           transition: all .5s ease; }
  .slice.on { opacity: 1; transform: translateY(0); }
  button { margin-top: 24px; padding: 10px 28px; border: none; border-radius: 8px;
           background: #38d6c4; color: #123a52; font-weight: bold; cursor: pointer; }
</style>
</head>
<body>
  <table id="grid"></table>
  <button id="play">播放过渡动画</button>
<script>
  const COLS = 5, ROWS = 3;
  const grid = document.getElementById('grid');
  const slices = [];
  for (let y = 0; y < ROWS; y++) {
    const tr = document.createElement('tr');
    for (let x = 0; x < COLS; x++) {
      const td = document.createElement('td');
      const d = document.createElement('div');
      d.className = 'slice';
      d.style.backgroundPosition = (-x * 100) + 'px ' + (-y * 100) + 'px';
      td.appendChild(d); tr.appendChild(td); slices.push(d);
    }
    grid.appendChild(tr);
  }
  document.getElementById('play').addEventListener('click', () => {
    slices.forEach(s => s.classList.remove('on'));
    slices.forEach((s, i) => setTimeout(() => s.classList.add('on'), i * 80));
    // 最后一片动画结束时收尾
    slices[slices.length - 1].addEventListener('transitionend', () => {
      console.log('过渡完成!');
    }, { once: true });
  });
</script>
</body>
</html>

📋 实习任务书

任务目标:攻克竞赛任务11/12/13三个难题

任务内容与评分
任务1:写出eval的三条罪状(10分)
任务2:完成任务11计算器(含括号与错误处理)(30分)
任务3:完成任务12拖拽(25分)
任务4:完成任务13切片动画(25分)
任务5:用事件委托重写一个列表点击(10分)

自我评价

📚 课后作业与预习