如何正确向Set添加唯一对象?Recipe食材去重异常排查
问题
我开发了一个包含Recipe数组的应用,每个Recipe包含ingredients子数组,想要提取所有Recipe中的唯一食材。当前实现为遍历所有食材,调用getEmptyIngredient创建空Calculation对象并加入Set,但控制台打印的Set中对象已被填充,且因对象引用差异导致相同食材被重复添加。此外,计算逻辑在打印之后执行,我疑惑为何打印时对象已被修改。
当前代码如下:
calculate(recipes: Recipe[]): Observable<Calculation[]> { let calculations: Calculation[] = []; let uniqueIngredients = new Set(); for (let i = 0; i < recipes.length; i++) { for (let j = 0; j < recipes[i].ingredients.length; j++) { // First get all the unique ingredients uniqueIngredients.add(this.getEmptyIngredient(recipes[i].ingredients[j])); } } console.log('unique ingredients', uniqueIngredients); let $this = this; uniqueIngredients.forEach(function (value) { let ingredientCalculation = value as Calculation; for (let i = 0; i < recipes.length; i++) { for (let j = 0; j < recipes[i].ingredients.length; j++) { if (ingredientCalculation.ingredient.definition.name === recipes[i].ingredients[j].definition.name) { // Calculation already exists so update it let calculation: Calculation = $this.calculateIngredient(recipes[i].ingredients[j], recipes[i].quantity); ingredientCalculation.quantityMeasurement += calculation.quantityMeasurement; ingredientCalculation.quantityUnits += calculation.quantityUnits; ingredientCalculation.totalCost += calculation.totalCost; } } } calculations.push(ingredientCalculation); }); return of(calculations); } private getEmptyIngredient(ingredient: Ingredient): Calculation { let calculation: Calculation = { ingredient: ingredient, quantityUnits: 0, quantityMeasurement: 0, totalCost: 0 }; return calculation; }
控制台打印结果显示存在多个相同名称(如Apple)的对象,且属性值不为0。我理解对象相等性的问题,但不明白为何打印时对象已被修改,毕竟计算逻辑在打印之后执行?
解答
1. 为什么打印时对象已经被修改?
这是因为浏览器/Node.js的控制台对引用类型(比如对象)采用延迟求值机制。你调用console.log时,只是把对象的引用传给了控制台,但控制台真正渲染对象内容的时候,后面的循环已经修改了这些对象的属性。
简单说,console.log不会立刻生成对象的当前状态快照,而是保存了对象的引用,当你展开控制台里的对象条目时,才会去读取它的最新值——这时候你的计算逻辑已经执行完毕,所以看到的是修改后的属性。
如果想要打印对象的即时状态,可以在console.log时把对象转成JSON字符串:
console.log('unique ingredients', JSON.stringify([...uniqueIngredients], null, 2));
这样就能看到调用log时对象的真实初始状态了。
2. Set无法去重相同食材的问题
Set判断元素相等是基于引用相等的,你每次调用getEmptyIngredient都会创建一个新的Calculation对象,哪怕它们关联的食材名称相同,也是不同的引用,所以Set会把它们全部存储进去。
修复后的代码示例
calculate(recipes: Recipe[]): Observable<Calculation[]> { // 先按食材名称分组,收集所有相同名称的食材实例 const ingredientMap = new Map<string, Ingredient[]>(); for (const recipe of recipes) { for (const ingredient of recipe.ingredients) { const name = ingredient.definition.name; if (!ingredientMap.has(name)) { ingredientMap.set(name, []); } ingredientMap.get(name)!.push(ingredient); } } // 遍历分组后的食材,创建并计算每个唯一食材的Calculation const calculations: Calculation[] = []; for (const [name, ingredients] of ingredientMap) { // 用第一个食材实例初始化空的Calculation const calculation = this.getEmptyIngredient(ingredients[0]); // 遍历所有该名称的食材,累加计算值 for (const ingredient of ingredients) { // 找到当前食材所属的食谱 const recipe = recipes.find(r => r.ingredients.includes(ingredient))!; const calcResult = this.calculateIngredient(ingredient, recipe.quantity); calculation.quantityMeasurement += calcResult.quantityMeasurement; calculation.quantityUnits += calcResult.quantityUnits; calculation.totalCost += calcResult.totalCost; } calculations.push(calculation); } return of(calculations); } private getEmptyIngredient(ingredient: Ingredient): Calculation { return { ingredient: ingredient, quantityUnits: 0, quantityMeasurement: 0, totalCost: 0 }; }
修复思路说明
- 先用
Map按食材名称分组,确保相同名称的食材被归到同一组,自然实现“唯一食材”的筛选 - 针对每个分组创建一个Calculation对象,再遍历组内所有食材实例,累加计算结果
- 既解决了Set的引用相等问题,也避免了重复创建Calculation对象,逻辑更简洁高效
内容的提问来源于stack exchange,提问作者sterix24
相关产品推荐
相关产品推荐

