_.assign 仅当属性存在于目标对象中时

_.assign only if property exists in target object

本文关键字:目标对象 存在 属性 assign      更新时间:2023-09-26

我需要做一些类似_.assign的事情,但前提是目标对象已经分配了属性。 可以把它想象成源对象可能有一些属性要贡献,但也有一些我不想混入的属性。

我从未使用过 _.assign 的回调机制,但尝试了以下内容。 它"工作",但它仍然将属性分配给 dest 对象(未定义)。 我根本不希望它分配。

_.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

这是一个不可变的深度版本,我称之为"保留形状的合并",在使用 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]
    }
});

我在测试中使用了无缝不可变,但认为没有必要把它放在这个答案中。

我特此将其置于公共领域。

实现此目的的另一种方法是将_.mapObject_.has相结合

_.mapObject(object1, function(v, k) {
    return _.has(object2, k) ? object2[k] : v;
});

解释:

  1. 使用 _.mapObject遍历object1的所有键/值对
  2. 使用 _.has ,检查属性名称k是否也存在于object2中。
  3. 如果是这样,则将分配给键object2 k的值复制回object1,否则,只需返回 object1 的现有值(v)。

普伦克

按照@svarog的回答,我想出了这个(lodash 版本 4.17.15):

const mergeExistingProps = (target, source) => _.mapValues(target, (value, prop) => _.get(source, prop, value));

我最近在我的个人项目中也有同样的需求,我需要将值从一个对象(SOURCE)填充到另一个对象(TARGET),但不扩展其属性。此外,还应满足一些其他要求:

  1. 中具有null值的任何属性都不会更新到目标;
  2. 如果目标中的此类属性具有null值,则可以将中的任何值更新为目标
  3. 在目标
  4. 中保存数组的属性将根据来自源的数据加载,但数组的所有条目将与目标数组保持相同(因此目标中的空数组将不会获取任何数据,因为该项没有属性)
  5. 持有 2-D 数组(数组具有另一个数组作为其项目)的目标的属性将不会更新,因为合并两个具有不同形状的 2-D 数组的含义
  6. 对我来说不清楚。

下面是一个示例(代码中有详细说明):

假设您有一个包含有关您的所有数据的简历对象,您希望将数据填写到公司的申请表中(也是一个对象)。您希望结果与申请表具有相同的形状,因为公司不关心其他事情,那么您可以认为您的简历是 SOURCE 并且申请表是 TARGET。

请注意,TARGET 中的"附加"字段是null,这意味着可以根据数据在此处更新任何内容(如规则 #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))