JavaScript 事件是浏览器或页面元素在特定操作 / 状态下触发的「信号」,允许开发者通过绑定「事件处理函数」来响应交互 (如点击、输入)、状态变化 (如页面加载完成) 或系统通知 (如网络错误),是实现前端交互的核心机制;

事件的核心概念

  1. 事件三要素:

    1. 事件源:触发事件的对象 (如按钮、文档、window)
    2. 事件类型:事件的具体名称 (如 click 点击、input 输入、load 加载)
    3. 事件处理程序:事件触发后执行的函数 (也称「事件监听器」)
  2. 事件流 (Event Flow)

    1. 事件触发后,会在 DOM 树中按特定顺序传播,分为三个阶段 (DOM2 级事件规范定义)
      1. 捕获阶段 (Capture Phase):事件从最顶层的 window 开始,向下遍历 DOM 树,直到到达事件源的父元素 (从外到内)
      2. 目标阶段 (Target Phase):事件到达真正触发的目标元素 (事件源)
      3. 冒泡阶段 (Bubbling Phase):事件从事件源开始,向上回溯 DOM 树,直到 window (从内到外)
    2. 注意:
      1. 默认情况下,事件处理程序绑定在 「冒泡阶段」
      2. 可通过 addEventListener 的第三个参数控制绑定阶段 (true 表示捕获阶段,false/默认 表示冒泡阶段)
    3. 示例代码:
      <div id="parent">
        <button id="child">点击</button>
      </div>
      
      <script>
      const parent = document.querySelector('#parent');
      const child = document.querySelector('#child');
      
      // 捕获阶段绑定
      parent.addEventListener('click', () => console.log('父元素-捕获'), true);
      // 冒泡阶段绑定
      parent.addEventListener('click', () => console.log('父元素-冒泡'), false);
      // 目标元素(不分捕获/冒泡,按绑定顺序执行)
      child.addEventListener('click', () => console.log('子元素-目标1'));
      child.addEventListener('click', () => console.log('子元素-目标2'), true);
      
      // 点击按钮输出顺序:
      // 父元素-捕获 → 子元素-目标1 → 子元素-目标2 → 父元素-冒泡
      </script>
      
  3. 事件对象 (Event Object):事件触发时,浏览器会自动创建一个 Event 对象并传入事件处理函数,包含事件的所有信息;

    1. 核心属性 / 方法
      名称 作用
      target 真正触发事件的元素 (事件源)
      currentTarget 当前绑定事件处理程序的元素 (如委托时的父元素)
      type 事件类型 (如 click、input)
      stopPropagation() 阻止事件继续传播 (捕获 / 冒泡阶段)
      preventDefault() 阻止事件的默认行为 (如点击链接跳转、表单提交)
      bubbles 布尔值,判断事件是否支持冒泡
      eventPhase 数字,标识当前事件处于哪个阶段 (1 = 捕获,2 = 目标,3 = 冒泡)
    2. 示例:
      // 阻止链接跳转
      const link = document.querySelector('a');
      link.addEventListener('click', (e) => {
        e.preventDefault(); // 阻止默认跳转行为
        console.log('链接被点击,但不跳转');
      });
      
      // 阻止事件冒泡
      child.addEventListener('click', (e) => {
        e.stopPropagation(); // 父元素的冒泡事件不会触发
        console.log('子元素点击,不冒泡');
      });
      

事件的绑定方式

  1. HTML 内联绑定 (不推荐):直接在 HTML 标签中通过 onxxx 属性绑定,耦合度高,不利于维护;

    <button onclick="console.log('点击了')">按钮</button>
    <button onclick="handleClick()">按钮</button>
    
    <script>
        function handleClick() {
            alert('点击事件触发');
        }
    </script>
    
  2. DOM 属性绑定 (局限性):通过元素的 onxxx 属性赋值,缺点是同一事件只能绑定一个处理程序 (后绑定会覆盖前一个)

    const btn = document.querySelector('#btn');
    btn.onclick = function() {
      console.log('第一个点击处理程序'); // 会被覆盖
    };
    btn.onclick = function() {
      console.log('第二个点击处理程序'); // 最终执行这个
    };
    // 移除事件:赋值为 null
    btn.onclick = null;
    
  3. addEventListener (推荐)DOM2 级事件规范,支持绑定多个事件处理程序,可控制传播阶段,兼容性好 (IE9+)

    const btn = document.querySelector('#btn');
    // 绑定多个处理程序
    btn.addEventListener('click', () => console.log('处理程序1'));
    btn.addEventListener('click', () => console.log('处理程序2'));
    
    // 移除事件(必须传绑定的同一个函数引用,匿名函数无法移除)
    function handleClick() {
      console.log('可移除的处理程序');
    }
    btn.addEventListener('click', handleClick);
    btn.removeEventListener('click', handleClick); // 成功移除
    
  4. IE 兼容 (attachEvent/detachEvent,已淘汰)IE8 及以下不支持 addEventListener,需用 attachEvent (仅支持冒泡阶段)

    // 仅 IE8- 可用
    btn.attachEvent('onclick', handleClick); // 事件类型带 on
    btn.detachEvent('onclick', handleClick); // 移除事件
    

常见事件分类

  1. 鼠标事件:

    事件名称 触发时机
    click 鼠标左键单击 (按下 + 松开)
    dblclick 鼠标左键双击
    mousedown 鼠标按键按下 (任意键)
    mouseup 鼠标按键松开 (任意键)
    mousemove 鼠标在元素内移动 (持续触发)
    mouseover 鼠标移入元素 (含子元素,会冒泡)
    mouseout 鼠标移出元素 (含子元素,会冒泡)
    mouseenter 鼠标移入元素 (不含子元素,不冒泡),性能优于 mouseover
    mouseleave 鼠标移出元素 (不含子元素,不冒泡),性能优于 mouseout
    wheel 鼠标滚轮滚动时触发
    contextmenu 右键菜单触发 (可阻止默认行为自定义右键)
  2. 键盘事件

    事件名称 触发时机 核心属性
    keydown 键盘按键按下 (持续触发) key (按键名)、~keyCode~ (键码,已淘汰)
    keyup 键盘按键松开 key (按键名)、~keyCode~ (键码,已淘汰)
    keypress ~按下字符键~ (已淘汰) -
  3. 表单事件

    事件名称 触发时机
    submit 表单提交 (绑定到 form 元素)
    change 表单值改变且失去焦点 (如输入框、下拉框)
    input 表单值实时改变 (输入框、文本域)
    focus 元素获得焦点 (不冒泡)
    blur 元素失去焦点 (不冒泡)
    focusin 元素获得焦点 (冒泡,替代 focus)
    focusout 元素失去焦点 (冒泡,替代 blur)
  4. 文档 / 窗口事件

    事件名称 触发时机
    load 页面 / 资源 (图片、脚本) 加载完成
    DOMContentLoaded DOM 解析完成 (无需等待资源加载,更快)
    resize 窗口大小改变 (持续触发)
    scroll 页面 / 元素滚动 (持续触发)
    unload 页面卸载 (如关闭标签,慎用)
    beforeunload 页面即将卸载 (可提示用户确认离开)
  5. 剪切板事件

    事件名称 触发时机
    cut 用户执行剪切操作 (支持冒泡、可阻止默认行为)
    copy 用户执行复制操作 (支持冒泡、可修改剪贴板内容)
    paste 用户执行粘贴操作 (支持冒泡、可读取剪贴板内容)
  6. 触摸事件 (移动端)

    事件名称 触发时机
    touchstart 手指触摸屏幕
    touchmove 手指在屏幕上移动
    touchend 手指离开屏幕
    touchcancel 触摸被中断 (如弹窗、电话)

事件委托(事件代理)

  1. 核心原理:利用事件冒泡,将子元素的事件绑定到父元素上,通过 event.target 判断触发事件的子元素,从而处理事件;

  2. 优势:

    1. 减少事件绑定数量,优化性能 (如列表有 1000 项,只需绑定 1 个事件)
    2. 支持动态添加的子元素 (无需重新绑定事件)
  3. 示例:

    <ul id="list">
      <li>项1</li>
      <li>项2</li>
      <li>项3</li>
    </ul>
    
    <script>
    const list = document.querySelector('#list');
    // 事件委托到父元素 ul
    list.addEventListener('click', (e) => {
      // 判断触发元素是否为 li
      if (e.target.tagName === 'LI') {
        console.log('点击了:', e.target.textContent);
      }
    });
    
    // 动态添加 li(无需重新绑定事件)
    const newLi = document.createElement('li');
    newLi.textContent = '项4';
    list.appendChild(newLi);
    </script>
    

事件优化

防抖(Debounce)

  1. 核心概念:触发事件后,在指定时间内如果再次触发该事件,则重新计时,直到最后一次触发后等待指定时间,才执行目标函数;

  2. 应用场景:

    1. 搜索框输入联想 (避免输入过程中频繁请求接口)
    2. 窗口 resizescroll 事件 (避免频繁触发导致性能问题)
    3. 按钮点击防重复提交 (避免快速点击多次触发)
  3. 实现方案:

    /**
     * 基础防抖函数(非立即执行)
     * @param {Function} fn - 目标执行函数
     * @param {number} delay - 延迟时间(毫秒)
     * @returns {Function} 包装后的防抖函数
     */
    function debounce(fn, delay) {
      let timer = null; // 保存定时器标识
    
      return function (...args) {
        // 每次触发时,清除之前的定时器(重新计时)
        clearTimeout(timer);
        // 重新设置定时器,延迟执行目标函数
        timer = setTimeout(() => {
          fn.apply(this, args); // 绑定this和传递参数
        }, delay);
      };
    }
    
    // 测试示例
    const input = document.querySelector('input');
    input.addEventListener('input', debounce(function (e) {
      console.log('搜索关键词:', this.value); // this指向input元素
    }, 500));
    
    /**
     * 防抖函数(支持立即执行)
     * @param {Function} fn - 目标执行函数
     * @param {number} delay - 延迟时间(毫秒)
     * @param {boolean} immediate - 是否立即执行
     * @returns {Function} 包装后的防抖函数
     */
    function debounce(fn, delay, immediate = false) {
      let timer = null;
    
      return function (...args) {
        // 清除之前的定时器
        clearTimeout(timer);
    
        // 立即执行逻辑
        if (immediate && !timer) {
          fn.apply(this, args); // 首次触发立即执行
        }
    
        // 重新设置定时器:如果是立即执行,延迟期间禁止再次执行;非立即执行则延迟执行
        timer = setTimeout(() => {
          if (!immediate) {
            fn.apply(this, args);
          }
          timer = null; // 延迟结束后重置定时器,允许下次立即执行
        }, delay);
      };
    }
    
    // 测试示例:按钮点击立即执行,后续快速点击不触发,直到延迟结束
    const btn = document.querySelector('button');
    btn.addEventListener('click', debounce(function () {
      console.log('按钮点击执行');
    }, 1000, true));
    
    /**
     * 防抖函数(支持立即执行 + 取消)
     * @param {Function} fn - 目标执行函数
     * @param {number} delay - 延迟时间(毫秒)
     * @param {boolean} immediate - 是否立即执行
     * @returns {Function} 包装后的防抖函数(包含cancel方法)
     */
    function debounce(fn, delay, immediate = false) {
      let timer = null;
    
      const debounced = function (...args) {
        clearTimeout(timer);
    
        if (immediate && !timer) {
          fn.apply(this, args);
        }
    
        timer = setTimeout(() => {
          if (!immediate) {
            fn.apply(this, args);
          }
          timer = null;
        }, delay);
      };
    
      // 取消防抖:清除定时器,重置状态
      debounced.cancel = function () {
        clearTimeout(timer);
        timer = null;
      };
    
      return debounced;
    }
    
    // 测试示例:手动取消防抖
    const debouncedFn = debounce(() => console.log('执行'), 1000);
    debouncedFn(); // 触发防抖
    setTimeout(() => {
      debouncedFn.cancel(); // 取消防抖,函数不会执行
    }, 500);
    

节流(Throttle)

  1. 核心概念:触发事件后,在指定时间内只允许执行一次目标函数,即使多次触发,也仅执行一次;

  2. 应用场景:

    1. 滚动加载 (避免滚动时频繁请求数据)
    2. 高频点击按钮 (限制点击频率)
    3. 鼠标移动 / 拖拽事件 (减少计算次数)
  3. 实现方案:

/**
 * 节流函数(时间戳版)
 * @param {Function} fn - 目标执行函数
 * @param {number} interval - 节流间隔(毫秒)
 * @returns {Function} 包装后的节流函数
 */
function throttle(fn, interval) {
  let lastTime = 0; // 上一次执行的时间戳

  return function (...args) {
    const now = Date.now(); // 当前时间戳
    // 如果当前时间 - 上一次执行时间 >= 间隔,执行函数
    if (now - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = now; // 更新上一次执行时间
    }
  };
}

// 测试示例:滚动事件节流
window.addEventListener('scroll', throttle(function () {
  console.log('滚动位置:', window.scrollY);
}, 500));
/**
 * 节流函数(定时器版)
 * @param {Function} fn - 目标执行函数
 * @param {number} interval - 节流间隔(毫秒)
 * @returns {Function} 包装后的节流函数
 */
function throttle(fn, interval) {
  let timer = null;

  return function (...args) {
    // 如果没有定时器,设置定时器(阻塞期间不重复设置)
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null; // 执行后清空定时器,允许下次触发
      }, interval);
    }
  };
}

// 测试示例:点击节流(延迟执行,快速点击仅最后一次触发后延迟执行)
const btn = document.querySelector('button');
btn.addEventListener('click', throttle(() => {
  console.log('按钮点击执行');
}, 1000));
/**
 * 节流函数(综合版:立即执行 + 最后一次执行)
 * @param {Function} fn - 目标执行函数
 * @param {number} interval - 节流间隔(毫秒)
 * @returns {Function} 包装后的节流函数(包含cancel方法)
 */
function throttle(fn, interval) {
  let lastTime = 0;
  let timer = null;

  const throttled = function (...args) {
    const now = Date.now();
    // 计算剩余时间:如果当前时间离上一次执行不足间隔,剩余时间 = 间隔 - (当前 - 上一次)
    const remaining = interval - (now - lastTime);

    // 情况1:剩余时间 <= 0 → 立即执行(时间戳逻辑)
    if (remaining <= 0) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(this, args);
      lastTime = now;
    } 
    // 情况2:剩余时间 > 0 且无定时器 → 设置定时器(保证最后一次触发执行)
    else if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        lastTime = Date.now();
        timer = null;
      }, remaining);
    }
  };

  // 取消节流
  throttled.cancel = function () {
    clearTimeout(timer);
    timer = null;
    lastTime = 0;
  };

  return throttled;
}

// 测试示例:兼顾立即执行和最后一次执行
const throttledFn = throttle(() => console.log('执行'), 1000);
throttledFn(); // 立即执行
throttledFn(); // 阻塞,1秒后执行最后一次

常见问题与注意事项

  1. this 指向:

    1. 事件处理函数中,this 默认指向绑定事件的元素 (currentTarget)
    2. 箭头函数的 this 指向外层作用域 (非元素),需注意;
  2. 事件移除:

    1. addEventListener 绑定的匿名函数无法移除,需使用具名函数;
    2. 页面卸载前移除不必要的事件,避免内存泄漏;
  3. 默认行为阻止:

    1. preventDefault() 仅阻止默认行为,不阻止事件传播;
    2. return false (仅内联 / DOM 属性绑定) 等价于 preventDefault() + stopPropagation()
  4. 被动事件监听器:移动端滚动事件中,添加 { passive: true } 可提升滚动性能 (避免浏览器等待 preventDefault())

    window.addEventListener('touchmove', handler, { passive: true });
    

H4 拖拽 vs H5 拖拽

核心差异总览

维度 H4 拖拽(模拟实现) H5 原生拖拽 API
底层原理 监听 mousedown/mousemove/mouseup 模拟 浏览器原生支持的拖拽事件体系
事件体系 自定义鼠标事件组合 专属拖拽事件 (dragstart/drag/dragend 等)
数据传递 手动维护变量传递 内置 DataTransfer 对象管理拖拽数据
跨元素 / 跨窗口 需手动处理边界,跨窗口几乎不可行 原生支持跨元素,部分浏览器支持跨窗口
兼容性 所有浏览器 (IE6+) IE10+Chrome/Firefox/Safari 主流版本
开发成本 (需处理偏移、防抖、边界等) (原生 API 封装,只需处理核心逻辑)
功能丰富度 需手动扩展 (如拖拽预览、禁止拖拽) 原生支持拖拽预览、拖放效、禁止 / 允许拖放等

H4 拖拽

  1. 核心原理:H4 没有专门的拖拽 API,完全通过监听鼠标的三个核心事件模拟拖拽流程:

    1. mousedown:按下鼠标,标记 “开始拖拽”,记录初始位置 (鼠标坐标、元素偏移)
    2. mousemove:鼠标移动时,计算偏移量,动态修改元素的 position (通常是 absolute/fixed) 实现跟随;
    3. mouseup:松开鼠标,结束拖拽,清除标记 / 事件监听;
  2. 关键问题 (需手动处理)

    1. 坐标偏移:元素的 top/left 需基于鼠标点击位置 (而非元素左上角) 计算,否则会出现 “瞬移”;
    2. 事件防抖:mousemove 触发频率极高,需避免过度重绘;
    3. 边界限制:若需限制拖拽范围 (如窗口内),需手动判断坐标边界;
    4. 层级问题:拖拽元素需设置 z-index 避免被遮挡;
    5. 事件冒泡:需阻止事件冒泡,避免影响其他元素;
  3. 示例代码:

    1. e.preventDefault() 为什么要加?
      1. 阻止文本选中:如果拖拽元素内有文本,鼠标按下并移动时,浏览器默认会选中文本,导致拖拽过程中出现选中文本的蓝色背景,体验极差;
      2. 阻止默认的拖拽行为:浏览器对部分元素 (如图片、链接) 有默认的拖拽逻辑,preventDefault() 可以屏蔽这些默认行为,避免和自定义拖拽冲突;
      3. 避免滚动 / 选中其他元素:防止鼠标按下时意外触发页面滚动、选中页面其他文本等问题;
    2. 为什么 mousedown 绑定到 dragEle,而 mousemove/mouseup 绑定到 document
      事件 绑定目标 核心原因
      mousedown dragEle 只有在拖拽元素上按下鼠标时,才应该触发拖拽初始化 (其他区域按下无意义)
      mousemove document 拖拽时鼠标可能快速移动,甚至移出拖拽元素范围 (比如元素小、鼠标移快),绑定到 document 能确保「鼠标移动时始终能检测到」,避免拖拽中断
      mouseup document 同理,鼠标松开时可能已经移出拖拽元素范围,绑定到 document 能确保「无论鼠标在哪松开,都能结束拖拽」,避免出现「元素粘在鼠标上」的 bug
    <!DOCTYPE html>
    <html>
    <head>
      <style>
        #drag-h4 {
          width: 100px;
          height: 100px;
          background: #42b983;
          position: absolute;
          cursor: move;
          user-select: none;
          transition: z-index 0.2s ease;
          /* 初始transform重置 */
          transform: translate(0, 0);
          text-align: center;
          line-height: 100px;
          color: white;
        }
        
        /* 确保body无默认边距,避免滚动条干扰 */
        body {
          margin: 0;
          overflow: hidden; /* 彻底禁用滚动条(可选,根据需求调整) */
        }
      </style>
    </head>
    <body>
      <div id="drag-h4">H4 拖拽</div>
    
      <script>
        const dragEle = document.getElementById('drag-h4');
        let isDragging = false;
        let startX, startY;
        let initTranslateX = 0, initTranslateY = 0; // 记录transform初始偏移
        let eleWidth, eleHeight;
    
        // 初始化元素尺寸
        const initEleSize = () => {
          eleWidth = dragEle.offsetWidth;
          eleHeight = dragEle.offsetHeight;
        };
    
        // 计算视口可用区域(排除滚动条)
        const getViewportBounds = () => {
          // 可用宽度 = 视口宽度 - 滚动条宽度(如有)
          const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
          const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
          return {
            maxX: Math.max(0, viewportWidth - eleWidth),
            maxY: Math.max(0, viewportHeight - eleHeight)
          };
        };
    
        // 限制坐标在视口内(无滚动条)
        const clampPosition = (x, y) => {
          const { maxX, maxY } = getViewportBounds();
          return {
            x: Math.max(0, Math.min(x, maxX)),
            y: Math.max(0, Math.min(y, maxY))
          };
        };
    
        // 解析当前transform的偏移值
        const getCurrentTranslate = () => {
          const transform = window.getComputedStyle(dragEle).transform;
          if (transform === 'none') return { x: 0, y: 0 };
          
          const matrix = new DOMMatrix(transform);
          return {
            x: matrix.e,
            y: matrix.f
          };
        };
    
        // 开始拖拽
        const handleMouseDown = (e) => {
          isDragging = true;
          initEleSize();
          
          // 记录鼠标初始位置
          startX = e.clientX;
          startY = e.clientY;
          
          // 获取当前transform偏移(关键:替代offsetLeft/Top)
          const currentTranslate = getCurrentTranslate();
          initTranslateX = currentTranslate.x;
          initTranslateY = currentTranslate.y;
          
          dragEle.style.zIndex = 100;
          e.preventDefault();
          
          // 阻止拖拽时页面滚动
          document.addEventListener('scroll', preventScroll, { passive: false });
        };
    
        // 拖拽移动
        const handleMouseMove = (e) => {
          if (!isDragging) return;
          
          // 计算偏移增量
          const deltaX = e.clientX - startX;
          const deltaY = e.clientY - startY;
          
          // 计算目标位置(初始transform + 增量)
          const targetX = initTranslateX + deltaX;
          const targetY = initTranslateY + deltaY;
          
          // 限制边界
          const { x, y } = clampPosition(targetX, targetY);
          
          // 使用transform更新位置(性能更优)
          dragEle.style.transform = `translate(${x}px, ${y}px)`;
        };
    
        // 结束拖拽
        const handleMouseUp = () => {
          if (isDragging) {
            isDragging = false;
            dragEle.style.zIndex = 1;
            // 恢复页面滚动
            document.removeEventListener('scroll', preventScroll);
          }
        };
    
        // 阻止页面滚动
        const preventScroll = (e) => {
          e.preventDefault();
        };
    
        // 监听鼠标离开窗口
        const handleMouseLeave = () => {
          handleMouseUp();
        };
    
        // 窗口大小变化时重新校准位置
        const handleResize = () => {
          if (isDragging) {
            initEleSize();
            const currentTranslate = getCurrentTranslate();
            const { x, y } = clampPosition(currentTranslate.x, currentTranslate.y);
            dragEle.style.transform = `translate(${x}px, ${y}px)`;
          }
        };
    
        // 绑定事件
        dragEle.addEventListener('mousedown', handleMouseDown);
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
        document.addEventListener('mouseleave', handleMouseLeave);
        window.addEventListener('resize', handleResize);
    
        // 初始化
        initEleSize();
    
        // 清理事件监听
        window.addEventListener('beforeunload', () => {
          dragEle.removeEventListener('mousedown', handleMouseDown);
          document.removeEventListener('mousemove', handleMouseMove);
          document.removeEventListener('mouseup', handleMouseUp);
          document.removeEventListener('mouseleave', handleMouseLeave);
          window.removeEventListener('resize', handleResize);
        });
      </script>
    </body>
    </html>
    
  4. 效果展示:

H5 拖拽

案例(五子棋游戏实现)

  1. 实现代码

    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>五子棋游戏</title>
        <style>
            * {
                margin: 0;
                padding: 0;
                box-sizing: border-box;
            }
    
            body {
                display: flex;
                flex-direction: column;
                align-items: center;
                background-color: #f5f5f5;
                font-family: "Microsoft Yahei", sans-serif;
                padding: 20px;
            }
    
            .game-title {
                font-size: 32px;
                color: #333;
                margin-bottom: 20px;
            }
    
            .game-container {
                position: relative;
            }
    
            /* 棋盘样式 */
            #chessboard {
                background-color: #eec085;
                border: 8px solid #b98648;
                box-shadow: 0 0 10px rgba(0,0,0,0.2);
            }
    
            /* 控制按钮区域 */
            .control-panel {
                margin-top: 20px;
                display: flex;
                gap: 15px;
                align-items: center;
            }
    
            .btn {
                padding: 10px 20px;
                border: none;
                border-radius: 5px;
                background-color: #4CAF50;
                color: white;
                font-size: 16px;
                cursor: pointer;
                transition: background-color 0.3s;
            }
    
            .btn:hover {
                background-color: #45a049;
            }
    
            .btn:disabled {
                background-color: #cccccc;
                cursor: not-allowed;
            }
    
            .status {
                font-size: 18px;
                color: #333;
                min-width: 200px;
                text-align: center;
            }
    
            /* 胜利提示弹窗 */
            .modal {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background-color: rgba(0,0,0,0.5);
                display: none;
                justify-content: center;
                align-items: center;
                z-index: 100;
            }
    
            .modal-content {
                background-color: white;
                padding: 30px;
                border-radius: 10px;
                text-align: center;
                box-shadow: 0 0 20px rgba(0,0,0,0.3);
            }
    
            .modal-title {
                font-size: 24px;
                margin-bottom: 20px;
                color: #333;
            }
    
            .modal-btn {
                padding: 8px 20px;
                font-size: 16px;
            }
        </style>
    </head>
    <body>
        <h1 class="game-title">五子棋</h1>
        <div class="game-container">
            <canvas id="chessboard" width="600" height="600"></canvas>
        </div>
        <div class="control-panel">
            <button class="btn" id="restartBtn">重新开始</button>
            <button class="btn" id="regretBtn" disabled>悔棋</button>
            <div class="status" id="status">当前回合:黑棋</div>
        </div>
    
        <!-- 胜利弹窗 -->
        <div class="modal" id="winModal">
            <div class="modal-content">
                <h2 class="modal-title" id="winText">黑棋获胜!</h2>
                <button class="btn modal-btn" id="modalRestartBtn">再来一局</button>
            </div>
        </div>
    
        <script>
            // 1. 基础配置
            const config = {
                chessSize: 600,    // 棋盘尺寸
                gridCount: 15,     // 网格数(15x15)
                lineWidth: 1,      // 线条宽度
                pieceRadius: 10,   // 棋子半径
                pieceMargin: 2     // 棋子边距
            };
    
            // 计算网格间距
            config.gridSize = config.chessSize / (config.gridCount - 1);
    
            // 2. DOM元素获取
            const chessboard = document.getElementById('chessboard');
            const ctx = chessboard.getContext('2d');
            const restartBtn = document.getElementById('restartBtn');
            const regretBtn = document.getElementById('regretBtn');
            const statusText = document.getElementById('status');
            const winModal = document.getElementById('winModal');
            const winText = document.getElementById('winText');
            const modalRestartBtn = document.getElementById('modalRestartBtn');
    
            // 3. 游戏状态管理
            let gameState = {
                chessData: Array(config.gridCount).fill().map(() => Array(config.gridCount).fill(0)), // 0:空 1:黑 2:白
                currentPlayer: 1,  // 当前玩家(1:黑 2:白)
                gameOver: false,   // 游戏是否结束
                history: [],       // 落子历史(悔棋用)
                lastPos: null      // 最后落子位置
            };
    
            // 4. 初始化棋盘
            function initChessboard() {
                // 清空画布
                ctx.clearRect(0, 0, config.chessSize, config.chessSize);
    
                // 设置线条样式
                ctx.strokeStyle = '#000';
                ctx.lineWidth = config.lineWidth;
    
                // 绘制网格线
                for (let i = 0; i < config.gridCount; i++) {
                    // 横线
                    ctx.beginPath();
                    ctx.moveTo(0, i * config.gridSize);
                    ctx.lineTo(config.chessSize, i * config.gridSize);
                    ctx.stroke();
    
                    // 竖线
                    ctx.beginPath();
                    ctx.moveTo(i * config.gridSize, 0);
                    ctx.lineTo(i * config.gridSize, config.chessSize);
                    ctx.stroke();
                }
    
                // 绘制天元和星位
                drawStarPoints();
            }
    
            // 绘制天元和星位(五子棋标准点位)
            function drawStarPoints() {
                const starPositions = [
                    {x: 3, y: 3}, {x: 11, y: 3},
                    {x: 7, y: 7},  // 天元
                    {x: 3, y: 11}, {x: 11, y: 11}
                ];
    
                ctx.fillStyle = '#000';
                starPositions.forEach(pos => {
                    const x = pos.x * config.gridSize;
                    const y = pos.y * config.gridSize;
                    ctx.beginPath();
                    ctx.arc(x, y, 5, 0, Math.PI * 2);
                    ctx.fill();
                });
            }
    
            // 5. 落子逻辑
            function placeChess(x, y) {
                // 边界检查
                if (x < 0 || x >= config.gridCount || y < 0 || y >= config.gridCount) return false;
    
                // 检查是否已有棋子/游戏结束
                if (gameState.chessData[y][x] !== 0 || gameState.gameOver) return false;
    
                // 记录落子
                gameState.chessData[y][x] = gameState.currentPlayer;
                gameState.history.push({x, y, player: gameState.currentPlayer});
                gameState.lastPos = {x, y};
    
                // 绘制棋子
                drawChessPiece(x, y, gameState.currentPlayer);
    
                // 检查胜负
                if (checkWin(x, y)) {
                    gameState.gameOver = true;
                    showWinModal(gameState.currentPlayer);
                    return true;
                }
    
                // 切换玩家
                gameState.currentPlayer = gameState.currentPlayer === 1 ? 2 : 1;
                updateStatus();
    
                // 启用悔棋按钮
                regretBtn.disabled = false;
    
                return true;
            }
    
            // 绘制棋子
            function drawChessPiece(x, y, player) {
                const posX = x * config.gridSize;
                const posY = y * config.gridSize;
    
                // 设置棋子颜色
                ctx.fillStyle = player === 1 ? '#000' : '#fff';
    
                // 绘制棋子
                ctx.beginPath();
                ctx.arc(posX, posY, config.pieceRadius - config.pieceMargin, 0, Math.PI * 2);
                ctx.fill();
    
                // 绘制白棋边框
                if (player === 2) {
                    ctx.strokeStyle = '#000';
                    ctx.lineWidth = 1;
                    ctx.stroke();
                }
            }
    
            // 6. 胜负判断(核心算法)
            function checkWin(x, y) {
                const player = gameState.currentPlayer;
                const directions = [
                    [1, 0],   // 水平
                    [0, 1],   // 垂直
                    [1, 1],   // 对角线
                    [1, -1]   // 反对角线
                ];
    
                // 检查每个方向
                for (let [dx, dy] of directions) {
                    let count = 1;  // 当前棋子计数
    
                    // 正向检查
                    for (let i = 1; i < 5; i++) {
                        const nx = x + dx * i;
                        const ny = y + dy * i;
                        if (nx < 0 || nx >= config.gridCount || ny < 0 || ny >= config.gridCount) break;
                        if (gameState.chessData[ny][nx] === player) count++;
                        else break;
                    }
    
                    // 反向检查
                    for (let i = 1; i < 5; i++) {
                        const nx = x - dx * i;
                        const ny = y - dy * i;
                        if (nx < 0 || nx >= config.gridCount || ny < 0 || ny >= config.gridCount) break;
                        if (gameState.chessData[ny][nx] === player) count++;
                        else break;
                    }
    
                    // 五子连珠
                    if (count >= 5) return true;
                }
    
                return false;
            }
    
            // 7. 悔棋功能
            function regretChess() {
                if (gameState.history.length === 0 || gameState.gameOver) return;
    
                // 获取最后一步
                const lastStep = gameState.history.pop();
                const {x, y} = lastStep;
    
                // 清空该位置
                gameState.chessData[y][x] = 0;
                gameState.lastPos = gameState.history.length > 0 ? gameState.history[gameState.history.length - 1] : null;
    
                // 重新绘制棋盘
                initChessboard();
    
                // 重新绘制所有棋子
                gameState.history.forEach(step => {
                    drawChessPiece(step.x, step.y, step.player);
                });
    
                // 切换回上一玩家
                gameState.currentPlayer = gameState.currentPlayer === 1 ? 2 : 1;
                updateStatus();
    
                // 如果没有历史记录,禁用悔棋按钮
                if (gameState.history.length === 0) {
                    regretBtn.disabled = true;
                }
    
                // 如果游戏结束后悔棋,恢复游戏状态
                if (gameState.gameOver) {
                    gameState.gameOver = false;
                    winModal.style.display = 'none';
                }
            }
    
            // 8. 界面更新函数
            // 更新状态文本
            function updateStatus() {
                statusText.textContent = gameState.gameOver 
                    ? '游戏结束' 
                    : `当前回合:${gameState.currentPlayer === 1 ? '黑棋' : '白棋'}`;
            }
    
            // 显示胜利弹窗
            function showWinModal(winner) {
                winText.textContent = `${winner === 1 ? '黑棋' : '白棋'}获胜!`;
                winModal.style.display = 'flex';
                updateStatus();
            }
    
            // 重置游戏
            function resetGame() {
                gameState = {
                    chessData: Array(config.gridCount).fill().map(() => Array(config.gridCount).fill(0)),
                    currentPlayer: 1,
                    gameOver: false,
                    history: [],
                    lastPos: null
                };
                initChessboard();
                updateStatus();
                regretBtn.disabled = true;
                winModal.style.display = 'none';
            }
    
            // 9. 事件绑定
            // 棋盘点击事件
            chessboard.addEventListener('click', (e) => {
                if (gameState.gameOver) return;
    
                // 获取点击位置(相对棋盘)
                const rect = chessboard.getBoundingClientRect();
                const clickX = e.clientX - rect.left;
                const clickY = e.clientY - rect.top;
    
                // 计算最近的网格点
                const x = Math.round(clickX / config.gridSize);
                const y = Math.round(clickY / config.gridSize);
    
                // 落子
                placeChess(x, y);
            });
    
            // 重新开始按钮
            restartBtn.addEventListener('click', resetGame);
            modalRestartBtn.addEventListener('click', resetGame);
    
            // 悔棋按钮
            regretBtn.addEventListener('click', regretChess);
    
            // 初始化游戏
            initChessboard();
            updateStatus();
        </script>
    </body>
    </html>
    
  2. 效果展示

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

你好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 事件的核心概念
  2. 2. 事件的绑定方式
  3. 3. 常见事件分类
  4. 4. 事件委托(事件代理)
  5. 5. 事件优化
    1. 5.1. 防抖(Debounce)
    2. 5.2. 节流(Throttle)
  6. 6. 常见问题与注意事项
  7. 7. H4 拖拽 vs H5 拖拽
    1. 7.1. 核心差异总览
    2. 7.2. H4 拖拽
    3. 7.3. H5 拖拽
  8. 8. 案例(五子棋游戏实现)