_.assign 仅当 属性 存在于目标对象中
_.assign only if property exists in target object
我需要做一些类似 _.assign 的事情,但前提是目标对象已经分配了 属性。把它想象成源对象可能有一些属性可以贡献,但也有一些我不想混入的属性。
我没用过_.assign的回调机制,但是尝试了下面的方法。它 'worked',但它仍然将 属性 分配给目标对象(未定义)。我根本不希望它分配。
_.assign(options, defaults, initial, function (destVal, sourceVal) {
return typeof destVal == 'undefined' ? undefined : sourceVal;
});
我编写了以下函数来执行此操作,但想知道 lodash 是否已经内置了一些更优雅的东西。
function softMerge (dest, source) {
return Object.keys(dest).reduce(function (dest, key) {
var sourceVal = source[key];
if (!_.isUndefined(sourceVal)) {
dest[key] = sourceVal;
}
return dest;
}, dest);
}
你可以只拿第一个对象的钥匙
var firstKeys = _.keys(options);
然后从第二个对象中取出一个子集对象,只取第一个对象上存在的那些键:
var newDefaults = _.pick(defaults, firstKeys);
然后使用该新对象作为 _.assign
的参数:
_.assign(options, newDefaults);
或在一行中:
_.assign(options, _.pick(defaults, _.keys(options)));
我在这里测试时似乎可以工作:http://jsbin.com/yiyerosabi/1/edit?js,console
这是一个不可变的深度版本,我称之为"merge that retains the shape",在使用lodash的TypeScript中:
function _mergeKeepShapeArray(dest: Array<any>, source: Array<any>) {
if (source.length != dest.length) {
return dest;
}
let ret = [];
dest.forEach((v, i) => {
ret[i] = _mergeKeepShape(v, source[i]);
});
return ret;
}
function _mergeKeepShapeObject(dest: Object, source: Object) {
let ret = {};
Object.keys(dest).forEach((key) => {
let sourceValue = source[key];
if (typeof sourceValue !== "undefined") {
ret[key] = _mergeKeepShape(dest[key], sourceValue);
} else {
ret[key] = dest[key];
}
});
return ret;
}
function _mergeKeepShape(dest, source) {
// else if order matters here, because _.isObject is true for arrays also
if (_.isArray(dest)) {
if (!_.isArray(source)) {
return dest;
}
return _mergeKeepShapeArray(dest, source);
} else if (_.isObject(dest)) {
if (!_.isObject(source)) {
return dest;
}
return _mergeKeepShapeObject(dest, source);
} else {
return source;
}
}
/**
* Immutable merge that retains the shape of the `existingValue`
*/
export const mergeKeepShape = <T>(existingValue: T, extendingValue): T => {
return _mergeKeepShape(existingValue, extendingValue);
}
还有一个简单的测试,看看我认为这种合并应该如何工作:
let newObject = mergeKeepShape(
{
a : 5,
// b is not here
c : 33,
d : {
e : 5,
// f is not here
g : [1,1,1],
h : [2,2,2],
i : [4,4,4],
}
},
{
a : 123,
b : 444,
// c is not here
d : {
e : 321,
f : 432,
// g is not here
h : [3,3,3],
i : [1,2],
}
}
);
expect(newObject).toEqual({
a : 123,
// b is not here
c : 33,
d : {
e : 321,
// f is not here,
g : [1,1,1],
h : [3,3,3],
i : [4,4,4]
}
});
我自己在测试中使用了 seamless-immutable,但没觉得有必要把它放在这个答案中。
我特此将其放在 Public 域中。
实现此目的的另一种方法是结合 _.mapObject
with _.has
_.mapObject(object1, function(v, k) {
return _.has(object2, k) ? object2[k] : v;
});
解释:
- 使用
_.mapObject
遍历所有key/value对object1
- 使用
_.has
,检查 属性 名称 k
是否也存在于 object2
.
- 如果是,则将分配给键
object2
的 k
的值复制回 object1
,否则,仅 return 对象 1 的现有值(v
).
根据@svarog 的回答,我想到了这个(lodash 版本 4.17.15):
const mergeExistingProps = (target, source) => _.mapValues(target, (value, prop) => _.get(source, prop, value));
我最近在我的个人项目中有同样的需求,我需要将一个对象(SOURCE)的值填充到另一个对象(TARGET) 但不要展开它的 属性。此外,还应满足一些额外要求:
- 在 source 中具有
null
值的任何 属性 将不会更新到 target;
- 来自 source 的任何值都可以更新到 target 如果这样 属性 in target 有
null
值。
- 目标中保存数组的 属性 将根据源中的数据加载,但数组的所有条目将与目标数组保持相同(因此目标中的空数组不会获得任何数据,因为该项目没有 属性)
- 属性 的目标持有一个二维数组(数组有另一个数组作为它的项)不会被更新,因为合并两个不同形状的二维数组的意义不明确对我来说。
下面是一个例子(在代码中有详细解释):
假设你有一个resume对象,里面有你的所有数据,你想把这些数据填到公司的申请表中(也是一个对象)。您希望结果与申请表具有相同的形状,因为公司不关心其他事情,那么您可以认为您的简历是 SOURCE 而申请表是 目标.
请注意TARGET中的“附加”字段是null
,这意味着可以根据SOURCE[=49=在这里更新任何内容] 数据(作为规则 #2)
控制台输出是JSON格式,复制到一些JSON到JS-OBJ转换器比如
https://www.convertsimple.com/convert-json-to-javascript/
有更好的视野
const applicationForm = {
name: 'Your Name',
gender: 'Your Gender',
email: 'your@email.com',
birth: 0,
experience: [ // employer want you list all your experience
{
company: 'Some Company',
salary: 0,
city: ['', '', ''], // list all city worked for each company
}
],
language: { // employer only care about 2 language skills
english: {
read: false,
write: false,
speak: 'Speak Level'
},
chinese: {
read: false,
write: false,
speak: 'Speak Level'
}
},
additional: null // add anything you want the employer to know
}
const resume = {
name: 'Yunfan',
gender: 'Male',
birth: 1995,
phone: '1234567',
email: 'example@gmail.com',
experience: [
{
company: 'Company A',
salary: 100,
city: ['New York', 'Chicago', 'Beijing'],
id: '0001',
department: 'R&D'
},
{
company: 'Company B',
salary: 200,
city: ['New York'],
id: '0002',
department: 'HR'
},
{
company: 'Company C',
salary: 300,
city: ['Tokyo'],
id: '0003',
}
],
language: {
english: {
read: true,
write: true,
speak: 'Native Speaker'
},
chinese: {
read: true,
write: false,
speak: 'HSK Level 3'
},
spanish: {
read: true,
write: true,
speak: 'Native Speaker'
}
},
additional: {
music: 'Piano',
hometown: 'China',
interest: ['Cooking', 'Swimming']
}
}
function safeMerge(source, target) {
// traverse the keys in the source object, if key not found in target or with different type, drop it, otherwise:
// 1. Use object merge if the value is an object (Can go deeper inside the object and apply same rule on all its properties)
// 2. Use array merge if value is array (Extend the array item from source, but keep the obj format of target)
// 3. Assign the value in other case (For other type, no need go deeper, assign directly)
for (const key in source) {
let value = source[key]
const targetValueType = typeof target[key]
const sourceValueType = typeof value
// if key not found in target or type not match
if (targetValueType === 'undefined' || targetValueType !== sourceValueType) {
continue // property not found in target or type not match
}
// for both type in object, need additional check
else if (targetValueType === 'object' && sourceValueType === 'object') {
// if value in target is null, assign any value from source to target, ignore format
if (target[key] === null) {
target[key] = source[key]
}
// if value in target is array, merge the item in source to target using the format of target only if source value is array
else if (Array.isArray(target[key]) && Array.isArray(value)) {
target[key] = mergeArray(value, target[key])
}
// if value in target is 'real' object (not null or array)', use object merge to do recurring merge, keep target format
else if (!Array.isArray(target[key])){
if (!Array.isArray(value) && value !== null) {
safeMerge(value, target[key])
}
}
}
// if target value and source value has same type but not object, assign directly
else if (targetValueType === sourceValueType) {
target[key] = value
}
}
}
function mergeArray(sourceArray, targetArray) {
// the rule of array merge need additional declare, assume the target already have values or objects in save format in the property<Array>,
// otherwise will not merge item from source to target since cannot add item property,
// NOTE: the item in target array will be totally overwrite instead of append on the tail, only the format will be keep,
// so the lenth of this property will same as source, below is a example:
// target = [{a: 1, b: 2}, {a: 3, b: 4}] // Must in same format, otherwise the first one will be standard
// source = [{a: 5, b: 6, c: 7}]
// mergeArray(source, target) => [{a: 5, b: 6}] // use format of target, but data from source
// double check both of values are array
if (!Array.isArray(sourceArray) || !Array.isArray(targetArray)) {
return
}
// if target array is empty, don't push data in, since format is empty
if (targetArray.length === 0) {
return
}
let resultArray = [] // array to save the result
let targetFormat = targetArray[0]
let targetArrayType = typeof targetArray[0]
// assign value from source to target, if item in target array is not object
if (targetArrayType !== 'object'){
sourceArray.forEach((value) => {
// assign value directly if the type matched
if (targetArrayType === typeof value) {
resultArray.push(value)
}
})
}
// if the item in target is null, push anything in source to target (accept any format)
else if (targetArray[0] === null) {
sourceArray.forEach((value) => {
resultArray.push(value)
})
}
// if the item in target is array, drop it (the meaning of merge 2-d array to a 2-d array is not clear, so skip the situation)
else if (!Array.isArray(targetArray[0])){
// the item is a 'real' object, do object merge based on format of first item of target array
sourceArray.forEach((value) => {
safeMerge(value, targetFormat) // data in targetFormat keep changing, so need to save a independent copy to the result
resultArray.push(JSON.parse(JSON.stringify(targetFormat)))
})
}
else {
console.log('2-d array will be skipped')
}
// replace the value of target with newly built array (Assign result to target array will not work, must assign outside)
return resultArray
}
safeMerge(resume, applicationForm)
console.log(JSON.stringify(applicationForm))
我需要做一些类似 _.assign 的事情,但前提是目标对象已经分配了 属性。把它想象成源对象可能有一些属性可以贡献,但也有一些我不想混入的属性。
我没用过_.assign的回调机制,但是尝试了下面的方法。它 'worked',但它仍然将 属性 分配给目标对象(未定义)。我根本不希望它分配。
_.assign(options, defaults, initial, function (destVal, sourceVal) {
return typeof destVal == 'undefined' ? undefined : sourceVal;
});
我编写了以下函数来执行此操作,但想知道 lodash 是否已经内置了一些更优雅的东西。
function softMerge (dest, source) {
return Object.keys(dest).reduce(function (dest, key) {
var sourceVal = source[key];
if (!_.isUndefined(sourceVal)) {
dest[key] = sourceVal;
}
return dest;
}, dest);
}
你可以只拿第一个对象的钥匙
var firstKeys = _.keys(options);
然后从第二个对象中取出一个子集对象,只取第一个对象上存在的那些键:
var newDefaults = _.pick(defaults, firstKeys);
然后使用该新对象作为 _.assign
的参数:
_.assign(options, newDefaults);
或在一行中:
_.assign(options, _.pick(defaults, _.keys(options)));
我在这里测试时似乎可以工作:http://jsbin.com/yiyerosabi/1/edit?js,console
这是一个不可变的深度版本,我称之为"merge that retains the shape",在使用lodash的TypeScript中:
function _mergeKeepShapeArray(dest: Array<any>, source: Array<any>) {
if (source.length != dest.length) {
return dest;
}
let ret = [];
dest.forEach((v, i) => {
ret[i] = _mergeKeepShape(v, source[i]);
});
return ret;
}
function _mergeKeepShapeObject(dest: Object, source: Object) {
let ret = {};
Object.keys(dest).forEach((key) => {
let sourceValue = source[key];
if (typeof sourceValue !== "undefined") {
ret[key] = _mergeKeepShape(dest[key], sourceValue);
} else {
ret[key] = dest[key];
}
});
return ret;
}
function _mergeKeepShape(dest, source) {
// else if order matters here, because _.isObject is true for arrays also
if (_.isArray(dest)) {
if (!_.isArray(source)) {
return dest;
}
return _mergeKeepShapeArray(dest, source);
} else if (_.isObject(dest)) {
if (!_.isObject(source)) {
return dest;
}
return _mergeKeepShapeObject(dest, source);
} else {
return source;
}
}
/**
* Immutable merge that retains the shape of the `existingValue`
*/
export const mergeKeepShape = <T>(existingValue: T, extendingValue): T => {
return _mergeKeepShape(existingValue, extendingValue);
}
还有一个简单的测试,看看我认为这种合并应该如何工作:
let newObject = mergeKeepShape(
{
a : 5,
// b is not here
c : 33,
d : {
e : 5,
// f is not here
g : [1,1,1],
h : [2,2,2],
i : [4,4,4],
}
},
{
a : 123,
b : 444,
// c is not here
d : {
e : 321,
f : 432,
// g is not here
h : [3,3,3],
i : [1,2],
}
}
);
expect(newObject).toEqual({
a : 123,
// b is not here
c : 33,
d : {
e : 321,
// f is not here,
g : [1,1,1],
h : [3,3,3],
i : [4,4,4]
}
});
我自己在测试中使用了 seamless-immutable,但没觉得有必要把它放在这个答案中。
我特此将其放在 Public 域中。
实现此目的的另一种方法是结合 _.mapObject
with _.has
_.mapObject(object1, function(v, k) {
return _.has(object2, k) ? object2[k] : v;
});
解释:
- 使用
_.mapObject
遍历所有key/value对 - 使用
_.has
,检查 属性 名称k
是否也存在于object2
. - 如果是,则将分配给键
object2
的k
的值复制回object1
,否则,仅 return 对象 1 的现有值(v
).
object1
根据@svarog 的回答,我想到了这个(lodash 版本 4.17.15):
const mergeExistingProps = (target, source) => _.mapValues(target, (value, prop) => _.get(source, prop, value));
我最近在我的个人项目中有同样的需求,我需要将一个对象(SOURCE)的值填充到另一个对象(TARGET) 但不要展开它的 属性。此外,还应满足一些额外要求:
- 在 source 中具有
null
值的任何 属性 将不会更新到 target; - 来自 source 的任何值都可以更新到 target 如果这样 属性 in target 有
null
值。 - 目标中保存数组的 属性 将根据源中的数据加载,但数组的所有条目将与目标数组保持相同(因此目标中的空数组不会获得任何数据,因为该项目没有 属性)
- 属性 的目标持有一个二维数组(数组有另一个数组作为它的项)不会被更新,因为合并两个不同形状的二维数组的意义不明确对我来说。
下面是一个例子(在代码中有详细解释):
假设你有一个resume对象,里面有你的所有数据,你想把这些数据填到公司的申请表中(也是一个对象)。您希望结果与申请表具有相同的形状,因为公司不关心其他事情,那么您可以认为您的简历是 SOURCE 而申请表是 目标.
请注意TARGET中的“附加”字段是null
,这意味着可以根据SOURCE[=49=在这里更新任何内容] 数据(作为规则 #2)
控制台输出是JSON格式,复制到一些JSON到JS-OBJ转换器比如 https://www.convertsimple.com/convert-json-to-javascript/ 有更好的视野
const applicationForm = {
name: 'Your Name',
gender: 'Your Gender',
email: 'your@email.com',
birth: 0,
experience: [ // employer want you list all your experience
{
company: 'Some Company',
salary: 0,
city: ['', '', ''], // list all city worked for each company
}
],
language: { // employer only care about 2 language skills
english: {
read: false,
write: false,
speak: 'Speak Level'
},
chinese: {
read: false,
write: false,
speak: 'Speak Level'
}
},
additional: null // add anything you want the employer to know
}
const resume = {
name: 'Yunfan',
gender: 'Male',
birth: 1995,
phone: '1234567',
email: 'example@gmail.com',
experience: [
{
company: 'Company A',
salary: 100,
city: ['New York', 'Chicago', 'Beijing'],
id: '0001',
department: 'R&D'
},
{
company: 'Company B',
salary: 200,
city: ['New York'],
id: '0002',
department: 'HR'
},
{
company: 'Company C',
salary: 300,
city: ['Tokyo'],
id: '0003',
}
],
language: {
english: {
read: true,
write: true,
speak: 'Native Speaker'
},
chinese: {
read: true,
write: false,
speak: 'HSK Level 3'
},
spanish: {
read: true,
write: true,
speak: 'Native Speaker'
}
},
additional: {
music: 'Piano',
hometown: 'China',
interest: ['Cooking', 'Swimming']
}
}
function safeMerge(source, target) {
// traverse the keys in the source object, if key not found in target or with different type, drop it, otherwise:
// 1. Use object merge if the value is an object (Can go deeper inside the object and apply same rule on all its properties)
// 2. Use array merge if value is array (Extend the array item from source, but keep the obj format of target)
// 3. Assign the value in other case (For other type, no need go deeper, assign directly)
for (const key in source) {
let value = source[key]
const targetValueType = typeof target[key]
const sourceValueType = typeof value
// if key not found in target or type not match
if (targetValueType === 'undefined' || targetValueType !== sourceValueType) {
continue // property not found in target or type not match
}
// for both type in object, need additional check
else if (targetValueType === 'object' && sourceValueType === 'object') {
// if value in target is null, assign any value from source to target, ignore format
if (target[key] === null) {
target[key] = source[key]
}
// if value in target is array, merge the item in source to target using the format of target only if source value is array
else if (Array.isArray(target[key]) && Array.isArray(value)) {
target[key] = mergeArray(value, target[key])
}
// if value in target is 'real' object (not null or array)', use object merge to do recurring merge, keep target format
else if (!Array.isArray(target[key])){
if (!Array.isArray(value) && value !== null) {
safeMerge(value, target[key])
}
}
}
// if target value and source value has same type but not object, assign directly
else if (targetValueType === sourceValueType) {
target[key] = value
}
}
}
function mergeArray(sourceArray, targetArray) {
// the rule of array merge need additional declare, assume the target already have values or objects in save format in the property<Array>,
// otherwise will not merge item from source to target since cannot add item property,
// NOTE: the item in target array will be totally overwrite instead of append on the tail, only the format will be keep,
// so the lenth of this property will same as source, below is a example:
// target = [{a: 1, b: 2}, {a: 3, b: 4}] // Must in same format, otherwise the first one will be standard
// source = [{a: 5, b: 6, c: 7}]
// mergeArray(source, target) => [{a: 5, b: 6}] // use format of target, but data from source
// double check both of values are array
if (!Array.isArray(sourceArray) || !Array.isArray(targetArray)) {
return
}
// if target array is empty, don't push data in, since format is empty
if (targetArray.length === 0) {
return
}
let resultArray = [] // array to save the result
let targetFormat = targetArray[0]
let targetArrayType = typeof targetArray[0]
// assign value from source to target, if item in target array is not object
if (targetArrayType !== 'object'){
sourceArray.forEach((value) => {
// assign value directly if the type matched
if (targetArrayType === typeof value) {
resultArray.push(value)
}
})
}
// if the item in target is null, push anything in source to target (accept any format)
else if (targetArray[0] === null) {
sourceArray.forEach((value) => {
resultArray.push(value)
})
}
// if the item in target is array, drop it (the meaning of merge 2-d array to a 2-d array is not clear, so skip the situation)
else if (!Array.isArray(targetArray[0])){
// the item is a 'real' object, do object merge based on format of first item of target array
sourceArray.forEach((value) => {
safeMerge(value, targetFormat) // data in targetFormat keep changing, so need to save a independent copy to the result
resultArray.push(JSON.parse(JSON.stringify(targetFormat)))
})
}
else {
console.log('2-d array will be skipped')
}
// replace the value of target with newly built array (Assign result to target array will not work, must assign outside)
return resultArray
}
safeMerge(resume, applicationForm)
console.log(JSON.stringify(applicationForm))