无论深度如何,如何删除具有负值的对象属性?

How to remove object properties with negative values, regardless of depth?

我正在寻找一种方法来删除具有负 的对象属性。尽管提供了现有解决方案 here,但它仅适用于无深度对象。我正在寻找一种解决方案来消除 any 深度的负面对象属性。

这需要一个递归的解决方案,我做了一个让我进步的尝试,但仍然不完全。

考虑以下 stocksMarkets 数据。结构故意乱七八糟,以表明我希望无论深度如何都删除负面属性。

const stocksMarkets = {
  tokyo: {
    today: {
      toyota: -1.56,
      sony: -0.89,
      nippon: -0.94,
      mitsubishi: 0.65,
    },
    yearToDate: {
      toyota: -75.95,
      softbank: -49.83,
      canon: 22.9,
    },
  },
  nyc: {
    sp500: {
      ea: 8.5,
      tesla: -66,
    },
    dowJones: {
      visa: 3.14,
      chevron: 2.38,
      intel: -1.18,
      salesforce: -5.88,
    },
  },
  berlin: {
    foo: 2,
  },
  paris: -3,
};

我想要一个函数,让我们称它为 removeNegatives() 它将 return 以下输出:

// pseudo-code
removeNegatives(stocksMarkets)

// {
//   tokyo: {
//     today: {
//       mitsubishi: 0.65,
//     },
//     yearToDate: {
//       canon: 22.9,
//     },
//   },
//   nyc: {
//     sp500: {
//       ea: 8.5,
//     },
//     dowJones: {
//       visa: 3.14,
//       chevron: 2.38,
//     },
//   },
//   berlin: {
//     foo: 2,
//   },
// };

这是我的尝试:

const removeNegatives = (obj) => {
  return Object.entries(obj).reduce((t, [key, value]) => {
    return {
      ...t,
      [key]:
        typeof value !== 'object'
          ? removeNegatives(value)
          : Object.values(value).filter((v) => v >= 0),
    };
  }, {});
};

但它并没有真正让我得到我想要的东西:-/它只在第二个深度级别上工作(见berlin),即便如此,它return是一个只有值而不是完整的 属性(即包括密钥)。

// { tokyo: [], nyc: [], berlin: [ 2 ], paris: {} }

不必那么复杂。只需遍历对象。如果当前 属性 是一个对象,则用该对象再次调用该函数,否则检查 属性 值是否小于 0,如果是,则将其删除。最后 return 更新的对象。

const stocksMarkets={tokyo:{today:{toyota:-1.56,sony:-.89,nippon:-.94,mitsubishi:.65},yearToDate:{toyota:-75.95,softbank:-49.83,canon:22.9}},nyc:{sp500:{ea:8.5,tesla:-66},dowJones:{visa:3.14,chevron:2.38,intel:-1.18,salesforce:-5.88}},berlin:{foo:2},paris:-3};

function remove(obj) {
  for (const prop in obj) {
    if (typeof obj[prop] === 'object') {
      remove(obj[prop]);
      continue;
    }
    if (obj[prop] < 0) delete obj[prop];
  }
  return obj;
}

console.log(remove(stocksMarkets));

注意:这会改变原始对象。如果您想制作该对象的深层副本并使用它,那么最快的方法可能是字符串化,然后解析它。

const copy = JSON.parse(JSON.stringify(stocksMarkets));

或者:在某些 浏览器中有一个全新的功能叫做structuredClone(检查兼容性table)。

const clone = structureClone(stocksMarkets);

使用这个:

const removeNegatives = (obj) => {
  return Object.entries(obj).reduce((t, [key, value]) => {
    const v =
      value && typeof value === "object"
        ? removeNegatives(value)
        : value >= 0
        ? value
        : null;
    if (v!==null) {
      t[key] = v;
    }

    return t;
  }, {});
};

const stocksMarkets = {
  tokyo: {
    today: {
      toyota: -1.56,
      sony: -0.89,
      nippon: -0.94,
      mitsubishi: 0.65,
    },
    yearToDate: {
      toyota: -75.95,
      softbank: -49.83,
      canon: 22.9,
    },
  },
  nyc: {
    sp500: {
      ea: 8.5,
      tesla: -66,
    },
    dowJones: {
      visa: 3.14,
      chevron: 2.38,
      intel: -1.18,
      salesforce: -5.88,
    },
  },
  berlin: {
    foo: 2,
  },
  paris: -3,
};
const positives = removeNegatives(stocksMarkets);

console.log({ positives, stocksMarkets });
.as-console-wrapper { min-height: 100%!important; top: 0; }

您可以获取条目并使用正值或嵌套对象收集它们。

const
    filterBy = fn => {
         const f = object => Object.fromEntries(Object
            .entries(object)
            .reduce((r, [k, v]) => {
                if (v && typeof v === 'object') r.push([k, f(v)]);
                else if (fn(v)) r.push([k, v]);
                return r;
            }, [])
        );
        return f;
    },
    fn = v => v >= 0,
    filter = filterBy(fn),
    stocksMarkets = { tokyo: { today: { toyota: -1.56, sony: -0.89, nippon: -0.94, mitsubishi: 0.65 }, yearToDate: { toyota: -75.95, softbank: -49.83, canon: 22.9 } }, nyc: { sp500: { ea: 8.5, tesla: -66 }, dowJones: { visa: 3.14, chevron: 2.38, intel: -1.18, salesforce: -5.88 } }, berlin: { foo: 2 }, paris: -3 },
    result = filter(stocksMarkets);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

注意: 这会改变对象。如果不可变性是您的用例所关心的问题,您应该首先克隆该对象。

对于递归对象节点操作,最好创建一个允许递归“访问”对象节点的辅助函数,使用提供的访问者函数可以根据业务逻辑自由操作对象节点。一个基本的实现看起来像这样(以打字稿显示,以便清楚发生了什么):

function visit_object(obj: any, visitor: (o: any, k: string )=>any|void ) {
  for (let key in obj) {
    if (typeof obj[key] === 'object') {
      visit_object(obj[key],visitor);
    } else {
      visitor(obj,key);//visit the node object, allowing manipulation.
    }
  }
  // not necessary to return obj; js objects are pass by reference value. 
}

关于您的具体用例,下面演示如何删除值为负的节点。

visit_object( yourObject, (o,k)=>{
  if(o[k]<0){
    delete o[k];
  }
});

编辑:curried 版本以提高性能,包括可选的 deepClone

const deepClone = (inObject) => {
  let outObject, value, key;

  if (typeof inObject !== "object" || inObject === null) {
    return inObject; // Return the value if inObject is not an object
  }

  // Create an array or object to hold the values
  outObject = Array.isArray(inObject) ? [] : {};

  for (key in inObject) {
    value = inObject[key];

    // Recursively (deep) copy for nested objects, including arrays
    outObject[key] = deepClone(value);
  }

  return outObject;
},
visit_object = visitor => ( obj ) => {
  for (let key in obj) {
    if (typeof obj[key] === 'object') {
      visit_object( obj[key], visitor);
    } else {
      visitor(obj,key);//visit the node object, allowing manipulation.
    }
  }
  // not necessary to return obj; js objects are pass by reference value. 
},
filter=(o,k)=>{
  if(o[k]<0){
    delete o[k];
  }
};

Array.prototype.flatMapObject.entriesObject.fromEntries 的组合以及一定量的递归可以使这样的问题变得相当简单:

const removeNegatives = (obj) => Object (obj) === obj
  ? Object .fromEntries (Object .entries (obj) .flatMap (
      ([k, v]) => v < 0 ? [] : [[k, removeNegatives (v)]]
    ))
  : obj

const stockMarkets = {tokyo: {today: {toyota: -1.56, sony: -0.89, nippon: -0.94, mitsubishi: 0.65, }, yearToDate: {toyota: -75.95, softbank: -49.83, canon: 22.9}, }, nyc: {sp500: {ea: 8.5, tesla: -66}, dowJones: {visa: 3.14, chevron: 2.38, intel: -1.18, salesforce: -5.88, }, }, berlin: {foo: 2}, paris: -3}

console .log (removeNegatives (stockMarkets))
.as-console-wrapper {max-height: 100% !important; top: 0}

如果我们的输入不是一个对象,我们只是 return 它原封不动。如果是,我们将它分成 key-value 对,然后对于每一对,如果值为负数,我们跳过它;否则我们会重复该值。然后我们将这些结果 key-value 对拼接回一个对象。

您可能想在 v < 0 之前在 v 上做一个 type-check。由你决定。

不过,这是在乞求多一层抽象。我可能更愿意这样写:

const filterObj = (pred) => (obj) => Object (obj) === obj
  ? Object .fromEntries (Object .entries (obj) .flatMap (
      ([k, v]) => pred (v) ? [[k, filterObj (pred) (v)]] : []
    ))
  : obj

const removeNegatives = filterObj ((v) => typeof v !== 'number' || v > 0)

更新:简单叶过滤

OP 要求一种允许对叶子进行更简单过滤的方法。我知道最简单的方法是通过这样的中间阶段:

[
  [["tokyo", "today", "toyota"], -1.56], 
  [["tokyo", "today", "sony"], -0.89], 
  [["tokyo", "today", "nippon"], -0.94], 
  [["tokyo", "today", "mitsubishi"], 0.65], 
  [["tokyo", "yearToDate", "toyota"], -75.95], 
  [["tokyo", "yearToDate", "softbank"], -49.83], 
  [["tokyo", "yearToDate", "canon"], 22.9], 
  [["nyc", "sp500", "ea"], 8.5], 
  [["nyc", "sp500", "tesla"], -66], 
  [["nyc", "dowJones", "visa"], 3.14], 
  [["nyc", "dowJones", "chevron"], 2.38], 
  [["nyc", "dowJones", "intel"], -1.18], 
  [["nyc", "dowJones", "salesforce"], -5.88], 
  [["berlin", "foo"], 2], 
  [["paris"], -3]
]

然后 运行 我们对这些条目进行简单过滤以获取:

[
  [["tokyo", "today", "mitsubishi"], 0.65], 
  [["tokyo", "yearToDate", "canon"], 22.9], 
  [["nyc", "sp500", "ea"], 8.5], 
  [["nyc", "dowJones", "visa"], 3.14], 
  [["nyc", "dowJones", "chevron"], 2.38], 
  [["berlin", "foo"], 2], 
]

并将其重新组合成一个对象。我有 lying around 功能可以进行提取和再水化,因此只需将它们捆绑在一起即可:

// utility functions
const pathEntries = (obj) =>
  Object (obj) === obj
    ? Object .entries (obj) .flatMap (
        ([k, x]) => pathEntries (x) .map (([p, v]) => [[Array.isArray(obj) ? Number(k) : k, ... p], v])
      ) 
    : [[[], obj]]

const setPath = ([p, ...ps]) => (v) => (o) =>
  p == undefined ? v : Object .assign (
    Array .isArray (o) || Number.isInteger (p) ? [] : {},
    {...o, [p]: setPath (ps) (v) ((o || {}) [p])}
  )

const hydrate = (xs) =>
  xs .reduce ((a, [p, v]) => setPath (p) (v) (a), {})

const filterLeaves = (fn) => (obj) => 
  hydrate (pathEntries (obj) .filter (([k, v]) => fn (v)))


// main function
const removeNegatives = filterLeaves ((v) => v >= 0)


// sample data
const stockMarkets = {tokyo: {today: {toyota: -1.56, sony: -0.89, nippon: -0.94, mitsubishi: 0.65, }, yearToDate: {toyota: -75.95, softbank: -49.83, canon: 22.9}, }, nyc: {sp500: {ea: 8.5, tesla: -66}, dowJones: {visa: 3.14, chevron: 2.38, intel: -1.18, salesforce: -5.88, }, }, berlin: {foo: 2}, paris: -3}


// demo
console .log (removeNegatives (stockMarkets))
.as-console-wrapper {max-height: 100% !important; top: 0}

您可以在 various other answers 中查看 pathEntriessetPathhydrate 的详细信息。这里的重要函数是 filterLeaves,它只接受叶值的谓词 运行s pathEntries,使用该谓词过滤结果并在结果上调用 hydrate。这使得我们的主要功能变得微不足道,filterLeaves ((v) => v > 0).

但是我们可以用这个分解来做各种各样的事情。我们可以根据键和值进行过滤。我们可以在水合之前过滤并映射它们。我们甚至可以映射到多个新的 key-value 结果。这些可能性中的任何一种都像 filterLeaves.

一样简单

基于Object.entries, Array.prototype.reduce和一些基本类型检查的精益非变异递归(树行走)实现...

function cloneStructureButKeepNumberTypeEntriesOfJustPositiveValues(root) {
  return Object
    .entries(root)
    .reduce((node, [key, value]) => {

      // simple but reliable object type test.
      if (value && ('object' === typeof value)) {

        node[key] =
          cloneStructureButKeepNumberTypeEntriesOfJustPositiveValues(value);

      } else  if (
        // either not a number type
        ('number' !== typeof value) ||

        // OR (if number type then)
        // a positive number value.
        (Math.abs(value) === value)
      ) {
        node[key] = value;
      }
      return node;

    }, {});
}

const stocksMarkets = {
  tokyo: {
    today: {
      toyota: -1.56,
      sony: -0.89,
      nippon: -0.94,
      mitsubishi: 0.65,
    },
    yearToDate: {
      toyota: -75.95,
      softbank: -49.83,
      canon: 22.9,
    },
  },
  nyc: {
    sp500: {
      ea: 8.5,
      tesla: -66,
    },
    dowJones: {
      visa: 3.14,
      chevron: 2.38,
      intel: -1.18,
      salesforce: -5.88,
    },
  },
  berlin: {
    foo: 2,
  },
  paris: -3,
};
const rosyOutlooks =
  cloneStructureButKeepNumberTypeEntriesOfJustPositiveValues(stocksMarkets);

console.log({ rosyOutlooks, stocksMarkets });
.as-console-wrapper { min-height: 100%!important; top: 0; }

由于一直在讨论性能问题...许多开发人员仍然低估了 JIT 编译器对函数的性能提升 statements/declarations。

我觉得可以免费使用 r3wt 的 性能测试参考,我可以说的是,两个带有 corecursion 的函数语句在 chrome/mac 环境中表现最好...

function corecursivelyAggregateEntryByTypeAndValue(node, [key, value]) {
  // simple but reliable object type test.
  if (value && ('object' === typeof value)) {

    node[key] =
      cloneStructureButKeepNumberTypeEntriesOfJustPositiveValues(value);

  } else  if (
    // either not a number type
    ('number' !== typeof value) ||

    // OR (if number type then)
    // a positive number value.
    (Math.abs(value) === value)
  ) {
    node[key] = value;
  }
  return node;
}
function cloneStructureButKeepNumberTypeEntriesOfJustPositiveValues(root) {
  return Object
    .entries(root)
    .reduce(corecursivelyAggregateEntryByTypeAndValue, {});
}

const stocksMarkets = {
  tokyo: {
    today: {
      toyota: -1.56,
      sony: -0.89,
      nippon: -0.94,
      mitsubishi: 0.65,
    },
    yearToDate: {
      toyota: -75.95,
      softbank: -49.83,
      canon: 22.9,
    },
  },
  nyc: {
    sp500: {
      ea: 8.5,
      tesla: -66,
    },
    dowJones: {
      visa: 3.14,
      chevron: 2.38,
      intel: -1.18,
      salesforce: -5.88,
    },
  },
  berlin: {
    foo: 2,
  },
  paris: -3,
};
const rosyOutlooks =
  cloneStructureButKeepNumberTypeEntriesOfJustPositiveValues(stocksMarkets);

console.log({ rosyOutlooks, stocksMarkets });
.as-console-wrapper { min-height: 100%!important; top: 0; }

Edit ... 重构上述基于 corecursion 的实现,以涵盖最新 2 条评论提到的 2 个开放点...

"OP requested in the comments the ability to filter by a predicate function, which your answer doesn't do, but nonetheless i added it to the bench [...]" – r3wt

"Nice (although you know I personally prefer terse names!) I wonder, why did you choose Math.abs(value) === value over value >= 0?" – Scott Sauyet

// The implementation of a generic approach gets covered
// by two corecursively working function statements.
function corecursivelyAggregateEntryByCustomCondition(
  { condition, node }, [key, value],
) {
  if (value && ('object' === typeof value)) {

    node[key] =
      copyStructureWithConditionFulfillingEntriesOnly(value, condition);

  } else if (condition(value)) {

    node[key] = value;
  }
  return { condition, node };
}
function copyStructureWithConditionFulfillingEntriesOnly(
  root, condition,
) {
  return Object
    .entries(root)
    .reduce(
      corecursivelyAggregateEntryByCustomCondition,
      { condition, node: {} },
    )
    .node;
}

// the condition ... a custom predicate function.
function isNeitherNumberTypeNorNegativeValue(value) {
  return (
    'number' !== typeof value ||
    0 <= value
  );
}

// the data to work upon.
const stocksMarkets = {
  tokyo: {
    today: {
      toyota: -1.56,
      sony: -0.89,
      nippon: -0.94,
      mitsubishi: 0.65,
    },
    yearToDate: {
      toyota: -75.95,
      softbank: -49.83,
      canon: 22.9,
    },
  },
  nyc: {
    sp500: {
      ea: 8.5,
      tesla: -66,
    },
    dowJones: {
      visa: 3.14,
      chevron: 2.38,
      intel: -1.18,
      salesforce: -5.88,
    },
  },
  berlin: {
    foo: 2,
  },
  paris: -3,
};

// object creation.
const rosyOutlooks =
  copyStructureWithConditionFulfillingEntriesOnly(
    stocksMarkets,
    isNeitherNumberTypeNorNegativeValue,
  );

console.log({ rosyOutlooks, stocksMarkets });
.as-console-wrapper { min-height: 100%!important; top: 0; }