比较 2 个嵌套数据结构,目标 + 源,与源对应物相比,缺少目标值的适当合并策略是什么?

Comparing 2 nested data-structures,target+source,what are appropriate merge-strategies for missing target values compared to their source counterpart?

执行此操作的更好方法是什么。我正在将两个 属性 值(来自两个不同的对象)中的任何一个分配给第三个数据结构,具体取决于它们的存在。

如果 args 对象的值为 nullish,则从 default 对象访问非空值并将其分配给最终结构。

return {
  first: {
    visible: args.first?.visible ?? defaulttest.first?.visible,
    emoji: args.first?.emoji ?? defaulttest.first?.emoji,
    style: args.first?.style ?? defaulttest.first?.style,
  },
  back: {
    visible: args.back?.visible ?? defaulttest.back?.visible,
    emoji: args.back?.emoji ?? defaulttest.back?.emoji,
    style: args.back?.style ?? defaulttest.back?.style,
  },
  page: {
    visible: args.page?.visible ?? defaulttest.page?.visible,
    emoji: args.page?.emoji ?? defaulttest.page?.emoji,
    style: args.page?.style ?? defaulttest.page?.style,
  },
  forward: {
    visible: args.forward?.visible ?? defaulttest.forward?.visible,
    emoji: args.forward?.emoji ?? defaulttest.forward?.emoji,
    style: args.forward?.style ?? defaulttest.forward?.style,
  },

  last: {
    visible: args.last?.visible ?? defaulttest.last?.visible,
    emoji: args.last?.emoji ?? defaulttest.last?.emoji,
    style: args.last?.style ?? defaulttest.last?.style,
  },
  Mdelete: {
    visible: args.Mdelete?.visible ?? defaulttest.Mdelete?.visible,
    emoji: args.Mdelete?.emoji ?? defaulttest.Mdelete?.emoji,
    style: args.Mdelete?.style ?? defaulttest.Mdelete?.style,
  },
  removeBtn: {
    visible: args.removeBtn?.visible ?? defaulttest.removeBtn?.visible,
    emoji: args.removeBtn?.emoji ?? defaulttest.removeBtn?.emoji,
    style: args.removeBtn?.style ?? defaulttest.removeBtn?.style,
  },
};

如果您的对象结构与您提供的结构相同,您可以这样做:

function normalize(input, defaultValue) {
    // Loop on the outer keys
    Object.keys(input).forEach(mainKey => {
        // Loop on the inner keys
        Object.keys(input[mainKey]).forEach(key => {
            // set the value of the key as itself or default if null
            input[mainKey][key] = input[mainKey]?.[key] ?? defaultValue[mainKey]?.[key]
        })
    })
    return input;
}

调用 normalize(args, defaulttest) 您将循环访问每个内部键,检查它是否存在,如果不存在,您将其替换为同一路径中的默认值。

示例:

const x = {
  a: {a1: '1', a2: '2'},
  b: {b1: '1', b2: null}
}

const y = {b: {b2: '5'}}

console.log(normalize(x,y))

输出:

{
    "a": {
        "a1": "1",
        "a2": "2"
    },
    "b": {
        "b1": "1",
        "b2": "5"
    }
}

使用这种方法,您必须在 args 输入中输入密钥。如果密钥丢失,则不会用默认值替换。为了使其即使使用 not-present 键也能工作,您需要使用第三个结构,例如所有可能的路径。

根据我上面的评论...

1/2 ... The OP actually is not really comparing. For a certain set of properties the OP looks up each property at a target object, and only in case it features a nullish value there will be an assignment from a source object's counterpart to the missing property. Thus an approach I would choose was ...

2/2 ... implementing a generic function which merges two objects in a way that a source property can only be written/assigned in case the target structure does not already provide a non nullish value. This function then has to be invoked twice once for args and defaulttest and a second time for the to be returned entirely empty data structure and args.

上面的说法有点雄心勃勃,因为至少有两种策略可以实现这种合并。

因此,下面提供的示例代码实现了两种方法

  • 一个,称为 refit,由于强制分配,它遵循 pushing/patching 议程source-object 中的每个非无效 属性 到 target-object.

    的非无效对应项
  • 第二个,称为 revive,它类似于拉式方法,因为它只是重新分配无效的 target-object 属性及其非无效 source-object 对应物。

他们为一个和相同的预设产生的结果的差异将由...

// "refit" ... a pushing/patching approach.
// - force the assignement of every non nullish property in source
//   to its non nullish counterpart in target ... hence a *refit*.
function refitNullishValuesRecursively(target, source) {
  if (
    // are both values array-types?
    Array.isArray(source) &&
    Array.isArray(target)
  ) {
    source
      // for patching always iterate the source items ...
      .forEach((sourceItem, idx) => {
        // ... and look whether a target counterpart exists.
        if (target[idx] == null) {

          // either assign an existing structured clone ...
          if (sourceItem != null) {
            target[idx] = cloneDataStructure(sourceItem);
          }
        } else {
          // ... or proceed recursively.
          refitNullishValuesRecursively(target[idx], sourceItem);
        }
      });
  } else if (
    // are both values object-types?
    source && target &&
    'object' === typeof source &&
    'object' === typeof target
  ) {
    Object
      // for patching ...
      .entries(source)
      // ... always iterate the source entries (key value pairs) ...
      .forEach(([key, sourceValue], idx) => {
        // ... and look whether a target counterpart exists.
        if (target[key] == null) {

          // either assign an existing structured clone ...
          if (sourceValue != null) {
            target[key] = cloneDataStructure(sourceValue);
          }
        } else {
          // ... or proceed recursively.
          refitNullishValuesRecursively(target[key], sourceValue);
        }
      });
  }
  return target;
}
// "revive" ... a pulling approach.
// - just reassign the nullish target properties with their
//   non nullish source counterparts ... hence a *revive*.
function reviveNullishValuesRecursively(target, source) {
  if (
    // are both values array-types?
    Array.isArray(target) &&
    Array.isArray(source)
  ) {
    target
      // for fixing always iterate the target items.
      .forEach((targetItem, idx) => {
        if (targetItem == null) {

          // either assign an existing structured clone ...
          target[idx] = cloneDataStructure(source[idx]) ?? targetItem;
        } else {
          // ... or proceed recursively.
          reviveNullishValuesRecursively(targetItem, source[idx]);
        }
      });
  } else if (
    // are both values object-types?
    target && source &&
    'object' === typeof target &&
    'object' === typeof source
  ) {
    Object
      // for fixing ...
      .entries(target)
      // ... always iterate the target entries (key value pairs).
      .forEach(([key, targetValue], idx) => {
        if (targetValue == null) {

          // either assign an existing structured clone ...
          target[key] = cloneDataStructure(source[key]) ?? targetValue;
        } else {
          // ... or proceed recursively.
          reviveNullishValuesRecursively(targetValue, source[key]);
        }
      });
  }
  return target;
}
const cloneDataStructure =
  ('function' === typeof structuredClone)
  && structuredClone
  || (value => JSON.parse(JSON.stringify(value)));

const targetBlueprint = {
  x: { xFoo: 'foo', xBar: 'bar', xBaz: { xBiz: null } },
  y: { yFoo: 'foo', yBar: null },
};
const patch = {
  x: { xFoo: null, xBar: null, xBaz: { xBiz: 'biz' } },
  y: { yFoo: null, yBar: 'bar', yBaz: { yBiz: 'biz' } },
};
let target = cloneDataStructure(targetBlueprint);

console.log('"refit" ... a pushing/patching approach.');
console.log('before refit ...', { target, patch });
refitNullishValuesRecursively(target, patch);
console.log('after refit ...', { target, patch });

target = cloneDataStructure(targetBlueprint);

console.log('"revive" ... a pulling approach.');
console.log('before revive ...', { target, patch });
reviveNullishValuesRecursively(target, patch);
console.log('after revive ...', { target, patch });
.as-console-wrapper { min-height: 100%!important; top: 0; }

至于 OP 的示例,其目标是创建一种 config-object,完全可以 patch/refit 临时或当前 args-config 的克隆,而在最后一步是通过提供最基本的空 config-base 来控制 config-object 的最终结构,它刚刚被之前创建的完整 patch/refit-config.

恢复

function refitNullishValuesRecursively(target, source) {
  if (Array.isArray(source) && Array.isArray(target)) {
    source
      .forEach((sourceItem, idx) => {
        if (target[idx] == null) {

          if (sourceItem != null) {
            target[idx] = cloneDataStructure(sourceItem);
          }
        } else {
          refitNullishValuesRecursively(target[idx], sourceItem);
        }
      });
  } else if (
    source && target &&
    'object' === typeof source &&
    'object' === typeof target
  ) {
    Object
      .entries(source)
      .forEach(([key, sourceValue], idx) => {
        if (target[key] == null) {

          if (sourceValue != null) {
            target[key] = cloneDataStructure(sourceValue);
          }
        } else {
          refitNullishValuesRecursively(target[key], sourceValue);
        }
      });
  }
  return target;
}
function reviveNullishValuesRecursively(target, source) {
  if (Array.isArray(target) && Array.isArray(source)) {
    target
      .forEach((targetItem, idx) => {
        if (targetItem == null) {

          target[idx] = cloneDataStructure(source[idx]) ?? targetItem;
        } else {
          reviveNullishValuesRecursively(targetItem, source[idx]);
        }
      });
  } else if (
    target && source &&
    'object' === typeof target &&
    'object' === typeof source
  ) {
    Object
      .entries(target)
      .forEach(([key, targetValue], idx) => {
        if (targetValue == null) {

          target[key] = cloneDataStructure(source[key]) ?? targetValue;
        } else {
          reviveNullishValuesRecursively(targetValue, source[key]);
        }
      });
  }
  return target;
}
const cloneDataStructure =
  ('function' === typeof structuredClone)
  && structuredClone
  || (value => JSON.parse(JSON.stringify(value)));

const defaultConfig = {
  first: {
    visible: 'default.first.visible',
    emoji: 'default.first.emoji',
    style: 'default.first.style',
  },
  forward: {
    visible: 'default.forward.visible',
    emoji: 'default.forward.emoji',
    style: 'default.forward.style',
  },
  removeBtn: {
    visible: 'default.removeBtn.visible',
    emoji: 'default.removeBtn.emoji',
    style: 'default.removeBtn.style',
  },
};
const currentConfig = {
  first: {
    visible: 'current.first.visible',
    emoji: 'current.first.emoji',
    style: 'current.first.style',
  },
  forward: {
    visible: 'current.forward.visible',
    emoji: null,
  },
  FOO: {
    visible: 'current.FOO.visible',
    emoji: 'current.FOO.emoji',
    style: 'current.FOO.style',
  }
};

function getConfiguration(baseConfig) {
  return reviveNullishValuesRecursively(
    cloneDataStructure(baseConfig),
    refitNullishValuesRecursively(
      cloneDataStructure(currentConfig),
      defaultConfig,
    ),
  );
}
console.log(
  getConfiguration({
    first: null,
    forward: null,
    removeBtn: null,
  })
);
.as-console-wrapper { min-height: 100%!important; top: 0; }