Object.assign - 覆盖嵌套属性

81

我有一个像这样的对象 a:

const a = {
  user: {
   …
   groups: […]
   …
  }
}

因此,a.user 中有更多的属性。

我只想更改a.user.groups的值。如果我这样做:

const b = Object.assign({}, a, {
  user: {
    groups: {}
  }
});

b除了b.user.groups之外没有其他属性,所有其他属性都被删除了。是否有ES6的方法可以使用Object.assign仅更改嵌套属性而不失去所有其他属性?


7
在我的看法中,它似乎正在执行 Object.assign 应该完成的操作。它使用提供的新对象更改了 user 属性。如果这是您想要修改的唯一内容,为什么不直接执行 b.user.groups = /* value */ - taguenizy
你可能会对assign-deepobject-assign-deep包感兴趣。 - Gras Double
参考此问题的相关内容:https://dev59.com/p14c5IYBdhLWcg3wa52M - Gras Double
11个回答

146

尝试了一些方法之后,我找到了一个看起来非常不错的解决方案:

const b = Object.assign({}, a, {
  user: {
    ...a.user,
    groups: 'some changed value'
  }
});

为了让答案更完整,这里有一个小提示:
const b = Object.assign({}, a)

本质上与以下代码相同:

const b = { ...a }

因为它只是将a (...a)的所有属性复制到一个新对象中。所以,上面的代码可以改写成:

 const b = {
   ...a,          //copy everything from a
   user: {        //override the user property
      ...a.user,  //same sane: copy the everything from a.user
      groups: 'some changes value'  //override a.user.group
   }
 }

6
我点赞的原因是解释得好,同时运用了语法糖 :) 顺便说一下,如果你想返回一个新元素被添加后的数组,基本上是同样的机制:[ ...someArray, newElement ]。 - Alex
我必须说,这正是我所寻找的,而且它完美地完成了任务。 - Shahrukh Anwar
Object.assign和spread运算符并不完全相同。Spread运算符不会复制原型,而Object.assign会。 - jonybekov

16
这里有一个名为Object_assign的小函数(如果需要进行嵌套赋值,只需将.替换为_)。
该函数通过直接粘贴源值或在目标值和源值都是非null对象时递归调用Object_assign来设置所有目标值。
const target = {
  a: { x: 0 },
  b: { y: { m: 0, n: 1      } },
  c: { z: { i: 0, j: 1      } },
  d: null
}

const source1 = {
  a: {},
  b: { y: {       n: 0      } },
  e: null
}

const source2 = {
  c: { z: {            k: 2 } },
  d: {}
}

function Object_assign (target, ...sources) {
  sources.forEach(source => {
    Object.keys(source).forEach(key => {
      const s_val = source[key]
      const t_val = target[key]
      target[key] = t_val && s_val && typeof t_val === 'object' && typeof s_val === 'object'
                  ? Object_assign(t_val, s_val)
                  : s_val
    })
  })
  return target
}

console.log(Object_assign(Object.create(target), source1, source2))


它像魔法一样运作。我的情况是这样的:我在根对象的嵌套对象上设置了一个自定义属性。这个属性仅存在于前端,用于图形增强目的。然后,当我执行来自根对象的操作调用后端时,我使用Object.assign将我的根对象与服务器发送的数据进行刷新。不幸的是,我的嵌套对象上的自定义属性被“删除”了。感谢您的函数,现在不再是这种情况。 - stephane brun
嗯,我发现一个使用案例完全混乱了你的算法:我的后端处理一个列表,其中项目按“creationDate desc”排序。因此,当我从前端请求后端将新元素添加到我的列表中时,当服务器发送数据回给我时,之前在位置“0”索引的项目现在在位置“1”索引(以此类推...)。由于这种索引移位,您的算法破坏了我的所有数据模型:( - stephane brun
@stephanebrun 哎呀。你不应该假设对象字段保持它们的顺序。是的,它们的顺序是确定的,但没有“干净/最小”的方法来保证它。如果您需要逻辑上索引值,请将键设置为其索引。其他选项包括使用数组或映射。我现在能想到的最快(不太干净)的方法是,随着它们的到来缓存所有键,然后根据那个顺序创建结果对象。 - Gust van de Wal
是的,我认为使用它们的键来索引对象将解决问题。无论如何,你的算法很棒,我会保留它的。只是我有一个奇怪的情况,我正在尝试找到适用于所有情况的解决方案。但是我认为这(轻易地)是不可能的。感谢你的时间! - stephane brun

8

你特别要求使用ES6中的Object.assign方法,但是也许其他人更喜欢我的“通用”答案-你可以轻松地使用lodash实现,个人认为这个解决方案更易读。

import * as _ from 'lodash';
_.set(a, 'user.groups', newGroupsValue);

它会改变对象。

1
当然了,这就是我假定你不会喜欢使用我的解决方案,但是其他遇到类似问题的人可能会觉得有用。 - Patryk Wlaź

5
你可以这样更改:
const b = Object.assign({}, a, {
  user: Object.assign({}, a.user, {
          groups: {}
        })
});

12
不要改变 b.user.groups,因为 user 在 b 和 a 之间是共享的。 - borovsky
这里是链接,指向描述@borovsky提到的内容的文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Deep_Clone - Dillon Ryan Redding

4

下面是更通用的解决方案。它仅在存在冲突(相同的键名)时才进行递归赋值。它解决了分配具有许多引用点的复杂对象的问题。

例如,在npm中用于嵌套对象分配的现有软件包nested-object-assign,由于它递归地遍历所有键,因此卡在了复杂对象分配上。

var objecttarget = {
  a: 'somestring',
  b: {x:2,y:{k:1,l:1},z:{j:3,l:4}},
  c: false,
  targetonly: true
};

var objectsource = {
  a: 'somestring else',
  b: {k:1,x:2,y:{k:2,p:1},z:{}},
  c: true,
  sourceonly: true
};

function nestedassign(target, source) {
  Object.keys(source).forEach(sourcekey=>{
    if (Object.keys(source).find(targetkey=>targetkey===sourcekey) !== undefined && typeof source[sourcekey] === "object") {
      target[sourcekey]=nestedassign(target[sourcekey],source[sourcekey]);
    } else {
      target[sourcekey]=source[sourcekey];
    }
  });
  return target;
}

// It will lose objecttarget.b.y.l and objecttarget.b.z
console.log(Object.assign(Object.create(objecttarget),objectsource));

// Improved object assign
console.log(nestedassign(Object.create(objecttarget),objectsource));


很好,只需要修改这行代码:if (Object.keys( source ).find( targetkey =>...改为:if (Object.keys( target ).find( targetkey => - sicko
@sicko 感谢您的反馈,但是您是错误的。如果您像那样修改代码,您将丢失仅存在于源代码中的值。结果是基于目标的。这就是为什么迭代是基于源代码的,以便将它们添加到目标中,即使在目标中不存在。我在片段中添加了objecttarget.targetonly和objectsource.sourceonly. 尝试按照您所指出的方法更改代码。您会发现结果中没有sourceonly键和值。 - Won
请查看我的答案以获取正确的NestedObjectAssign()函数:https://dev59.com/CVgR5IYBdhLWcg3wNLKC#71174859。 如果我使用您的函数和我的参数(创建以真正测试该函数),那么您的函数会抛出错误。 顺便说一下,您if语句的第一部分将始终为true: Object.keys(source).forEach(sourcekey=>{ if (Object.keys(source).find(targetkey=>targetkey===sourcekey) !== undefined - sicko

4

Object.assign()方法用于将一个或多个源对象的所有可枚举自身属性的值复制到目标对象中。它将返回目标对象。

assign方法将从源对象复制并创建一个新对象,如果您稍后更改源对象中的任何方法,新对象将不会反映这些更改。

请使用create()方法

Object.create()方法使用指定的原型对象和属性创建一个新对象。

const b = Object.create(a)
b.user.groups = {}
// if you don't want the prototype link add this
// b.prototype = Object.prototype 

通过原型将b链接到a,如果您对a进行任何更改,它将反映在b中,而对b进行的任何更改都不会影响a。


这确实是一个有趣的观点,但在这种情况下,我需要创建源代码的副本。 - philipp
那么你自己的答案就是你正在寻找的。 - Abdullah Alsigar
或者使用 create() 然后执行。b.prototype = Object.prototype。 - Abdullah Alsigar

2

一个可以支持嵌套数组的工作版本

const target = {
   var1: "...",
   var2: {
       nestVar1: "...",
       nestVar2: [...]   
       }
}

const source = {
  var2: {
      nestVar3: { ... }
      nestVar4: "...",
      nestVar5: [...]
      },
  var3: "...",
  var4: [...]
}

功能:

function objectAssign(target, source) {
    Object.keys(source).forEach(sourceKey => {
      if(Object.keys(source).find(targetKey => targetKey === sourceKey) !== undefined && typeof source[sourceKey] === "object") {
        target[sourceKey] = objectAssign(target[sourceKey], source[sourceKey]);
      } else if(!Array.isArray(source)) {
        target[sourceKey] = source[sourceKey];
      } else {
        target = source;
      }
    });
    return target;
}

那么

let newObj = objectAssign({}, target, source);

2
菲利普斯的第一个答案需要微调。
const object1 = {
  abc: {
    a: 1
  }
};

const object2 = {
  abc: {
    b: 2
  }
};

Object.assign(object1, {
    abc: {
        ...object1.abc,
        ...object2.abc
    }
});

console.log(object1);
// Object { abc: Object { a: 1, b: 2 } }

1
这是我的NestedObjectAssign()贡献,因为我看到一个函数在帖子中被复制,但它并不好用。
这个版本就像例子中展示的那样正常工作。

function NestedObjectAssign(target, source) 
{
    Object.keys(source).forEach(sourceKey => 
    {
        if (typeof source[sourceKey] === "object") 
        {
            target[sourceKey] = NestedObjectAssign(target[sourceKey] || {}, source[sourceKey])
        }
        else 
        {
            target[sourceKey] = source[sourceKey]
        }
    })

    return target
}

var target = 
{
    stringTarget: "target",
    stringCommon: "targetCommon",
    nestedCommon: 
    {
        target: 1,
        common: 1,
        nestedTarget: 
        {
            target: 1,
            common: 1
        }
    }
}

var source = 
{
    stringSource: "source",
    stringCommon: "sourceCommon",
    nestedCommon: 
    {
        source: 2,
        common: 3,
        nestedSource: 
        {
            source: 2,
            common: 3
        }
    }
}

var t = NestedObjectAssign({}, target)
var s = NestedObjectAssign({}, source)
var targetSource = NestedObjectAssign(t, s)

console.log({ target })
console.log({ source })
console.log({ targetSource })


0

我知道这是一个老问题,也有很多答案,但对于将来会看到这个问题的开发人员来说,有一个更简单的解决方案:我们需要将自定义对象转换为JSON对象。

解决方案:

JSON.parse(JSON.stringify(YourCustomObjectHere));

这将把您的自定义对象转换为JSON,然后重新解析为JSON对象。

希望这个答案能帮助所有遇到这个问题的开发人员,谢谢。


1
这只是一个“穷人版的深度克隆”,完全没有考虑性能,只有当你传入的对象完全可序列化为json时才能按预期工作。这不包括函数、类、日期、利用原型链的类实例等等。 - philipp
谢谢,我刚刚注意到我只需要将它存储在Firebase Firestore中,因此我没有注意到在从JSON字符串解析后,这些函数没有分配给类。 - ABDO-AR

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