JavaScript 事件是浏览器或页面元素在特定操作 / 状态下触发的「信号」,允许开发者通过绑定「事件处理函数」来响应交互 (如点击、输入)、状态变化 (如页面加载完成) 或系统通知 (如网络错误),是实现前端交互的核心机制;
事件的核心概念
-
事件三要素:
- 事件源:触发事件的对象 (如按钮、文档、window);
- 事件类型:事件的具体名称 (如 click 点击、input 输入、load 加载);
- 事件处理程序:事件触发后执行的函数 (也称「事件监听器」);
-
事件流 (Event Flow):
- 事件触发后,会在 DOM 树中按特定顺序传播,分为三个阶段 (DOM2 级事件规范定义)
- 捕获阶段 (Capture Phase):事件从最顶层的 window 开始,向下遍历 DOM 树,直到到达事件源的父元素 (从外到内);
- 目标阶段 (Target Phase):事件到达真正触发的目标元素 (事件源);
- 冒泡阶段 (Bubbling Phase):事件从事件源开始,向上回溯 DOM 树,直到 window (从内到外);
- 注意:
- 默认情况下,事件处理程序绑定在 「冒泡阶段」;
- 可通过 addEventListener 的第三个参数控制绑定阶段 (true 表示捕获阶段,false/默认 表示冒泡阶段);
- 示例代码:
<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>
- 事件触发后,会在 DOM 树中按特定顺序传播,分为三个阶段 (DOM2 级事件规范定义)
-
事件对象 (Event Object):事件触发时,浏览器会自动创建一个 Event 对象并传入事件处理函数,包含事件的所有信息;
- 核心属性 / 方法
名称 作用 target 真正触发事件的元素 (事件源) currentTarget 当前绑定事件处理程序的元素 (如委托时的父元素) type 事件类型 (如 click、input) stopPropagation() 阻止事件继续传播 (捕获 / 冒泡阶段) preventDefault() 阻止事件的默认行为 (如点击链接跳转、表单提交) bubbles 布尔值,判断事件是否支持冒泡 eventPhase 数字,标识当前事件处于哪个阶段 (1 = 捕获,2 = 目标,3 = 冒泡) - 示例:
// 阻止链接跳转 const link = document.querySelector('a'); link.addEventListener('click', (e) => { e.preventDefault(); // 阻止默认跳转行为 console.log('链接被点击,但不跳转'); }); // 阻止事件冒泡 child.addEventListener('click', (e) => { e.stopPropagation(); // 父元素的冒泡事件不会触发 console.log('子元素点击,不冒泡'); });
- 核心属性 / 方法
事件的绑定方式
-
HTML 内联绑定 (不推荐):直接在 HTML 标签中通过 onxxx 属性绑定,耦合度高,不利于维护;
<button onclick="console.log('点击了')">按钮</button> <button onclick="handleClick()">按钮</button> <script> function handleClick() { alert('点击事件触发'); } </script> -
DOM 属性绑定 (局限性):通过元素的 onxxx 属性赋值,缺点是同一事件只能绑定一个处理程序 (后绑定会覆盖前一个);
const btn = document.querySelector('#btn'); btn.onclick = function() { console.log('第一个点击处理程序'); // 会被覆盖 }; btn.onclick = function() { console.log('第二个点击处理程序'); // 最终执行这个 }; // 移除事件:赋值为 null btn.onclick = null; -
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); // 成功移除 -
IE 兼容 (attachEvent/detachEvent,已淘汰):IE8 及以下不支持 addEventListener,需用 attachEvent (仅支持冒泡阶段);
// 仅 IE8- 可用 btn.attachEvent('onclick', handleClick); // 事件类型带 on btn.detachEvent('onclick', handleClick); // 移除事件
常见事件分类
-
鼠标事件:
事件名称 触发时机 click 鼠标左键单击 (按下 + 松开) dblclick 鼠标左键双击 mousedown 鼠标按键按下 (任意键) mouseup 鼠标按键松开 (任意键) mousemove 鼠标在元素内移动 (持续触发) mouseover 鼠标移入元素 (含子元素,会冒泡) mouseout 鼠标移出元素 (含子元素,会冒泡) mouseenter 鼠标移入元素 (不含子元素,不冒泡),性能优于 mouseover mouseleave 鼠标移出元素 (不含子元素,不冒泡),性能优于 mouseout wheel 鼠标滚轮滚动时触发 contextmenu 右键菜单触发 (可阻止默认行为自定义右键) -
键盘事件
事件名称 触发时机 核心属性 keydown 键盘按键按下 (持续触发) key (按键名)、~ keyCode~ (键码,已淘汰)keyup 键盘按键松开 key (按键名)、~ keyCode~ (键码,已淘汰)keypress ~ 按下字符键~ (已淘汰)- -
表单事件
事件名称 触发时机 submit 表单提交 (绑定到 form 元素) change 表单值改变且失去焦点 (如输入框、下拉框) input 表单值实时改变 (输入框、文本域) focus 元素获得焦点 (不冒泡) blur 元素失去焦点 (不冒泡) focusin 元素获得焦点 (冒泡,替代 focus) focusout 元素失去焦点 (冒泡,替代 blur) -
文档 / 窗口事件
事件名称 触发时机 load 页面 / 资源 (图片、脚本) 加载完成 DOMContentLoaded DOM 解析完成 (无需等待资源加载,更快) resize 窗口大小改变 (持续触发) scroll 页面 / 元素滚动 (持续触发) unload 页面卸载 (如关闭标签,慎用) beforeunload 页面即将卸载 (可提示用户确认离开) -
剪切板事件
事件名称 触发时机 cut 用户执行剪切操作 (支持冒泡、可阻止默认行为) copy 用户执行复制操作 (支持冒泡、可修改剪贴板内容) paste 用户执行粘贴操作 (支持冒泡、可读取剪贴板内容) -
触摸事件 (移动端)
事件名称 触发时机 touchstart 手指触摸屏幕 touchmove 手指在屏幕上移动 touchend 手指离开屏幕 touchcancel 触摸被中断 (如弹窗、电话)
事件委托(事件代理)
-
核心原理:利用事件冒泡,将子元素的事件绑定到父元素上,通过 event.target 判断触发事件的子元素,从而处理事件;
-
优势:
- 减少事件绑定数量,优化性能 (如列表有 1000 项,只需绑定 1 个事件);
- 支持动态添加的子元素 (无需重新绑定事件);
-
示例:
<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)
-
核心概念:触发事件后,在指定时间内如果再次触发该事件,则重新计时,直到最后一次触发后等待指定时间,才执行目标函数;
-
应用场景:
- 搜索框输入联想 (避免输入过程中频繁请求接口);
- 窗口 resize、scroll 事件 (避免频繁触发导致性能问题);
- 按钮点击防重复提交 (避免快速点击多次触发);
-
实现方案:
/** * 基础防抖函数(非立即执行) * @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)
-
核心概念:触发事件后,在指定时间内只允许执行一次目标函数,即使多次触发,也仅执行一次;
-
应用场景:
- 滚动加载 (避免滚动时频繁请求数据);
- 高频点击按钮 (限制点击频率);
- 鼠标移动 / 拖拽事件 (减少计算次数);
-
实现方案:
/**
* 节流函数(时间戳版)
* @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秒后执行最后一次
常见问题与注意事项
-
this 指向:
- 事件处理函数中,this 默认指向绑定事件的元素 (currentTarget);
- 箭头函数的 this 指向外层作用域 (非元素),需注意;
-
事件移除:
- addEventListener 绑定的匿名函数无法移除,需使用具名函数;
- 页面卸载前移除不必要的事件,避免内存泄漏;
-
默认行为阻止:
preventDefault()仅阻止默认行为,不阻止事件传播;return false(仅内联 / DOM 属性绑定) 等价于preventDefault()+stopPropagation();
-
被动事件监听器:移动端滚动事件中,添加
{ 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 拖拽
-
核心原理:H4 没有专门的拖拽 API,完全通过监听鼠标的三个核心事件模拟拖拽流程:
- mousedown:按下鼠标,标记 “开始拖拽”,记录初始位置 (鼠标坐标、元素偏移);
- mousemove:鼠标移动时,计算偏移量,动态修改元素的 position (通常是 absolute/fixed) 实现跟随;
- mouseup:松开鼠标,结束拖拽,清除标记 / 事件监听;
-
关键问题 (需手动处理)
- 坐标偏移:元素的 top/left 需基于鼠标点击位置 (而非元素左上角) 计算,否则会出现 “瞬移”;
- 事件防抖:mousemove 触发频率极高,需避免过度重绘;
- 边界限制:若需限制拖拽范围 (如窗口内),需手动判断坐标边界;
- 层级问题:拖拽元素需设置 z-index 避免被遮挡;
- 事件冒泡:需阻止事件冒泡,避免影响其他元素;
-
示例代码:
- e.preventDefault() 为什么要加?
- 阻止文本选中:如果拖拽元素内有文本,鼠标按下并移动时,浏览器默认会选中文本,导致拖拽过程中出现选中文本的蓝色背景,体验极差;
- 阻止默认的拖拽行为:浏览器对部分元素 (如图片、链接) 有默认的拖拽逻辑,preventDefault() 可以屏蔽这些默认行为,避免和自定义拖拽冲突;
- 避免滚动 / 选中其他元素:防止鼠标按下时意外触发页面滚动、选中页面其他文本等问题;
- 为什么 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> - e.preventDefault() 为什么要加?
-
效果展示:
H5 拖拽
案例(五子棋游戏实现)
-
实现代码
<!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> -
效果展示
剑指 Offer 26.树的子结构
上一篇