如何用另一个对象的键动态替换一个对象中的某些键?

How can I dynamically replace certain keys in one object with keys from another object?

我有两个对象代表两种不同语言的翻译。它们在结构上完全相同,但值已被翻译。

英语

{
  about: {
    title: "About",
    subtitle: "Something",
    table: {
      columns: [...],
    }
  },
  products: {
    columns: [...]
  },
  games: {
    details: {
      title: "Game Details",
      columns: [...]
    }
  }
}

法语

{
  about: {
    title: "À propos de",
    subtitle: "Quelque chose",
    table: {
      columns: [...],
    }
  },
  products: {
    columns: [...]
  },
  games: {
    details: {
      title: "Détails du jeu",
      columns: [...]
    }
  }
}

我想保留法语对象,但将 columns 的所有实例替换为第一个对象的英语版本。我该怎么做?

我使用的对象很大而且嵌套很深,所以我想我需要某种递归函数。不过,我不确定如何跟踪要更换的钥匙。

我设法通过展平两个对象、遍历 FR 对象的每个键并检查键是否包含“列”来实现这一点。如果是这样,我用 EN 键替换那个键。完成后,我将新对象展开回其原始形状。

const enFlattened = flattenObject(enJSON);
const frFlattened = flattenObject(frJSON);

// loop through each key in flattened FR object
for (let k in frFlattened) {
  // if key includes "columns"
  if(k.includes('columns')) {
    // replace value with EN value
    frFlattened[k] = enFlattened[k];
  }
}

const translations = unflattenObject(frFlattened);

展平对象使比较更易于管理。我在这里找到了 flattenObject()unFlattenObject() 函数:

function flattenObject(ob, prefix = false, result = null) {
  result = result || {};

  // Preserve empty objects and arrays, they are lost otherwise
  if (prefix && typeof ob === 'object' && ob !== null && Object.keys(ob).length === 0) {
    result[prefix] = Array.isArray(ob) ? [] : {};
    return result;
  }

  prefix = prefix ? prefix + '.' : '';

  for (const i in ob) {
    if (Object.prototype.hasOwnProperty.call(ob, i)) {
      if (typeof ob[i] === 'object' && ob[i] !== null) {
        // Recursion on deeper objects
        flattenObject(ob[i], prefix + i, result);
      } else {
        result[prefix + i] = ob[i];
      }
    }
  }
  return result;
}

function unflattenObject(ob) {
  const result = {};
  for (const i in ob) {
    if (Object.prototype.hasOwnProperty.call(ob, i)) {
      const keys = i.match(/^\.+[^.]*|[^.]*\.+$|(?:\.{2,}|[^.])+(?:\.+$)?/g); // Just a complicated regex to only match a single dot in the middle of the string
      keys.reduce((r, e, j) => {
        return (
          r[e] || (r[e] = isNaN(Number(keys[j + 1])) ? (keys.length - 1 === j ? ob[i] : {}) : [])
        );
      }, result);
    }
  }
  return result;
}

具体例子

首先我们建立一个定义明确的例子,为 en 列填充一些值 -

let en = 
  {
    about: {
      title: "About",
      subtitle: "Something",
      table: {
        columns: ["en_about_1", "en_about_2"]   // <-
      }
    },
    products: {
      columns: ["en_products"]                  // <-
    },
    games: {
      details: {
        title: "Game Details",
        columns: ["en_games_1", "en_games_2"]   // <-
      }
    }
  }

我们对 fr -

做同样的事情
let fr =
  {
    about: {
      title: "À propos de",
      subtitle: "Quelque chose",
      table: {
        columns: ["fr_apropos_1", "fr_apropos_2"], // <-
      }
    },
    products: {
      columns: ["fr_produit"]                      // <-
    },
    games: {
      details: {
        title: "Détails du jeu",
        columns: ["fr_details_1", "fr_details_2"]  // <-
      }
    }
  }

遍历

接下来我们需要一种方法遍历给定对象中的所有paths -

function* paths (t)
{ switch(t?.constructor)
  { case Object:
      for (const [k,v] of Object.entries(t))
        for (const path of paths(v))
          yield [k, ...path]
      break
    default:
      yield []
  }
}

let fr =
  {about: {title: "À propos de",subtitle: "Quelque chose",table: {columns: ["fr_apropos_1", "fr_apropos_2"],}},products: {columns: ["fr_produit"]},games: {details: {title: "Détails du jeu",columns: ["fr_details_1", "fr_details_2"]}}}
  
for (const path of paths(fr))
  console.log(JSON.stringify(path))

["about","title"]
["about","subtitle"]
["about","table","columns"]
["products","columns"]
["games","details","title"]
["games","details","columns"]

读写

接下来我们需要一种方法来将值从一个对象读写到另一个对象 -

  1. getAt 接受对象和路径,returns 接受值
  2. setAt 获取对象、路径和值,并设置值
function getAt (t, [k, ...path])
{ if (k == null)
    return t
  else
    return getAt(t?.[k], path)
}

function setAt (t, [k, ...path], v)
{ if (k == null)
    return v
  else
    return {...t, [k]: setAt(t?.[k] ?? {}, path, v) }
}

复制到路径

对于 fr 的每个 path,路径以 "columns" 结尾的地方,使用 en 中的值更新 path 处的 fr ] 在 path -

for (const path of paths(fr))              // for each path of fr
  if (path.slice(-1)[0] == "columns")      // where the path ends in "columns"
    fr = setAt(fr, path, getAt(en, path))  // update fr at path with value from en at path
    
console.log(JSON.stringify(fr))

展开下面的代码片段并在您自己的浏览器中验证结果 -

function* paths (t)
{ switch(t?.constructor)
  { case Object:
      for (const [k,v] of Object.entries(t))
        for (const path of paths(v))
          yield [k, ...path]
      break
    default:
      yield []
  }
}

function getAt (t, [k, ...path])
{ if (k == null)
    return t
  else
    return getAt(t?.[k], path)
}

function setAt (t, [k, ...path], v)
{ if (k == null)
    return v
  else
    return {...t, [k]: setAt(t?.[k] ?? {}, path, v) }
}


let en =
  {about: {title: "About",subtitle: "Something",table: {columns: ["en_about_1", "en_about_2"]}},products: {columns: ["en_products"]},games: {details: {title: "Game Details",columns: ["en_games_1", "en_games_2"]}}}

let fr =
  {about: {title: "À propos de",subtitle: "Quelque chose",table: {columns: ["fr_apropos_1", "fr_apropos_2"],}},products: {columns: ["fr_produit"]},games: {details: {title: "Détails du jeu",columns: ["fr_details_1", "fr_details_2"]}}}
  
for (const path of paths(fr))
  if (path.slice(-1)[0] == "columns")
    fr = setAt(fr, path, getAt(en, path))
    
console.log(JSON.stringify(fr, null, 2))

{
  "about": {
    "title": "À propos de",
    "subtitle": "Quelque chose",
    "table": {
      "columns": [                      // <-
        "en_about_1",
        "en_about_2" 
      ]
    }
  },
  "products": {
    "columns": [                        // <-
      "en_products"
    ]
  },
  "games": {
    "details": {
      "title": "Détails du jeu",
      "columns": [                      // <-
        "en_games_1",
        "en_games_2"
      ]
    }
  }
}

每个 "columns".

的所有 en 值都复制到 fr

此回答的结构与 Thankyou 的回答类似。但是有足够的差异值得添加。

我们将构建一个函数 substitute。它将接受一个谓词,它接受我们对象中的数组路径,例如 ["about", "table"]["games", "details", "columns", 1] 和 returns truefalsesubstitute returns 一个函数。该函数采用源对象和目标对象,在谓词函数接受的每条路径上将值从复制到目标,返回一个新对象。

我们使用 substitute 创建函数来解决这个问题,方法是向其传递一个谓词,该谓词测试路径的最后一个节点是否为 columns。这是一个实现:

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

const getPath = ([p, ...ps]) => (o) =>
  p == undefined ? o : getPath (ps) (o?.[p])

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 substitute = (pred) => (source, target) =>
  allPaths (target) 
    .filter (pred) 
    .reduce ((a, p) => setPath (p) (getPath (p) (source)) (a), target)


const replaceColumns = 
  substitute (path => path .slice (-1) [0] == 'columns')

let english = {about: {title: "About", subtitle: "Something", table: {columns: ["en_about_1", "en_about_2"]}}, products: {columns: ["en_products"]}, games: {details: {title: "Game Details", columns: ["en_games_1", "en_games_2"]}}}
let french = {about: {title: "À propos de",  subtitle: "Quelque chose",  table: {columns: ["fr_apropos_1",  "fr_apropos_2"]}},  products: {columns: ["fr_produit"]},  games: {details: {title: "Détails du jeu",  columns: ["fr_details_1",  "fr_details_2"]}}}

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

我们从 allPaths 开始,它递归地查找对象中的路径。对于 Thankyou 的增强版对象 french,我们将得到这些路径:

[
  ["about"],
  ["about", "title"],
  ["about", "subtitle"],
  ["about", "table"],
  ["about", "table", "columns"],
  ["about", "table", "columns", 0],
  ["about", "table", "columns", 1],
  ["products"],
  ["products", "columns"],
  ["products", "columns", 0],
  ["games"],
  ["games", "details"],
  ["games", "details", "title"],
  ["games", "details", "columns"],
  ["games", "details", "columns", 0],
  ["games", "details", "columns", 1]
]

路径是对象的节点名称数组或数组的数字索引。

我们编写了两个函数来根据此类路径在对象中获取和设置值。这些都是简单的递归,唯一的复杂性来自 setPath 来处理与其他对象分开的重建数组。

我们的主要功能 substitute 根据谓词过滤目标对象中的路径,并通过将每个路径的值设置为沿该路径在资源。在我们的例子中,我们 select 的路径是 ["about", "table", "columns"]["products", "columns"]["games", "details", "columns"].

而我们的 replaceColumns 只是将测试路径中最后一个节点是否为 "columns".

的谓词传递给 substitute

我们应该注意到,返回的对象在结构上与原始对象尽可能多地共享。如果你想让它完全分离,你需要在此基础上应用一些结构克隆。

与Thankyou给出的答案最大的区别在于,辅助函数都可以感知数组,并以遍历和重构包含数组的对象的方式来处理它们。虽然它们是我以前使用过的版本的变体,但这些特定版本并未经过充分测试。