object 在 Javascript 中过滤的变异问题
Mutation problem with object filtering in Javascript
几个月前我开始使用 React,直到现在我认为我对突变的理解足够好。
我有一个函数接受 object、选定的 itemId 和 parentId(可选)。数据可以有最多 1 层的嵌套元素。如果 itemId 匹配,我需要做的是更改键 selected
的值。如果 itemId 在 parent 内,那么我需要在 object 内设置 selected
。
另一个用例是,如果 itemId 是 parent 的 ID。在这种情况下,我还需要设置所有 children 的 selected 值。这就是问题所在。
当我试图设置 children 的 selected
值时,该函数正在改变我发送的数据,因此返回错误的结果。
这里是有问题的函数:
function returnSelectedItems(data, itemId, parentId) {
// created this object trying to rule out mutation problem in the tests. @see tests file.
const originalData = data.map(item => item);
if (parentId) {
// if parent id is present, a child element has been selected, then directly go to the element
// go through all the items
return originalData.map(parentItem => {
// see if the id matches the parent item
if (parentItem.id === parentId) {
// if yes, check if it contains any children
if (parentItem.children && parentItem.children.length > 0) {
// iterate through their children and assign the resulting array as the children for the parent
parentItem.children = parentItem.children.map(childItem => {
// find the child with the id selected and change its value
if (childItem.id === itemId) {
childItem.selected = !childItem.selected;
}
return childItem;
});
}
}
return parentItem;
});
} else {
return originalData.map(item => {
if (item.id === itemId) {
// set the item to the opposite it was before
item.selected = !item.selected;
// if it has children, we have to set the values for all of them.
if (item.children && item.children.length > 0) {
item.children = item.children.map(childItem => {
childItem.selected = item.selected;
return childItem;
});
}
}
return item;
});
}
}
下面是我创建的测试,用于测试这是否按预期工作:
test('WITHOUT children test for selectedItems function', () => {
const data = [
{
id: 1,
name: 'test',
selected: false
},
{
id: 2,
name: 'test',
selected: false
},
{
id: 3,
name: 'test',
selected: false
},
{
id: 4,
name: 'test',
selected: false
},
{
id: 5,
name: 'test',
selected: false
}
];
expect(returnSelectedItems(data, 4)).toEqual(
data.map(item => {
if (item.id === 4) {
item.selected = true;
}
return item;
})
);
expect(returnSelectedItems(data, 5)).toEqual(
data.map(item => {
if (item.id === 5) {
item.selected = true;
}
return item;
})
);
expect(
returnSelectedItems(
data.map(item => {
if (item.id === 4) {
item.selected = true;
}
return item;
}, 4)
)
).toEqual(data);
expect(
returnSelectedItems(
data.map(item => {
if (item.id === 5) {
item.selected = true;
}
return item;
}, 5)
)
).toEqual(data);
});
test('WITH children test for selectedItems function', () => {
const data = [
{
id: 1,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 2,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 3,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 4,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 5,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
}
];
const backupData = [
{
id: 1,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 2,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 3,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 4,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 5,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
}
];
// this console log is correct
console.log(data.filter(item => item.id === 4)[0]);
const dataBack = returnSelectedItems(data, 4);
// this console is also correct
console.log(dataBack.filter(item => item.id === 4)[0]);
// but here the value of the data is changed
console.log(data.filter(item => item.id === 4)[0]);
// this passes, but it shouldn't
expect(returnSelectedItems(data, 4)).toEqual(
data.map(item => {
if (item.id === 4) {
item.selected = true;
}
return item;
})
);
// this should pass but it does not
expect(backupData).toEqual(data);
});
感谢您的帮助!
const originalData = data.map(item => item)
仅创建 data
数组的浅表副本。因此,您在 originalData
数组中修改的任何 object
项目都会导致 data
数组发生变化。因此,您必须深度复制 data
数组。为了方便起见,使用像 Lodash 这样的库。
几个月前我开始使用 React,直到现在我认为我对突变的理解足够好。
我有一个函数接受 object、选定的 itemId 和 parentId(可选)。数据可以有最多 1 层的嵌套元素。如果 itemId 匹配,我需要做的是更改键 selected
的值。如果 itemId 在 parent 内,那么我需要在 object 内设置 selected
。
另一个用例是,如果 itemId 是 parent 的 ID。在这种情况下,我还需要设置所有 children 的 selected 值。这就是问题所在。
当我试图设置 children 的 selected
值时,该函数正在改变我发送的数据,因此返回错误的结果。
这里是有问题的函数:
function returnSelectedItems(data, itemId, parentId) {
// created this object trying to rule out mutation problem in the tests. @see tests file.
const originalData = data.map(item => item);
if (parentId) {
// if parent id is present, a child element has been selected, then directly go to the element
// go through all the items
return originalData.map(parentItem => {
// see if the id matches the parent item
if (parentItem.id === parentId) {
// if yes, check if it contains any children
if (parentItem.children && parentItem.children.length > 0) {
// iterate through their children and assign the resulting array as the children for the parent
parentItem.children = parentItem.children.map(childItem => {
// find the child with the id selected and change its value
if (childItem.id === itemId) {
childItem.selected = !childItem.selected;
}
return childItem;
});
}
}
return parentItem;
});
} else {
return originalData.map(item => {
if (item.id === itemId) {
// set the item to the opposite it was before
item.selected = !item.selected;
// if it has children, we have to set the values for all of them.
if (item.children && item.children.length > 0) {
item.children = item.children.map(childItem => {
childItem.selected = item.selected;
return childItem;
});
}
}
return item;
});
}
}
下面是我创建的测试,用于测试这是否按预期工作:
test('WITHOUT children test for selectedItems function', () => {
const data = [
{
id: 1,
name: 'test',
selected: false
},
{
id: 2,
name: 'test',
selected: false
},
{
id: 3,
name: 'test',
selected: false
},
{
id: 4,
name: 'test',
selected: false
},
{
id: 5,
name: 'test',
selected: false
}
];
expect(returnSelectedItems(data, 4)).toEqual(
data.map(item => {
if (item.id === 4) {
item.selected = true;
}
return item;
})
);
expect(returnSelectedItems(data, 5)).toEqual(
data.map(item => {
if (item.id === 5) {
item.selected = true;
}
return item;
})
);
expect(
returnSelectedItems(
data.map(item => {
if (item.id === 4) {
item.selected = true;
}
return item;
}, 4)
)
).toEqual(data);
expect(
returnSelectedItems(
data.map(item => {
if (item.id === 5) {
item.selected = true;
}
return item;
}, 5)
)
).toEqual(data);
});
test('WITH children test for selectedItems function', () => {
const data = [
{
id: 1,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 2,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 3,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 4,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 5,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
}
];
const backupData = [
{
id: 1,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 2,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 3,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 4,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
},
{
id: 5,
name: 'test',
selected: false,
children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
}
];
// this console log is correct
console.log(data.filter(item => item.id === 4)[0]);
const dataBack = returnSelectedItems(data, 4);
// this console is also correct
console.log(dataBack.filter(item => item.id === 4)[0]);
// but here the value of the data is changed
console.log(data.filter(item => item.id === 4)[0]);
// this passes, but it shouldn't
expect(returnSelectedItems(data, 4)).toEqual(
data.map(item => {
if (item.id === 4) {
item.selected = true;
}
return item;
})
);
// this should pass but it does not
expect(backupData).toEqual(data);
});
感谢您的帮助!
const originalData = data.map(item => item)
仅创建 data
数组的浅表副本。因此,您在 originalData
数组中修改的任何 object
项目都会导致 data
数组发生变化。因此,您必须深度复制 data
数组。为了方便起见,使用像 Lodash 这样的库。