Mongoose查询结果过滤异常:误删归属正确的instances对象
我通过Mongoose的find查询得到一个展览对象数组,结构如下(每个展览包含artworks数组,每个艺术品又包含instances数组):
let exhibitions = [ { "_id": "63047848edbb342f6805166e", "name":"Exhibition1", "artworks": [ { "_id": "6303519929e74916708b508b", "instances": [ { "_id": "630486e2a13932fa8e2ff4b5", "parentID": "6303519929e74916708b508b", "exhibitionID": "63047848edbb342f6805166e", "HangLineID": "555", }, { "_id": "630486e2a13932fa8e2ff4b5", "parentID": "6303519929e74916708b508b", "exhibitionID": "63047848edbb342f6805166e", "HangLineID": "444", }, ] }, { "_id": "6304878ba13932fa8e2ff4d9", "instances": [ { "_id": "6308377d7c60564627494e16", "parentID": "6304878ba13932fa8e2ff4d9", "exhibitionID": "6304dbfb2af05390425969f7", "HangLineID": "333", } ] }, ], }, { "_id": "6304dbfb2af05390425969f7", "name":"Exhibition2", "artworks": [ { "_id": "62f20e22dc17b1712596016a", "instances": [ { "_id": "6304dc672af0539042596a07", "parentID": "62f20e22dc17b1712596016a", "exhibitionID": "6304dbfb2af05390425969f7", "HangLineID": "000", "HangLineArtworkArrayPosition": 2, "__v": 0 } ] } ] } ]
需求是:移除instances中exhibitionID与所属展览_id不匹配的元素(两者均为Mongoose ObjectId类型)。
尝试用Lodash的remove方法实现时,成功移除了Exhibition1中不符合的实例,但误删了Exhibition2中归属正确的实例。我的代码如下:
exhibitions.map((exh, key)=>{ const artworks = exh.artworks; artworks.map((artwork,index1) =>{ const instances = artwork.instances _.remove( instances, v => ( v.exhibitionID.toString()!=(exh._id).toString()) )}) })
经测试,v.exhibitionID.toString()!==(exh._id).toString()的条件判断输出是正确的;改用v.HangLineID!="555"时能正常过滤,怀疑是对象比较逻辑的问题?
核心问题:闭包变量引用导致的循环异常
你用map遍历artworks时,内部的_.remove回调里引用了外部的exh变量,但由于JavaScript闭包的特性,当循环执行到后期时,回调里的exh可能已经指向数组的最后一个元素(也就是Exhibition2),导致所有instances都用最后一个展览的_id做判断,从而误删Exhibition2中原本符合条件的实例。
修复方案
可以通过以下两种方式解决:
方案1:用forEach替代map(避免闭包引用问题)
map的设计目的是返回新数组,这里我们只是要修改原数组,用forEach更合适,同时也能避免闭包变量的引用异常:
exhibitions.forEach(exh => { exh.artworks.forEach(artwork => { const currentExhId = exh._id.toString(); _.remove(artwork.instances, v => v.exhibitionID.toString() !== currentExhId); }); });
方案2:提前缓存当前展览的ID,避免闭包引用
如果坚持用map,可以提前把当前展览的ID转成字符串缓存起来,确保回调里用的是当前循环的ID:
exhibitions.map(exh => { const currentExhId = exh._id.toString(); exh.artworks.map(artwork => { _.remove(artwork.instances, v => v.exhibitionID.toString() !== currentExhId); }); return exh; // map需要返回元素,否则会生成全为undefined的数组 });
额外优化:用Mongoose内置方法比较ObjectId
Mongoose的ObjectId本身提供了equals方法,可以直接用来比较,不需要转字符串,这样更可靠:
// 用equals方法替代toString比较 _.remove(artwork.instances, v => !v.exhibitionID.equals(exh._id));
内容的提问来源于stack exchange,提问作者Miss_scare_all

