oxc/no-map-spread 性能
它的作用
禁止在 Array.prototype.map 与 Array.prototype.flatMap 中使用对象或数组的展开语法,以向数组项添加属性/元素。
此规则仅关注展开运算符用于合并对象或数组的情况,而非用于复制它们的情形。
为什么这是个问题?
展开操作常被用来向数组中的对象添加属性,或将多个对象合并在一起。然而,展开操作会引发新对象的重新分配,并伴随 O(n) 的内存拷贝开销。
// scores 中的每个对象都会被浅拷贝。由于 scores 永远不会被重用,展开操作效率低下。
function getDisplayData() {
const scores: Array<{ username: string; score: number }> = getScores();
const displayData = scores.map((score) => ({ ...score, rank: getRank(score) }));
return displayData;
}除非你预期映射后的数组中的对象后续会被修改,否则使用 Object.assign 更为合适。
// score 在原地被修改,性能更优。
function getDisplayData() {
const scores: Array<{ username: string; score: number }> = getScores();
const displayData = scores.map((score) => Object.assign(score, { rank: getRank(score) }));
return displayData;
}防止意外修改
在 map 调用中展开对象确实存在合理的使用场景,特别是当你希望返回数组的调用者能够自由修改这些数据而不影响原始数据时。此规则会尽力避免报告这类情况。
对类实例属性的展开操作将完全被忽略:
class AuthorsDb {
#authors = [];
public getAuthorsWithBooks() {
return this.#authors.map((author) => ({
// 保护原始数据免受修改,使调用者获得作者对象的独立(深度)副本。
...author,
books: getBooks(author),
}));
}
}默认情况下,对 map 调用后再次读取的数组展开也会被忽略。可通过 ignoreRereads 选项配置此行为。
/* "oxc/no-map-spread": ["error", { "ignoreRereads": true }] */
const scores = getScores();
const displayData = scores.map(score => ({ ...score, rank: getRank(score) }));
console.log(scores); // map 调用后再次读取 scores数组
对于数组展开的情况,应尽可能使用 Array.prototype.concat 或 Array.prototype.push。它们与数组展开具有略微不同的语义:展开作用于可迭代对象,而 concat 与 push 仅作用于数组。
let arr = [1, 2, 3];
let set = new Set([4]);
let a = [...arr, ...set]; // [1, 2, 3, 4]
let b = arr.concat(set); // [1, 2, 3, Set(1)]
// 比展开更高效但语义相同的一种替代方案。不幸的是,它更冗长。
let c = arr.concat(Array.from(set)); // [1, 2, 3, 4]
// 你也可以使用 `Symbol.isConcatSpreadable`
set[Symbol.isConcatSpreadable] = true;
let d = arr.concat(set); // [1, 2, 3, 4]自动修复
此规则可自动修复由对象展开引起的违规,但不会修复数组。未来可能会支持数组的修复。
仅包含单个展开元素的对象表达式不会被修复。
arr.map((x) => ({ ...x })); // 不会修复对于展开前有“正常”元素的对象,提供了一个 fix(使用 --fix)。由于 Object.assign 会修改第一个参数,且新对象将包含这些元素,因此展开标识符不会被修改。实际上,保留了展开的语义。
// 修复前
arr.map(({ x, y }) => ({ x, ...y }));
// 修复后
arr.map(({ x, y }) => Object.assign({ x }, y));当展开是对象的第一个属性时,会提供一个建议(使用 --fix-suggestions)。此修复会修改展开的标识符,可能导致未预期的副作用。
// 修复前
arr.map(({ x, y }) => ({ ...x, y }));
arr.map(({ x, y }) => ({ ...x, y }));
// 修复后
arr.map(({ x, y }) => Object.assign(x, { y }));
arr.map(({ x, y }) => Object.assign(x, y));示例
此规则的 错误 代码示例:
const arr = [{ a: 1 }, { a: 2 }, { a: 3 }];
const arr2 = arr.map((obj) => ({ ...obj, b: obj.a * 2 }));此规则的 正确 代码示例:
const arr = [{ a: 1 }, { a: 2 }, { a: 3 }];
arr.map((obj) => Object.assign(obj, { b: obj.a * 2 }));
// 实例属性被忽略
class UsersDb {
#users = [];
public get users() {
// 复制用户,为调用者提供其自身的深度(近似)副本。
return this.#users.map((user) => ({ ...user }));
}
}function UsersTable({ users }) {
const usersWithRoles = users.map((user) => ({ ...user, role: getRole(user) }));
return (
<table>
{usersWithRoles.map((user) => (
<tr>
<td>{user.name}</td>
<td>{user.role}</td>
</tr>
))}
<tfoot>
<tr>
{/* 再次读取 users */}
<td>总用户数: {users.length}</td>
</tr>
</tfoot>
</table>
);
}参考资料
配置
此规则接受一个配置对象,包含以下属性:
ignoreArgs
type: boolean
default: true
忽略作为函数参数传入的数组上的 map 操作。
此选项默认启用,以更好地避免误报。但代价是可能遗漏一些低效的展开操作。我们建议在 .oxlintrc.json 文件中将其关闭。
示例
当 ignoreArgs 为 true 时,此规则的 错误 代码示例:
/* "oxc/no-map-spread": ["error", { "ignoreArgs": true }] */
function foo(arr) {
let arr2 = arr.filter((x) => x.a > 0);
return arr2.map((x) => ({ ...x }));
}当 ignoreArgs 为 true 时,此规则的 正确 代码示例:
/* "oxc/no-map-spread": ["error", { "ignoreArgs": true }] */
function foo(arr) {
return arr.map((x) => ({ ...x }));
}ignoreRereads
type: boolean
default: true
忽略在 map 调用后被再次读取的映射数组。
被重复使用的数组可能依赖于浅拷贝行为来防止修改。在这种情况下,Object.assign 并不比展开操作更具性能优势。
如何使用
要通过配置文件或 CLI 启用此规则,可以使用:
{
"rules": {
"oxc/no-map-spread": "error"
}
}oxlint --deny oxc/no-map-spread