【问题标题】:Remove all empty strings, empty objects, and empty arrays from JS object从 JS 对象中删除所有空字符串、空对象和空数组
【发布时间】:2021-01-04 21:04:31
【问题描述】:

这是我的对象:

{
   "name":"fff",
   "onlineConsultation":false,
  
   "image":"",
   "primaryLocation":{
      "locationName":"ggg",
      "street":"",
   
   },
  
   "billingAndInsurance":[
      
   ],
  
   "categories":[
      ""
   ],
   "concernsTreated":[
      ""
   ],
   "education":[
      {
         "nameOfInstitution":"ffff",
         "description":"fff",
        
      }
   ],
   "experience":[
      {
         "from":"",
         "current":"",
        
      }
   ],
}

递归删除所有空对象和空数组的算法是什么? 这是我的代码:

function rm(obj) {
  for (let k in obj) {
    const s = JSON.stringify(obj[k]);

    if (s === '""' || s === "[]" || s === "{}") {
      delete obj[k];
    }
    if (Array.isArray(obj[k])) {
      obj[k] = obj[k].filter((x) => {
        const s = JSON.stringify(obj[x]);
        return s !== '""' && s !== "[]" && s !== "{}";
      });
      obj[k] = obj[k].map(x=>{
        return rm(x)
      })
      
    }
  }
  return obj
}

我尝试了多种算法,但都没有奏效。上面的应该更完整一点。但是我已经用尽了我所有的资源来让它工作

【问题讨论】:

  • 您要删除所有空白的属性吗?甚至那些在数组中的?
  • "删除所有空对象和空数组":您的代码似乎也想删除空字符串。你能澄清一下吗?

标签: javascript algorithm recursion


【解决方案1】:

保留有用的功能的一个好处是,您通常可以非常简单地解决您的新需求。使用我多年来编写的一些库函数,我能够编写这个版本:

const removeEmpties = (input) =>
  pathEntries (input)
    .filter (([k, v]) => v !== '')
    .reduce ((a, [k, v]) => assocPath (k, v, a), {})

这使用了我周围的两个函数,pathEntriesassocPath,我将在下面给出它们的实现。给定您提供的输入时,它会返回以下内容:

{
    name: "fff",
    onlineConsultation: false,
    primaryLocation: {
        locationName: "ggg"
    },
    education: [
        {
            nameOfInstitution: "ffff",
            description: "fff"
        }
    ]
}

这将删除空字符串、没有值的数组(在删除空字符串之后)和没有非空值的对象。

我们首先调用pathEntries(我在这里的其他答案中使用过它,包括a fairly recent one。)这会收集输入对象中所有叶节点的路径以及这些叶节点的值。路径存储为字符串数组(对于对象)或数字(对于数组)。它们嵌入到带有值的数组中。所以在这一步之后我们会得到类似的东西

[
  [["name"], "fff"],
  [["onlineConsultation"], false],
  [["image"], ""],
  [["primaryLocation", "locationName"], "ggg"],
  [["primaryLocation", "street"], ""],
  [["categories", 0], ""], 
  [["concernsTreated", 0], ""], 
  [["education", 0, "nameOfInstitution"], "ffff"],
  [["education", 0, "description"],"fff"],
  [["experience", 0, "from"], ""],
  [["experience", 0, "current"], ""]
]

这应该类似于 Object.entries 的对象结果,除了键不是属性名称而是整个路径。

接下来我们过滤掉任何一个空字符串值,产生:

[
  [["name"], "fff"],
  [["onlineConsultation"], false],
  [["primaryLocation", "locationName"], "ggg"],
  [["education", 0, "nameOfInstitution"], "ffff"],
  [["education", 0, "description"],"fff"],
]

然后通过在这个列表和一个空对象上减少对assocPath(另一个我用过很多次的函数,包括在a very interesting question中)的调用,我们将一个完整的对象与正确的叶节点结合起来路径,我们得到了我们正在寻找的答案。 assocPath 是另一个函数assoc 的扩展,它将属性名称与对象中的值不变地关联起来。虽然不是这么简单,但由于要处理数组和对象,您可以认为 assoc 就像 (name, val, obj) => ({...obj, [name]: val}) assocPath 对对象路径而不是属性名称做了类似的事情。

关键是我只为此编写了一个新函数,并且使用了我周围的东西。

通常我更愿意为此编写一个递归函数,并且我did so recently 解决了类似的问题。但这并不容易扩展到这个问题,如果我理解正确,我们想在数组中排除一个空字符串,然后,如果该数组本身现在是空的,也排除它。这种技术使这很简单。在下面的实现中,我们将看到pathEntries 依赖于递归函数,而assocPath 本身就是递归的,所以我想递归还在进行中!

我还应该注意assocPathpathEntries 中使用的path 函数的灵感来自Ramda(免责声明:我是它的作者之一。)我在@ 987654325@,只有在它工作之后,我才将它移植到 vanilla JS,使用我为之前的问题创建的依赖项版本。所以虽然下面的sn-p里面有很多函数,但是写起来还是挺快的。

const path = (ps = []) => (obj = {}) =>
  ps .reduce ((o, p) => (o || {}) [p], obj)

const assoc = (prop, val, obj) => 
  Number .isInteger (prop) && Array .isArray (obj)
    ? [... obj .slice (0, prop), val, ...obj .slice (prop + 1)]
    : {...obj, [prop]: val}

const assocPath = ([p = undefined, ...ps], val, obj) => 
  p == undefined
    ? obj
    : ps.length == 0
      ? assoc(p, val, obj)
      : assoc(p, assocPath(ps, val, obj[p] || (obj[p] = Number .isInteger (ps[0]) ? [] : {})), obj)

const getPaths = (obj) =>
  Object (obj) === obj
    ? Object .entries (obj) .flatMap (
        ([k, v]) => getPaths (v) .map (p => [Array.isArray(obj) ? Number(k) : k, ... p])
      )
    : [[]]

const pathEntries = (obj) => 
  getPaths (obj) .map (p => [p, path (p) (obj)])

const removeEmpties = (input) =>
  pathEntries (input)
    .filter (([k, v]) => v !== '')
    .reduce ((a, [k, v]) => assocPath (k, v, a), {})

const input = {name: "fff", onlineConsultation: false, image: "", primaryLocation: {locationName: "ggg", street:""}, billingAndInsurance: [], categories: [""], concernsTreated: [""], education: [{nameOfInstitution: "ffff", description: "fff"}], experience: [{from: "", current:""}]}

console .log(removeEmpties (input))

在某些时候,我可能会选择更进一步。我看到一个 hydrate 函数要被拉出:

const hydrate = (entries) =>
  entries .reduce ((a, [k, v]) => assocPath2(k, v, a), {})

const removeEmpties = (input) =>
  hydrate (pathEntries (input) .filter (([k, v]) => v !== ''))

而且我还可以看到这样写的更像 Ramda 风格:

const hydrate = reduce ((a, [k, v]) => assocPath(k, v, a), {})

const removeEmpties = pipe (pathEntries, filter(valueNotEmpty), hydrate)

使用适当版本的valuesNotEmpty

但这一切都是为了另一天。

【讨论】:

    【解决方案2】:

    这是一个有趣的问题。我认为如果我们编写一个适用于数组对象的通用mapfilter函数,它可以很好地解决-

    const map = (t, f) =>
      isArray(t)
        ? t.map(f)
    : isObject(t)
        ? Object.fromEntries(Object.entries(t).map(([k, v]) =>  [k, f(v, k)]))
    : t
    
    const filter = (t, f) =>
      isArray(t)
        ? t.filter(f)
    : isObject(t)
        ? Object.fromEntries(Object.entries(t).filter(([k, v]) =>  f(v, k)))
    : t
    

    我们现在可以轻松编写您的removeEmpties 程序 -

    1. 如果输入 t 是一个对象,则递归映射它并保留非空值
    2. (归纳)t 不是对象。如果t 是非空值,则返回t
    3. (归纳)t 不是对象,t 是空值。返回empty哨兵
    const empty =
      Symbol()
    
    const removeEmpties = (t = {}) =>
      isObject(t)
        ? filter(map(t, removeEmpties), nonEmpty) // 1
    : nonEmpty(t)
        ? t                                       // 2
    : empty                                       // 3
    

    现在我们必须定义nonEmpty的含义-

    const nonEmpty = t =>
      isArray(t)
        ? t.length > 0
    : isObject(t)
        ? Object.keys(t).length > 0
    : isString(t)
        ? t.length > 0
    : t !== empty   // <- all other t are OK, except for sentinel
    

    到目前为止,我们已经使用is* 函数进行动态类型检查。我们现在将定义它们 -

    const isArray = t => Array.isArray(t)
    const isObject = t => Object(t) === t
    const isString = t => String(t) === t
    const isNumber = t => Number(t) === t
    const isMyType = t => // As many types as you want 
    

    最后我们可以计算出resultinput -

    const input =
      {name:"fff",zero:0,onlineConsultation:false,image:"",primaryLocation:{locationName:"ggg",street:""},billingAndInsurance:[],categories:[""],concernsTreated:[""],education:[{nameOfInstitution:"ffff",description:"fff"}],experience:[{from:"",current:""}]}
    
    const result =
      removeEmpties(input)
    
    console.log(JSON.stringify(result, null, 2))
    
    {
      "name": "fff",
      "zero": 0,
      "onlineConsultation": false,
      "primaryLocation": {
        "locationName": "ggg"
      },
      "education": [
        {
          "nameOfInstitution": "ffff",
          "description": "fff"
        }
      ]
    }
    

    展开下面的程序,在浏览器中验证结果-

    const map = (t, f) =>
      isArray(t)
        ? t.map(f)
    : isObject(t)
        ? Object.fromEntries(Object.entries(t).map(([k, v]) =>  [k, f(v, k)]))
    : t
    
    const filter = (t, f) =>
      isArray(t)
        ? t.filter(f)
    : isObject(t)
        ? Object.fromEntries(Object.entries(t).filter(([k, v]) =>  f(v, k)))
    : t
    
    const empty =
      Symbol()
    
    const removeEmpties = (t = {}) =>
      isObject(t)
        ? filter(map(t, removeEmpties), nonEmpty)
    : nonEmpty(t)
        ? t
    : empty
    
    const isArray = t => Array.isArray(t)
    const isObject = t => Object(t) === t
    const isString = t => String(t) === t
    
    const nonEmpty = t =>
      isArray(t)
        ? t.length > 0
    : isObject(t)
        ? Object.keys(t).length > 0
    : isString(t)
        ? t.length > 0
    : t !== empty
    
    const input =
      {name:"fff",zero:0,onlineConsultation:false,image:"",primaryLocation:{locationName:"ggg",street:""},billingAndInsurance:[],categories:[""],concernsTreated:[""],education:[{nameOfInstitution:"ffff",description:"fff"}],experience:[{from:"",current:""}]}
    
    const result =
      removeEmpties(input)
    
    console.log(JSON.stringify(result, null, 2))

    【讨论】:

    • 比我的简单多了。我不知道为什么我没有看到这一点,因为我通常使用 mapfilter 以这种方式工作的函数。
    • 我现在只是在阅读您的答案,我对为什么首先发生 filter 感到有些困惑,但现在它是有道理的。它可能更复杂,但树的扁平化和重建仍然是一个可以想象的巧妙计算过程。
    • 是的,我一直在寻找在 Object.entriesObject.fromEntries 之间嵌套函数 :: [(k, v)] -&gt; [(k, v)] 的各种用途。我认为在pathEntrieshydrate 之间嵌套:: [(p, v)] -&gt; [(p, v)] 也有类似的好处。我确定我会进一步调查。
    【解决方案3】:

    function removeEmpty(obj){
        if(obj.__proto__.constructor.name==="Array"){
                obj = obj.filter(e=>e.length)
                return obj.map((ele,i)=>{
                if(obj.__proto__.constructor.name==="Object")return removeEmpty(ele) /* calling the same function*/
                else return ele
            })
        }
    
       if(obj.__proto__.constructor.name==="Object")for(let key in obj){
            switch(obj[key].__proto__.constructor.name){
                case "String":
                                if(obj[key].length===0)delete obj[key]
                                break;
                case "Array":
                                obj[key] = removeEmpty(obj[key]) /* calling the same function*/
                                if(! obj[key].length)delete obj[key]
                                break;
                case "Object":
                                obj[key] = removeEmpty(obj[key]) /* calling the same function*/
                                break;
            }
        }
        return obj;
    }
    
    const input = {name: "fff", onlineConsultation: false, image: "", primaryLocation: {locationName: "ggg", street:""}, billingAndInsurance: [], categories: [""], concernsTreated: [""], education: [{nameOfInstitution: "ffff", description: "fff"}], experience: [{from: "", current:""}]}
    
    console .log(removeEmpty(input))

    【讨论】:

    • education: [{…}] 不是空的,所以我想它不应该被删除。
    【解决方案4】:
    function dropEmptyElements(object) {
        switch (typeof object) {
            case "object":
                const keys = Object.keys(object || {}).filter(key => {
                    const value = dropEmptyElements(object[key]);
                    if(value === undefined) {
                        delete object[key];
                    }
                    return value !== undefined;
                });
                return keys.length === 0 ? undefined : object;
    
            case "string":
                return object.length > 0 ? object : undefined;
    
            default:
                return object;
        }
    }
    

    这应该对你有用;)

    【讨论】:

    • 您没有考虑到objectnull 的情况。 JS 报错
    • 欢迎来到 SO。这被认为是低质量的答案,因为它没有对提问者的原始问题发表任何评论,也没有对所提供的解决方案进行解释。这会导致复制/粘贴开发,并且是许多个人程序员和开发团队都感到头疼的根源。考虑解释您的思考过程或解决 OP 代码失败的原因,我会急切地删除我的反对票。
    猜你喜欢
    • 2020-02-01
    • 1970-01-01
    • 2016-11-26
    • 2014-07-09
    • 1970-01-01
    • 2016-02-26
    • 2015-12-24
    • 1970-01-01
    • 2019-09-15
    相关资源
    最近更新 更多