遍历Mongoose查询结果数组时出现对象未定义错误
问题描述
我写了下面这段Express路由代码:
router.get("/bookings", async (req, res) => { var bookings = await Booking.find() console.log(bookings) result = [] for (i = 0; i < bookings.length; i++) { const challan = await Challan.findOne({ _id: bookings[i].challan }); var packagesDelivered = 0; console.log('Booking', JSON.stringify(bookings[i])) console.log('GR NO', bookings[i].grNo) for (j = 0; j < bookings[i].deliveries.length; j++) { const delivery = await Delivery.findOne({ _id: bookings[i].deliveries[j] }) packagesDelivered += delivery.noOfPackages; } result.push({ packagesDelivered: packagesDelivered, deliveries: bookings[i].deliveries, bookingDate: bookings[i].bookingDate, grNo: bookings[i].grNo, noOfPackages: bookings[i].noOfPackages, contents: bookings[i].contents, weight: bookings[i].weight, toPay: bookings[i].toPay, privateMark: bookings[i].privateMark, challanNo: challan.challanNo }) } res.send(result) })
运行时偶尔会在console.log('GR NO', bookings[i].grNo)处抛出错误,提示bookings[i]为undefined:
Booking undefined console.log('GR NO', bookings[i].grNo) ^ TypeError: Cannot read properties of undefined (reading 'grNo') at /Users/john/projects/myproj1/routes.js:396:36 at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
我搞不懂为什么会出现这个问题,毕竟循环条件是基于bookings.length设置的。以下是bookings的示例数据:
[ { _id: new ObjectId("62d6a1c8663cadb009367207"), bookingDate: '2022-07-17', grNo: '379757', noOfPackages: 40, contents: 'OLD CLOTHX', weight: 2000, toPay: 12000, privateMark: 'UNKWN', deliveries: [], challan: new ObjectId("62d6a1c7663cadb0093671f7"), __v: 0 }, { _id: new ObjectId("62d6a1c8663cadb009367205"), bookingDate: '2022-07-16', grNo: '379753', noOfPackages: 6, contents: 'GOODS', weight: 180, toPay: 1260, privateMark: 'SHIVAM', deliveries: [], challan: new ObjectId("62d6a1c7663cadb0093671f7"), __v: 0 }, { _id: new ObjectId("62d6a1c8663cadb009367203"), bookingDate: '2022-07-14', grNo: '389553', noOfPackages: 1, contents: 'HARDWARE GOODS', weight: 50, toPay: 350, privateMark: 'UNKWN', deliveries: [], challan: new ObjectId("62d6a1c7663cadb0093671f7"), __v: 0 }, { _id: new ObjectId("62d6a1c7663cadb009367201"), bookingDate: '2022-07-16', grNo: '387197', noOfPackages: 11, contents: 'E RICKSHAW', weight: 450, toPay: 3150, privateMark: 'UNKWN', deliveries: [], challan: new ObjectId("62d6a1c7663cadb0093671f7"), __v: 0 }, { _id: new ObjectId("62d6a1c7663cadb0093671ff"), bookingDate: '2022-07-17', grNo: '379756', noOfPackages: 18, contents: 'OLD CLOTH', weight: 1140, toPay: 6840, privateMark: 'DL ', deliveries: [], challan: new ObjectId("62d6a1c7663cadb0093671f7"), __v: 0 }, { _id: new ObjectId("62d6a1c7663cadb0093671fd"), bookingDate: '2022-07-16', grNo: '379754', noOfPackages: 12, contents: 'OLD CLOTH', weight: 880, toPay: 5280, privateMark: 'BB', deliveries: [], challan: new ObjectId("62d6a1c7663cadb0093671f7"), __v: 0 }, { _id: new ObjectId("62d6a1c7663cadb0093671fb"), bookingDate: '2022-07-17', grNo: '79352', noOfPackages: 116, contents: 'WASTE PAPER', weight: 6920, toPay: 31140, privateMark: 'UNKWN', deliveries: [], challan: new ObjectId("62d6a1c7663cadb0093671f7"), __v: 0 }, { _id: new ObjectId("62d6a1c7663cadb0093671f9"), bookingDate: '2022-07-16', grNo: '86520', noOfPackages: 35, contents: 'BED SHEET', weight: 2800, toPay: 15900, privateMark: 'UNKWN', deliveries: [], challan: new ObjectId("62d6a1c7663cadb0093671f7"), __v: 0 }, { _id: new ObjectId("62d69f6e663cadb0093670de"), bookingDate: '2022-07-16', grNo: '388755', noOfPackages: 7, contents: 'HARDWARE GOODS', weight: 330, toPay: 2310, privateMark: 'UNKWN', deliveries: [], challan: new ObjectId("62d69f6a663cadb0093670b6"), __v: 0 }, { _id: new ObjectId("62d69f6d663cadb0093670dc"), bookingDate: '2022-07-15', grNo: '380048', noOfPackages: 6, contents: 'MOTOR PARTS', weight: 300, toPay: 2100, privateMark: 'UNKWN', deliveries: [], challan: new ObjectId("62d69f6a663cadb0093670b6"), __v: 0 } ]
问题原因
核心问题是你在for循环里用了全局变量i和j,没有用let或const声明。因为路由是并发处理的,当第一个请求的循环遇到await(比如await Challan.findOne或await Delivery.findOne)时,JavaScript会暂停这个异步函数的执行,去处理其他请求。这时候其他请求的循环会修改全局的i值,当第一个请求恢复执行时,i已经超出了当前bookings数组的长度,导致bookings[i]变成undefined。
举个例子:假设第一个请求的bookings长度是10,循环到i=2时遇到await暂停;这时候第二个请求进来,它的循环把i加到了11;当第一个请求恢复执行,继续用i=11去访问bookings[11],自然就是undefined。
解决方案
把循环变量i和j用let声明,让它们变成块级作用域变量,每个循环迭代都有独立的变量副本,不会被其他请求干扰。同时,建议把result也用let声明,避免全局污染:
router.get("/bookings", async (req, res) => { const bookings = await Booking.find() console.log(bookings) let result = [] // 用let声明i for (let i = 0; i < bookings.length; i++) { const challan = await Challan.findOne({ _id: bookings[i].challan }); let packagesDelivered = 0; console.log('Booking', JSON.stringify(bookings[i])) console.log('GR NO', bookings[i].grNo) // 用let声明j for (let j = 0; j < bookings[i].deliveries.length; j++) { const delivery = await Delivery.findOne({ _id: bookings[i].deliveries[j] }) packagesDelivered += delivery.noOfPackages; } result.push({ packagesDelivered, deliveries: bookings[i].deliveries, bookingDate: bookings[i].bookingDate, grNo: bookings[i].grNo, noOfPackages: bookings[i].noOfPackages, contents: bookings[i].contents, weight: bookings[i].weight, toPay: bookings[i].toPay, privateMark: bookings[i].privateMark, challanNo: challan.challanNo }) } res.send(result) })
额外优化建议
- 避免嵌套循环+await的低效写法:当前代码里的嵌套await会导致请求处理速度很慢,因为每次都要等待数据库查询完成才能继续。可以用
Promise.all批量处理查询,提升性能:
router.get("/bookings", async (req, res) => { const bookings = await Booking.find() // 批量查询所有challan const challanIds = bookings.map(b => b.challan) const challans = await Challan.find({ _id: { $in: challanIds } }) // 把challan转成id映射,方便查找 const challanMap = new Map(challans.map(c => [c._id.toString(), c.challanNo])) const result = await Promise.all(bookings.map(async booking => { // 批量查询当前booking的所有deliveries const deliveries = await Delivery.find({ _id: { $in: booking.deliveries } }) const packagesDelivered = deliveries.reduce((sum, d) => sum + d.noOfPackages, 0) return { packagesDelivered, deliveries: booking.deliveries, bookingDate: booking.bookingDate, grNo: booking.grNo, noOfPackages: booking.noOfPackages, contents: booking.contents, weight: booking.weight, toPay: booking.toPay, privateMark: booking.privateMark, challanNo: challanMap.get(booking.challan.toString()) } })) res.send(result) })
这种写法把多次数据库查询合并成少数几次批量查询,性能会提升很多,尤其是当bookings数量较多时。
- 处理可能的null值:比如
Challan.findOne或Delivery.findOne可能返回null,建议添加判断,避免后续访问属性时出错,比如:
const challan = await Challan.findOne({ _id: bookings[i].challan }); if (!challan) { // 可以跳过这条booking,或者给challanNo设默认值 continue; }
内容的提问来源于stack exchange,提问作者Saif
相关产品推荐
相关产品推荐

