lodash中是否有用于替换匹配项的函数?

179

我想知道在lodash中是否有一种更简单的方法来替换JavaScript集合中的项?(可能有重复的问题,但我没有理解那里的答案:)

我查看了他们的文档,但没有找到相关内容。

我的代码是:

var arr = [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];
// Can following code be reduced to something like _.XX(arr, {id:1}, {id:1, name: "New Name"});
_.each(arr, function(a, idx){
  if(a.id === 1){
    arr[idx] = {id:1, name: "Person New Name"};
    return false;
  }
});

_.each(arr, function(a){
  document.write(a.name);
});

更新: 我试图用另一个对象替代的对象有许多属性,例如

{id: 1, Prop1:...,Prop2:...,等等}

解决方案:

感谢dfsq,但我在 lodash 中找到了一个合适的解决方案,它似乎很好用并且非常简洁,我也将其放在了混合项中,因为我在很多地方都有这个要求。JSBin

var update = function(arr, key, newval) {
  var match = _.find(arr, key);
  if(match)
    _.merge(match, newval);
  else
    arr.push(newval);    
};

_.mixin({ '$update': update });

var arr = [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];

_.$update(arr, {id:1}, {id:1, name: "New Val"});


document.write(JSON.stringify(arr));

更快的解决方案 正如@dfsq所指出的那样,以下方法速度更快。

var upsert = function (arr, key, newval) {
    var match = _.find(arr, key);
    if(match){
        var index = _.indexOf(arr, _.find(arr, key));
        arr.splice(index, 1, newval);
    } else {
        arr.push(newval);
    }
};

7
我认为你可以在“更快的解决方案”中的第4行使用match作为_.indexOf的第二个参数,而不需要在那里重新计算该值,这应该会使事情变得更快一些。 - davertron
2
更快的方法:使用_.findIndex进行匹配。 - Julian K
2
只是为了扩展@JulianK和@davertron所说的,使用_.findIndex而不是_.find将让您放弃第二个_.find_.indexOf。当你只需要1次迭代数组时,你正在迭代3次。 - Justin Morgan
16个回答

236
在你的情况下,你只需要在数组中找到对象并使用Array.prototype.splice()方法,关于更多细节请点击这里:

var arr = [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];

// Find item index using _.findIndex (thanks @AJ Richardson for comment)
var index = _.findIndex(arr, {id: 1});

// Replace item at index using native splice
arr.splice(index, 1, {id: 100, name: 'New object.'});

// "console.log" result
document.write(JSON.stringify( arr ));
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>


1
好的,基于性能考虑,你的解决方案会更耗费资源,因为 indexOf 函数速度非常快(它将使用本地浏览器的 Array.prototype.indexOf 方法)。但无论如何,很高兴你找到了适合你的解决方案。 - dfsq
15
为什么不使用_.findIndex?这样就不需要使用_.indexOf了。 - AJ Richardson
实际上使用 find 而不是 findIndex 更有意义。在大多数情况下,当您想要更新对象时,您只想更新一个或两个属性。当只有一个或两个属性应该被更新时,即时重新创建对象是没有意义的。尽管如此,我并不是说您应该使用 indexOf。我想说的是其他答案更好。 - ataravati

66

看起来最简单的解决方案是使用ES6的.map或lodash的_.map

var arr = [{id: 1, name: "Person 1"}, {id: 2, name: "Person 2"}];

// lodash
var newArr = _.map(arr, function(a) {
  return a.id === 1 ? {id: 1, name: "Person New Name"} : a;
});

// ES6
var newArr = arr.map(function(a) {
  return a.id === 1 ? {id: 1, name: "Person New Name"} : a;
});

这会产生一个很好的效果,避免改变原始数组。


11
但是每次你都在创建一个新数组... 值得注意。 - kboom
11
不创建一个新数组的唯一选择是改变现有的数组,但这样做会对性能产生影响。而创建一个新数组可能不会对性能产生影响。点赞。 - Aurelio
有时候你不想改变现有的数组,而是创建一个新的数组。 - Envil

41

[ES6] 这段代码对我有效。

let result = array.map(item => item.id === updatedItem.id ? updatedItem : item)

1
  1. 你正在创建一个新的数组实例,因此它并不是真正的“替换”一个项目。
  2. 如果数组中没有包含具有相同“id”的项目,则会丢失你的updatedItem
- evilive
这是“更新”而不是“upsert”的解决方案(问题是“lodash中是否有替换匹配项的函数”),是的,它会创建数组的副本,因此如果您需要使用相同的数组,请不要使用它(我没有)。 - shebik

21
function findAndReplace(arr, find, replace) {
  let i;
  for(i=0; i < arr.length && arr[i].id != find.id; i++) {}
  i < arr.length ? arr[i] = replace : arr.push(replace);
}

现在,让我们测试所有方法的性能:

// TC's first approach
function first(arr, a, b) {
  _.each(arr, function (x, idx) {
    if (x.id === a.id) {
      arr[idx] = b;
      return false;
    }
  });
}

// solution with merge
function second(arr, a, b) {
  const match = _.find(arr, a);
  if (match) {
    _.merge(match, b);
  } else {
    arr.push(b);
  }
}

// most voted solution
function third(arr, a, b) {
  const match = _.find(arr, a);
  if (match) {
    var index = _.indexOf(arr, _.find(arr, a));
    arr.splice(index, 1, b);
  } else {
    arr.push(b);
  }
}

// my approach
function fourth(arr, a, b){
  let l;
  for(l=0; l < arr.length && arr[l].id != a.id; l++) {}
  l < arr.length ? arr[l] = b : arr.push(b);
}

function test(fn, times, el) {
  const arr = [], size = 250;
  for (let i = 0; i < size; i++) {
    arr[i] = {id: i, name: `name_${i}`, test: "test"};
  }

  let start = Date.now();
  _.times(times, () => {
    const id = Math.round(Math.random() * size);
    const a = {id};
    const b = {id, name: `${id}_name`};
    fn(arr, a, b);
  });
  el.innerHTML = Date.now() - start;
}

test(first, 1e5, document.getElementById("first"));
test(second, 1e5, document.getElementById("second"));
test(third, 1e5, document.getElementById("third"));
test(fourth, 1e5, document.getElementById("fourth"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>
<div>
  <ol>
    <li><b id="first"></b> ms [TC's first approach]</li>
    <li><b id="second"></b> ms [solution with merge]</li>
    <li><b id="third"></b> ms [most voted solution]</li>
    <li><b id="fourth"></b> ms [my approach]</li>
  </ol>
<div>


7
因为对人持消极态度("这是什么玩笑啊")会让他们不想学习,所以被踩了。想象一下,如果我以“作为聪明人,我期望你不要懒惰于控制情绪并考虑此事”结束会怎样。 - Aditya M P
6
我不想伤害任何人,但我想知道为什么比原问题提出者的方法更糟糕的解决方案会得到这么多票。给那个答案投票的人遵循哪些规则?我感到失望的是人们盲目相信得票最多的答案,没有批判性思维。 - evilive
1
@evilive 说得有道理,但我不明白为什么这些要求你表现得好像之前提供答案/投票的人都是白痴。这个答案的事实部分很棒,但其余部分带有一种几乎无法控制的优越感。这对任何人都没有帮助。您可以轻松地在没有过度情绪反应的情况下阐述观点。 - Vala
2
值得注意的是,您的解决方案和TC的解决方案仅通过ID进行过滤。这是这两个解决方案运行更快的第一个原因。另外两个解决方案允许您传递对象的任何部分以供过滤,这可能更适合作为upsert函数。 - Aram

15

如果你只想替换一个属性,lodash的_.find_.set应该足够了:

var arr = [{id: 1, name: "Person 1"}, {id: 2, name: "Person 2"}];

_.set(_.find(arr, {id: 1}), 'name', 'New Person');

这是最好的,因为我们中的一些人只想更新一个键值。这个可以做到。谢谢。 - Amir Hassan Azimi

12

您也可以使用findIndex和pick来达到相同的结果:

  var arr  = [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];
  var data = {id: 2, name: 'Person 2 (updated)'};
  var index = _.findIndex(arr, _.pick(data, 'id'));
  if( index !== -1) {
    arr.splice(index, 1, data);
  } else {
    arr.push(data);
  }

8
随着时间的推移,你应该采用更具有功能性的方法,避免数据变异并编写小型、单一职责函数。通过ECMAScript 6标准,你可以使用提供的mapfilterreduce方法在JavaScript中享受函数式编程范例。你不需要另外使用lodash、underscore或者其它东西来完成大多数基本操作。
下面我列出了一些解决这个问题的建议解决方案,以展示如何使用不同的语言特性来解决这个问题:

Using ES6 map:

const replace = predicate => replacement => element =>
  predicate(element) ? replacement : element
 
const arr = [ { id: 1, name: "Person 1" }, { id:2, name:"Person 2" } ];
const predicate = element => element.id === 1
const replacement = { id: 100, name: 'New object.' }

const result = arr.map(replace (predicate) (replacement))
console.log(result)


递归版本 - 等价于映射(mapping):

需要使用解构赋值数组展开运算符

const replace = predicate => replacement =>
{
  const traverse = ([head, ...tail]) =>
    head
    ? [predicate(head) ? replacement : head, ...tail]
    : []
  return traverse
}
 
const arr = [ { id: 1, name: "Person 1" }, { id:2, name:"Person 2" } ];
const predicate = element => element.id === 1
const replacement = { id: 100, name: 'New object.' }

const result = replace (predicate) (replacement) (arr)
console.log(result)

当最终数组的顺序不重要时,您可以使用一个对象作为哈希表数据结构。如果您已经有了以对象为键的集合,那么这将非常方便 - 否则您必须先更改表示形式。 需要对象剩余扩展运算符计算属性名Object.entries

const replace = key => ({id, ...values}) => hashMap =>
({
  ...hashMap,       //original HashMap
  [key]: undefined, //delete the replaced value
  [id]: values      //assign replacement
})

// HashMap <-> array conversion
const toHashMapById = array =>
  array.reduce(
    (acc, { id, ...values }) => 
    ({ ...acc, [id]: values })
  , {})
  
const toArrayById = hashMap =>
  Object.entries(hashMap)
  .filter( // filter out undefined values
    ([_, value]) => value 
  ) 
  .map(
    ([id, values]) => ({ id, ...values })
  )

const arr = [ { id: 1, name: "Person 1" }, { id:2, name:"Person 2" } ];
const replaceKey = 1
const replacement = { id: 100, name: 'New object.' }

// Create a HashMap from the array, treating id properties as keys
const hashMap = toHashMapById(arr)
console.log(hashMap)

// Result of replacement - notice an undefined value for replaced key
const resultHashMap = replace (replaceKey) (replacement) (hashMap)
console.log(resultHashMap)

// Final result of conversion from the HashMap to an array
const result = toArrayById (resultHashMap)
console.log(result)


5

我也遇到了这个问题,我就简单地这样解决了。

const persons = [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];
const updatedPerson = {id: 1, name: "new Person Name"}
const updatedPersons = persons.map(person => (
  person.id === updated.id
    ? updatedPerson
    : person
))

如果需要的话,我们可以将其概括。
const replaceWhere = (list, predicate, replacement) => {
  return list.map(item => predicate(item) ? replacement : item)
}

replaceWhere(persons, person => person.id === updatedPerson.id, updatedPerson)

3
使用lodash unionWith函数,您可以对对象进行简单的upsert操作。文档说明如果存在匹配项,则将使用第一个数组。将更新后的对象用[ ](数组)包装,并将其放置在联合函数的第一个数组中。只需指定匹配逻辑,如果找到它,它将替换它,如果没有找到它,它将添加它。
示例:
let contacts = [
     {type: 'email', desc: 'work', primary: true, value: 'email prim'}, 
     {type: 'phone', desc: 'cell', primary: true, value:'phone prim'},
     {type: 'phone', desc: 'cell', primary: false,value:'phone secondary'},
     {type: 'email', desc: 'cell', primary: false,value:'email secondary'}
]

// Update contacts because found a match
_.unionWith([{type: 'email', desc: 'work', primary: true, value: 'email updated'}], contacts, (l, r) => l.type == r.type && l.primary == r.primary)

// Add to contacts - no match found
_.unionWith([{type: 'fax', desc: 'work', primary: true, value: 'fax added'}], contacts, (l, r) => l.type == r.type && l.primary == r.primary)

3
var arr= [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];
var index = _.findIndex(arr, {id: 1});
arr[index] = {id: 100, name: 'xyz'}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接