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

问题描述 投票:0回答:3

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

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

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

所以,我现在的做法如下

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,如果传递到 optimouslySelProperties 的数据包含嵌套对象,并且我还需要有选择地选择属性。我无法在我的功能中实现这一点。

有办法实现吗?

这是我要复制的数据

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 data-structures nested clone partial
3个回答
0
投票

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

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
投票

这是一个受 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'
      }
    }
  }
}))

-1
投票

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

注意 ... OP 的请求用忽略列表比用必需列表更好地描述。

除了第一个参数(要克隆的值/数据)之外,该函数还接受一个

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的特定用例。

Set
实例现在将具有
keypath
值,而不是不够精确的属性名称,而递归函数将聚合当前作用域的
keypath
并将其传递给自身,据此做出忽略决策。

基于

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 array item.
          (keypath.endsWith(']') && (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; }

© www.soinside.com 2019 - 2024. All rights reserved.