如何用另一个对象的键动态替换一个对象中的某些键?
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"]
读写
接下来我们需要一种方法来将值从一个对象读写到另一个对象 -
getAt
接受对象和路径,returns 接受值
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 true
或 false
。 substitute
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给出的答案最大的区别在于,辅助函数都可以感知数组,并以遍历和重构包含数组的对象的方式来处理它们。虽然它们是我以前使用过的版本的变体,但这些特定版本并未经过充分测试。
我有两个对象代表两种不同语言的翻译。它们在结构上完全相同,但值已被翻译。
英语
{
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"]
读写
接下来我们需要一种方法来将值从一个对象读写到另一个对象 -
getAt
接受对象和路径,returns 接受值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 true
或 false
。 substitute
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给出的答案最大的区别在于,辅助函数都可以感知数组,并以遍历和重构包含数组的对象的方式来处理它们。虽然它们是我以前使用过的版本的变体,但这些特定版本并未经过充分测试。