如何有选择地从深度嵌套数据结构中复制属性?

How to selectively copy properties from a deep nested data structure?

提问人:user21357723 提问时间:10/26/2023 最后编辑:Peter Seligeruser21357723 更新时间:11/2/2023 访问量:168

问:

我想从现有对象复制一个对象。

但我只需要下面我想要的字段

由于需要白名单方法,我不能简单地复制整个对象,然后使用删除方法来删除不需要的字段

所以,我现在的方法如下

const copiedData = {
    productId: data.productId,
    time: data.time,
    lastModifiedDate: data.lastModifiedDate
    plan: {
        planDetails: optionallySelProperties({
            data: data.plan.planDetails,
            dependencies: planDetailsDependencies
        }), //--XXXXXXXXXXXXXXXX problem comes in
         
        analysis: optionallySelProperties({
            data: data.analysis,
            dependencies: ......})
        products: .......,
        }
}

如果他们的属性,我需要的是一个对象。我会用一个支持选择属性的函数来包装它。

const planDetailsDependencies = [
  'planName',
  'status',
  'customproductAllocations',
]


const optionallySelProperties = ({ data, dependencies }: IOptionallySelProperties) => {
  return Object.entries(pick(data, dependencies) || {}).reduce((ret, cur) => {
    return {
      ...ret,
      [cur[0]]: cur[1],
    }
  }, {})
}

PS:pick 是 lodash 函数

rn,如果传递给 optionallySelProperties 的数据包含嵌套对象,并且我还需要选择属性。我无法在我的职能中做到这一点。

有没有办法实现这一目标?

这是我想复制的数据

const data = {
  "abcId": "b27e21f7",
  "productId": "G0221837387", //----- field I want to take 
  "time": 1698303900879, //----- field I want to take
  "member": { //----- field I want to take
    "teamMembers": [{
      "roles": [],
      "type": "Team Member",
      "name": "Me",
    }],
  },
  "plan": { //----- field I want to take
    "id": 86, //----- field I want to take
    "lastModifiedDate": "2023-10-25T01:37:58.648146", //----- field I want to take
    "planDetails": { //----- field I want to take
      "planName": "20230202",
      "options": [{
        "value": 1,
        "text": "Pineapple",
      }],
      "status": "DRAFT", //----- field I want to take
      "customproductAllocations": [{ //----- field I want to take
        "id": 24744,
        "allocationDetail": { //----- field I want to take 
          "name": "Created on 17 August 2023",
          "dollar": "USD", //----- field I want to take
          "allocations": [{
            "id": "1005",
            "name": "chocolatePreferred", //----- field I want to take
          }, {
            "id": "1007",
            "name": "chocolate Large Cap", //----- field I want to take
          }],
        }],
      },
      "products": { //----- field I want to take
        "inital": 169000000, //----- field I want to take
        "externalproducts": [{ //----- field I want to take
          "id": 659,
          "name": "Additional", //----- field I want to take
        }],
        "productAllocation": { //----- field I want to take
          "productAllocation": [{
            "id": "1005",
            "category": "Card", //----- field I want to take     
          }, {
            "id": "1007",
            "category": "Fruit", //----- field I want to take
          }],
        },
      },
      "analysis": { //----- field I want to take
        "analysisA": { //----- field I want to take
          "id": 50443,
          "key": "Liq", //----- field I want to take
        },
        "analysisB": { //----- field I want to take
          "id": 50443,
          "key": "dity", //----- field I want to take
        },
      },
    },
  },
};
JavaScript 数据结构 嵌套 克隆 部分

评论

1赞 Adam Jenkins 10/26/2023
看起来除了嵌套对象的一些 ID 之外,您几乎想要所有内容。我的建议是,不要试图聪明地使用一些递归的难以阅读的函数,而只是创建一些辅助函数来完成克隆某些对象和删除 id 属性的工作——即先以愚蠢的方式去做
0赞 Peter Seliger 11/7/2023
@user21357723......你还在参与这个话题吗?
0赞 Peter Seliger 11/20/2023
@user21357723......你还在参与这个话题吗?
0赞 Peter Seliger 11/30/2023
@user21357723......你还在参与这个话题吗?至少我很好奇哪种方法/解决方案最能解决您的问题。只是在没有反馈的情况下消失,既不投票也不接受答案,这并不是那么礼貌。

答:

0赞 Danny 10/26/2023 #1

您可以使用另一个对象定义所需结果的形状。
并使用该对象来构建新对象。 例如

const data = {
  required1: 'data',
  required2: true,
  notRequired1: 'value',
  required3: 9,
  required4: {
    notRequiredNested1: 3,
    requiredNested1: [
      {
        required1: '___',
        notRequired1: {}
      },
      {
        required1: 'string',
        notRequired1: {}
      }
    ]
  }
}

const requiredKeys = {
  required1: undefined,
  required2: undefined,
  required3: undefined,
  required4: {
    requiredNested1: [
      {
        required1: undefined
      }
    ]
  },
}

const clonedData = copyRequiredProperties(data, requiredKeys)

console.log('Data', data)
console.log('Cloned Data', clonedData)

function copyRequiredProperties(obj, requiredKeys) {
  const clonedObj = {}

  for (const [key, value] of Object.entries(requiredKeys)) {
    if (value === undefined) {
      clonedObj[key] = obj[key]

      continue
    }

    if (Array.isArray(value)) {
      clonedObj[key] = []

      if (typeof value[0] === 'object') {
        for (const item of obj[key]) {
          const requiredKeysOfArrayItems = value[0]
          const clonedItem = copyRequiredProperties(item, requiredKeysOfArrayItems)
          clonedObj[key].push(clonedItem)
        }
      }
      else {
        for (const item of obj[key]) {
          clonedObj[key].push(item)
        }
      }

      continue
    }

    if (typeof value === 'object') {
      const requiredKeysOfNestedObject = value
      clonedObj[key] = copyRequiredProperties(obj[key], requiredKeysOfNestedObject)

      continue
    }
  }

  return clonedObj
}

-1赞 Peter Seliger 10/27/2023 #2

OP 问题的明显解决方案是实现一个函数,该函数递归克隆任何提供的数据结构,但并不完全,因为存在异常。

注意 ...OP 的请求最好用 ignore-list 来描述,而不是 required-list。

除了它的第一个参数(即要克隆的值/数据)之外,此函数还接受一个 Set 实例,该实例具有应该不克隆的属性的键名(因此是一个忽略列表)。

第一个粗略的解决方案,虽然它没有完全涵盖 OP 精确定位特定键路径的用例,但可以使用这样的键忽略列表,并且看起来像下面......

function cloneDataAndIgnoreKeys(
  dataSource, ignoredKeys = new Set, dataTarget = {}
) {
  if (Array.isArray(dataSource)) {

    dataTarget = dataSource
      .map(item =>
        cloneDataAndIgnoreKeys(item, ignoredKeys)
      );
  } else if (!!dataSource && (typeof dataSource === 'object')) {

    dataTarget = Object
      .entries(dataSource)
      .reduce((target, [key, value]) => {

        if (!ignoredKeys.has(key)) {
          target[key] =
            cloneDataAndIgnoreKeys(value, ignoredKeys);
        }
        return target;

      }, dataTarget);

  } else {
    dataTarget = dataSource;
  }
  return dataTarget;
}

const sampleData = {
  "abcId": "b27e21f7",
  "productId": "G0221837387", //----- field I want to take 
  "time": 1698303900879, //----- field I want to take
  "member": { //----- field I want to take
    "teamMembers": [{
      "roles": [],
      "type": "Team Member",
      "name": "Me",
    }],
  },
  "plan": { //----- field I want to take
    "id": 86, //----- field I want to take
    "lastModifiedDate": "2023-10-25T01:37:58.648146", //----- field I want to take
    "planDetails": { //----- field I want to take
      "planName": "20230202",
      "options": [{
        "value": 1,
        "text": "Pineapple",
      }],
      "status": "DRAFT", //----- field I want to take
      "customproductAllocations": [{ //----- field I want to take
        "id": 24744,
        "allocationDetail": { //----- field I want to take 
          "name": "Created on 17 August 2023",
          "dollar": "USD", //----- field I want to take
          "allocations": [{
            "id": "1005",
            "name": "chocolatePreferred", //----- field I want to take
          }, {
            "id": "1007",
            "name": "chocolate Large Cap", //----- field I want to take
          }],
        }
      }],
      "products": { //----- field I want to take
        "inital": 169000000, //----- field I want to take
        "externalproducts": [{ //----- field I want to take
          "id": 659,
          "name": "Additional", //----- field I want to take
        }],
        "productAllocation": { //----- field I want to take
          "productAllocation": [{
            "id": "1005",
            "category": "Card", //----- field I want to take     
          }, {
            "id": "1007",
            "category": "Fruit", //----- field I want to take
          }],
        },
      },
      "analysis": { //----- field I want to take
        "analysisA": { //----- field I want to take
          "id": 50443,
          "key": "Liq", //----- field I want to take
        },
        "analysisB": { //----- field I want to take
          "id": 50443,
          "key": "dity", //----- field I want to take
        },
      },
    },
  },
};

console.log(
  cloneDataAndIgnoreKeys(
    sampleData,
    new Set(['abcId', 'id']),
  )
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

上述第一种方法可以进行改进,以完全涵盖 OP 的特定用例。

该实例现在将具有值,而不是不够精确的属性名称,而递归函数将聚合并将要做出忽略决策的当前范围传递给自己。Setkeypathkeypath

基于的方法还允许符号......keypath

  • 两者都可以以路径中的任何数组项为目标而不管其确切的数组索引如何

    'planDetails.customproductAllocations[n].allocationDetail.allocations[n].id'
    
  • 或者精确地定位数据结构的特定成员。

    'planDetails.customproductAllocations[0].allocationDetail.allocations[3].id'
    

function cloneDataAndIgnoreKeypaths(
  dataSource, ignoredKeypaths = new Set, keypath = '', dataTarget = {},
) {
  if (Array.isArray(dataSource)) {

    dataTarget = dataSource
      .map((item, idx) =>

        cloneDataAndIgnoreKeypaths(
          item, ignoredKeypaths, `${ keypath }[${ idx }]`,
        )
      );
  } else if (!!dataSource && (typeof dataSource === 'object')) {

    dataTarget = Object
      .entries(dataSource)
      .reduce((target, [key, value]) => {

        const currentKeypath = (

          // - handling the root-path case.
          (!keypath && key) ||

          // - handling concatenation to an object member.
          `${ keypath }.${ key }`
        );
        const generalizedArrayItemPath = currentKeypath
          .replace((/\[\d+\]/g), '[n]')

        // look for both matches ...
        if (
          // - the exact match of a keypath,
          !ignoredKeypaths.has(currentKeypath) &&
          // - the match of any array-item within the
          //   path regardless of its exact array index.
          !ignoredKeypaths.has(generalizedArrayItemPath)
        ) {
          target[key] =

            cloneDataAndIgnoreKeypaths(
              value, ignoredKeypaths, currentKeypath,
            )
        }
        return target;

      }, dataTarget);

  } else {
    dataTarget = dataSource;
  }
  return dataTarget;
}

const sampleData = {
  "abcId": "b27e21f7",
  "productId": "G0221837387", //----- field I want to take 
  "time": 1698303900879, //----- field I want to take
  "member": { //----- field I want to take
    "teamMembers": [{
      "roles": [],
      "type": "Team Member",
      "name": "Me",
    }],
  },
  "plan": { //----- field I want to take
    "id": 86, //----- field I want to take
    "lastModifiedDate": "2023-10-25T01:37:58.648146", //----- field I want to take
    "planDetails": { //----- field I want to take
      "planName": "20230202",
      "options": [{
        "value": 1,
        "text": "Pineapple",
      }],
      "status": "DRAFT", //----- field I want to take
      "customproductAllocations": [{ //----- field I want to take
        "id": 24744,
        "allocationDetail": { //----- field I want to take 
          "name": "Created on 17 August 2023",
          "dollar": "USD", //----- field I want to take
          "allocations": [{
            "id": "1005",
            "name": "chocolatePreferred", //----- field I want to take
          }, {
            "id": "1007",
            "name": "chocolate Large Cap", //----- field I want to take
          }],
        }
      }],
      "products": { //----- field I want to take
        "inital": 169000000, //----- field I want to take
        "externalproducts": [{ //----- field I want to take
          "id": 659,
          "name": "Additional", //----- field I want to take
        }],
        "productAllocation": { //----- field I want to take
          "productAllocation": [{
            "id": "1005",
            "category": "Card", //----- field I want to take     
          }, {
            "id": "1007",
            "category": "Fruit", //----- field I want to take
          }],
        },
      },
      "analysis": { //----- field I want to take
        "analysisA": { //----- field I want to take
          "id": 50443,
          "key": "Liq", //----- field I want to take
        },
        "analysisB": { //----- field I want to take
          "id": 50443,
          "key": "dity", //----- field I want to take
        },
      },
    },
  },
};

console.log(
  cloneDataAndIgnoreKeypaths(
    sampleData,
    new Set([
      'abcId',
      'plan.planDetails.customproductAllocations[n].id',
      'plan.planDetails.customproductAllocations[n].allocationDetail.name',
      'plan.planDetails.customproductAllocations[n].allocationDetail.allocations[n].id',
      'plan.planDetails.products.externalproducts[n].id',
      'plan.planDetails.products.productAllocation.productAllocation[n].id',
      'plan.planDetails.analysis.analysisA.id',
      'plan.planDetails.analysis.analysisB.id',
    ]),
  )
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

评论

0赞 Peter Seliger 10/28/2023
我很好奇收到 -1 的原因。在我看来,上述方法和解决方案以可读(代码明智)和解释良好(关于方法)的方式带来了 OP 所要求的一切。没有技术故障。如果涉及到真正的实时使用,单个函数调用以及要忽略的路径列表就一针见血。无论谁投票,都可以就方法/解决方案在哪些方面失败或在哪些方面需要改进发表评论。这将对观众有所帮助,就像我的回答会从中受益一样。
-1赞 Dimava 10/27/2023 #3

下面是受 ArkType 语法启发的简单选择器,
它适用于数组、记录和可选键

https://tsplay.dev/wjpebm 具有用于自动完成的 typedefs

function mapObject(obj, mapper) {
    return Object.fromEntries(Object.entries(obj)
        .map(([k, v]) => mapper(k, v))
        .filter((e) => e?.length === 2));
}
function arkPick(obj, schema) {
    if (Array.isArray(obj))
        return obj.map(e => arkPick(e, schema));
    if (Object.keys(schema)[0] === '__record')
        return mapObject(obj, (k, v) => [k, arkPick(v, schema.__record)]);
    return mapObject(schema, (k, v) => {
        let opt = k.endsWith('?');
        if (opt)
            k = k.slice(0, -1);
        if (!(k in obj)) {
            if (opt)
                return [];
            else
                throw new Error(`missign property ${k}`);
        }
        if (v === 'any' || v === true)
            return [k, obj[k]];
        if (typeof v === 'string') {
            if (typeof obj[k] === v)
                return [k, obj[k]];
            else
                throw new Error(`incorrect type of property ${k}`);
        }
        return [k, arkPick(obj[k], v)];
    });
}
console.log(arkPick(data, {
  productId: 'string',
  time: 'number',
  'member?': 'object', // optional
  'ZZZmissingZZZ?': 'any', // ignored
  plan: {
    id: 'number',
    lastModifiedDate: 'string',
    planDetails: {
      customproductAllocations: {
        allocationDetail: { // array
          name: 'string'
        }
      }
    },
    analysis: {
      __record: { // record
        key: 'string'
      }
    }
  }
}))