导读:本期聚焦于小鱼创作的《如何优化JavaScript井字棋胜利判断逻辑并避免多循环引发的TypeError?》,敬请观看详情。井字棋项目里最常见的报错不是胜负判断写错,而是 TypeError: Cannot read properties of undefined。问题往往集中在多重循环里访问二维棋盘时出现:行索引或列索引越界、获胜线路数组配置了不存在的坐标、循环变量被错误复用,都会让代码尝试从 undefined 上继续读取下标。本文从一段典型的多循环判断代码入手,拆解 TypeError 的具体触发链路,然后改用一维数组表示棋盘,配合八条预定义胜利线和单层遍历完成胜负判断。优化后的实现不仅去掉了容易越界的嵌套结构,还通过前置校验和 Array every 增强健壮性。文章最后给出可直接运行的完整示例,并覆盖横向、纵向、对角线以及平局等测试场景,帮助前端开发者在类似棋盘类游戏里避免同类问题。

井字棋的胜负判断原本只是检查八条线,但用多循环硬套时,代码经常在运行阶段抛出 TypeError。这个错误并不是胜负算法本身复杂,而是访问二维数组时行或列越界,或者循环嵌套层级与棋盘结构不匹配。很多实现会先构造一个三行三列的二维数组,再分别用两层循环检查横线和竖线,最后单独处理两条对角线。表面上看逻辑覆盖了所有情况,实际上只要某个索引超出范围,JavaScript 就会返回 undefined,接着继续用方括号访问 undefined 的属性,于是 TypeError 就出现了。

如何优化JavaScript井字棋胜利判断逻辑并避免多循环引发的TypeError?

这类问题在浏览器控制台里通常显示为 Cannot read properties of undefined (reading '0') 或 Cannot read properties of undefined (reading '1')。排查时如果只盯着判断逻辑看,很难发现是数据结构或循环边界设计不当。更常见的是把一维数组的下标按二维方式访问,例如 board[row][col] 中的 board[row] 已经是 undefined,再取 [col] 就必然报错。下面先拆解一个典型的多循环写法,看看错误是如何被一步步触发的。

一、多循环判断的常见写法与TypeError触发链路

假设棋盘用一个二维数组表示,每个格子存放 'X'、'O' 或 null。为了判断胜利,很多人会写类似下面的代码。外层循环遍历行和列,内层循环逐个比较。问题在于,当行循环的索引变量被误用成列长度,或者胜利线数组中的坐标超出棋盘范围时,undefined 就会参与后续比较。

function checkWinner(board) {
  // board 是 3x3 二维数组
  for (let row = 0; row < board.length; row++) {
    if (board[row][0] === board[row][1] && board[row][1] === board[row][2]) {
      return board[row][0];
    }
  }
  for (let col = 0; col < board.length; col++) {
    if (board[0][col] === board[1][col] && board[1][col] === board[2][col]) {
      return board[0][col];
    }
  }
  if (board[0][0] === board[1][1] && board[1][1] === board[2][2]) {
    return board[0][0];
  }
  if (board[0][2] === board[1][1] && board[1][1] === board[2][0]) {
    return board[0][2];
  }
  return null;
}

这段代码在棋盘结构完整、大小固定为 3x3 时可以正常工作。但真实项目中棋盘可能来自网络请求、用户输入或状态恢复,一旦某一行缺失,例如 board[1] 为 undefined,执行 board[1][0] 就会抛出 TypeError。另一个隐蔽问题是如果棋盘被错误初始化成 2x3 的数组,列循环里的 board[2][col] 同样会访问不存在的第三行。多层循环加上硬编码下标,让这类错误在运行时才暴露,并且控制台堆栈经常指向判断函数内部,容易误导排查方向。

还有一个常见触发点是把一维数组误当二维数组使用。比如先用 const board = new Array(9).fill(null) 表示棋盘,却又在判断函数里写 board[row][col]。当 row 等于 0 时,board[0] 是 null,对 null 使用方括号访问属性也会直接报错。即使没有报错,null 与 'X' 比较也只是 false,无法正确判断胜负。因此,统一棋盘数据结构和访问方式,是解决 TypeError 的第一步。

二、改为一维棋盘并预定义胜利线

更稳健的做法是放弃二维数组,直接用长度为 9 的一维数组存储棋盘。九个格子的索引从 0 到 8,按照从左到右、从上到下的顺序排列。胜利判断不再需要嵌套循环,因为所有可能赢棋的线路只有八条:三条横线、三条竖线、两条对角线。把这八条线路预先定义为索引数组,然后逐条检查即可。

const WIN_LINES = [
  [0, 1, 2],
  [3, 4, 5],
  [6, 7, 8],
  [0, 3, 6],
  [1, 4, 7],
  [2, 5, 8],
  [0, 4, 8],
  [2, 4, 6]
];

function getWinner(board) {
  for (let i = 0; i < WIN_LINES.length; i++) {
    const [a, b, c] = WIN_LINES[i];
    if (board[a] && board[a] === board[b] && board[a] === board[c]) {
      return board[a];
    }
  }
  return null;
}

这个版本的循环只有一层,不再依赖二维数组的行列关系。每个获胜组合由三个明确存在的一维索引构成,只要 board 是长度为 9 的数组,就不会因为访问 board[1][0] 这类结构而触发 TypeError。判断条件中先写 board[a] 可以过滤掉 null、undefined 和空字符串,避免三个空格子被误判为胜利。逻辑上,只有当三个格子值相同且不为空时才返回胜利方。

如果需要同时判断是否平局,可以在没有胜利方时检查棋盘是否已满。此时仍然不需要多层循环,使用数组的 every 方法即可。例如 const isDraw = board.every(cell => cell !== null);。它与胜利判断分离,职责更清晰。这个方案的核心思想是用数据驱动代替过程控制:与其用循环描述如何找胜利线,不如把胜利线作为数据预先声明,让判断函数保持简短。

三、增加防御性校验和错误提示

即使改成一维数组,传入错误的数据结构仍然可能产生其他错误。比如调用方传入了二维数组,或传入的数组长度不足 9。此时判断函数可能在比较时得到 undefined,虽然不会抛 TypeError,但返回结果可能不符合预期。更稳妥的方式是在函数入口处做一次结构校验,遇到不符合约定的数据直接抛出带有明确信息的错误。

function assertValidBoard(board) {
  if (!Array.isArray(board) || board.length !== 9) {
    throw new TypeError('棋盘必须是包含 9 个元素的一维数组');
  }
  for (let i = 0; i < board.length; i++) {
    if (![null, undefined, 'X', 'O'].includes(board[i])) {
      throw new TypeError('棋盘第 ' + i + ' 个元素包含非法值: ' + board[i]);
    }
  }
}

function getWinnerSafe(board) {
  assertValidBoard(board);
  const winner = getWinner(board);
  return winner;
}

这里没有使用可选链,因为问题不是某个对象属性可能缺失,而是数组本身的结构与函数约定不一致。通过前置校验,可以让错误在开发阶段快速暴露,并携带更容易理解的提示。对于从接口返回的数据,也可以在赋值给状态前调用同样的校验函数,避免脏数据进入渲染层。

有些实现会使用 board?.[a] 避免读取 undefined 属性,但这只能掩盖问题。对于固定规则的井字棋来说,胜利判断不应该因为数据异常而静默返回 null,否则界面会一直显示对局未结束,用户却无法继续操作。防御性校验的重点不是吞掉异常,而是尽早暴露错误并给出可操作的上下文。

四、完整实现与边界场景验证

把上面的逻辑组合起来,可以得到一个完整的井字棋胜负判断模块。除了判断胜利方,还可以暴露检查平局和获取当前状态的方法。这样在实际项目中无论是 React 还是原生 JavaScript,都能直接调用,不需要再维护分散的多循环逻辑。

const WIN_LINES = [
  [0, 1, 2], [3, 4, 5], [6, 7, 8],
  [0, 3, 6], [1, 4, 7], [2, 5, 8],
  [0, 4, 8], [2, 4, 6]
];

function getWinner(board) {
  for (let i = 0; i < WIN_LINES.length; i++) {
    const [a, b, c] = WIN_LINES[i];
    if (board[a] && board[a] === board[b] && board[a] === board[c]) {
      return board[a];
    }
  }
  return null;
}

function isDraw(board) {
  return !getWinner(board) && board.every(cell => cell === 'X' || cell === 'O');
}

function getGameStatus(board) {
  const winner = getWinner(board);
  if (winner) {
    return winner + ' 获胜';
  }
  if (isDraw(board)) {
    return '平局';
  }
  return '对局进行中';
}

上面示例中 isDraw 使用数组 every 方法检查所有格子是否都被占满。getWinner 返回 null 时再用 isDraw 判断。由于 every 对空数组返回 true,但在棋盘长度固定的场景下不会出现空数组误判。如果担心传入空数组,可以在函数开头调用前面提到的 assertValidBoard。

测试时应当覆盖八条胜利线中的典型情况。例如横向胜利可构造 ['X','X','X',null,null,null,null,null,null];纵向胜利可构造 ['O',null,null,'O',null,null,'O',null,null];对角线胜利可构造 [null,null,'X',null,'X',null,'X',null,null]。将这些数组传入 getGameStatus,观察返回值是否与预期一致。再传入全满且无胜利方的数组,应返回平局。若传入二维数组或长度不足的数据,则应触发 TypeError,因为数据不符合约定。

JavaScript井字棋TypeError修改时间:2026-10-04 02:23:03

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1004/65347.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。