js array 添加对象_【技术干货第11期】JS嵌套对象非破坏性操作技巧(建议收藏!)

前言

上篇文章我们详细盘点了在JS中如何做非破坏性操作,但只涉及了扁平结构的对象,对于嵌套类型的对象并没有介绍。虽然原理一样,但对于深层次嵌套结构的对象做非破坏性操作有时候还是比较麻烦的,所以这次我们就单独拿出来一篇文章分析一下如何操作嵌套结构的对象。

嵌套结构

首先,我们先来定义一下什么叫"嵌套结构"的对象(nested object)。我们知道,JS的数据类型分为引用类型和值类型的,引用类型主要是ArrayObject两种,剩余的比如数字、字符串、布尔值等都属于值类型。嵌套类型就是指一个Object内的某个字段的值又是引用类型的,可以是Array或者Object。
比如如下结构:

var user = {
  id: 1,
  name: "Mike",
  personalInfo: {
    account: {
      phone: '18612345678',
      email: 'fake_email@gmail.com',
    },
    address: {
      province: 'beijing',
      district: 'haidian',
    },
  },
  hobby: ['reading', 'sports'],
}

在上面的对象中,user内部的personalInfo和hobby都是引用类型的,数据类型分别是Object和Array。而personalInfo又包含了account和address两个Object。所以它是一个3层嵌套的对象。
接下来我们就看一下如何操作嵌套结构的对象才是安全的。

总体原则

操作嵌套的对象和操作扁平的对象从原理上讲是一样的,使用的方式都是上篇文章中列出的那些。那么操作嵌套对象到底复杂在哪里呢?我觉得想要深刻理解嵌套对象的操作,首先要理解一个概念就是Shallow Copy(浅拷贝):Shallow Copy是指当我们复制一个对象时,对于引用类型的字段,我们复制的是引用,而不是引用的值。
举个简单的例子:

const obj = {
  // 内部的foo字段是引用类型的
	foo: {
    bar: 'bar'
  }
}
// 使用...操作符复制(Shallow Copy)obj对象,这里思考一下obj.foo有没有真的被复制?
const obj2 = { ...obj }
obj2.foo = 'foo2'
console.log(obj)  // { foo: { bar: 'bar2' } }
console.log(obj2) // { foo: { bar: 'bar2' } }

通过上边代码的执行结果我们可以看到,虽然我们通过...运算符复制了obj到obj2,但...只能做到Shallow Copy。obj.foo是引用类型的,那么我们通过...将obj浅拷贝到obj2之后,obj.foo和obj2.foo其实还是指向同一个地方。所以当我们修改obj.foo中的字段的时候,obj2.foo中对应的字段也被修改(因为他们本来就是同一个对象)。
那么我们操作嵌套对象的时候就要保证操作字段所在路径的所有字段得到复制。比如对于以上代码,我们可以这样做:

// 使用...操作符浅拷贝obj到obj2
// 注意:此时obj.foo和obj2.foo指向同一个地方
const obj2 = { ...obj }
// 然后,我们将obj.foo再次浅拷贝一份到obj2.foo,
// 这时,obj.foo和obj2.foo指向不同的对象了
obj2.foo = { ...obj.foo }

console.log(obj) // { foo: { bar: 'bar' } }
console.log(obj2) // { foo: { bar: 'bar2' } }

在上面的代码中,通过对嵌套的内部字段再次执行拷贝,保证了对新对象的修改不影响老的对象(因为这样使得他们的引用指向不同的对象)。
那么上边说的修改字段所在路径到底是啥意思呢?比如有以下对象:

const obj = {
  foo: {
    bar: {
      zac: 'zac',
    },
  },
  other: {
    inner: 'inner',
  }
}

如果我们要修改的字段是obj.foo.bar.zac,那么我们需要复制的就是obj、foo、bar三层,也就是从最上层一直到被修改字段所在层都需要复制。而其他字段不需要复制,比如上面中的other.inner字段我们如果不做修改,那么他们可以保持原来的引用不变。

这样说起来可能还是比较抽象,所以本文大部分内容还是通过代码的方式来展示,请重点注意看代码的注释部分,会对代码中比较重要的操作进行详细解释。另外为了节省篇幅,我们代码中都针对以下对象进行操作,下面示例代码中就不再重复列出:

const user = {
	id: 1,
  name: 'Mike',
  account: {
  	phone: '18612345678',
    email: 'fake_email@gmail.com',
  },
  personalInfo: {
    address: {
      province: 'beijing',
      district: 'haidian',
    },
    hobby: ['reading, sports'],
  },
}

示例

被操作对象为Array

  • Array中添加元素
// 假设我们要往user.personalInfo.hobby中增加一项。
const newUser = {
  // 使用...操作符浅拷贝原对象的内容
  // 此时,newUser中的所有引用类型的字段依然指向和user中一样的值
  ...user,
  // 由于我们需要修改personalInfo中的值,所以需要将personalInfo复制一遍
  // 并覆盖老的personalInfo引用的值
  personalInfo: {
    // 我们要修改personalInfo.hobby字段,所以把personalInfo浅拷贝一层
    ...user.personalInfo,
    // 使用非破坏性的方式增加字段,并覆盖老的hobby字段的引用
    hobby: [...user.personalInfo.hobby, 'movie'],
  },
}

console.log(user)
/* 输出:
 { id: 1,
  name: 'Mike',
  account: { phone: '18612345678', email: 'fake_email@gmail.com' },
  personalInfo:
   { address: { province: 'beijing', district: 'haidian' },
     hobby: [ 'reading', 'sports' ] } }
*/
console.log(newUser)
/* 输出:
{ id: 1,
  name: 'Mike',
  account: { phone: '18612345678', email: 'fake_email@gmail.com' },
  personalInfo:
   { address: { province: 'beijing', district: 'haidian' },
     hobby: [ 'reading', 'sports', 'movie' ] } }
*/
// 可以看到,newUser中的hobby增加了一项,老的对象user中的值(包括引用类型和简单类型)未遭到破坏
  • Array删除元素
// 假设我们要将user.personalInfo.hobby中的'movie删除
const newUser = {
  // 还是首先浅拷贝最外层字段
  ...user,
  // 需要修改personalInfo.hobby,所以需要将personalInfo这一层复制一遍
  personalInfo: {
    // 复制personalInfo
    ...user.personalInfo,
    // 非破坏性修改hobby
    hobby: user.personalInfo.hobby.filter(h => h !== 'movie')
  },
}

console.log(user)
/*输出:
 { id: 1,
  name: 'Mike',
  account: { phone: '18612345678', email: 'fake_email@gmail.com' },
  personalInfo:
   { address: { province: 'beijing', district: 'haidian' },
     hobby: [ 'reading', 'sports', 'movie' ] } }
*/
console.log(newUser)
/*输出:
{ id: 1,
  name: 'Mike',
  account: { phone: '18612345678', email: 'fake_email@gmail.com' },
  personalInfo:
   { address: { province: 'beijing', district: 'haidian' },
     hobby: [ 'reading', 'sports' ] } }
*/
  • Array替换元素
// 将hobby中的元素添加`[]`
// 修改方式同上
const newUser = {
  ...user,
  personalInfo: {
    ...user.personalInfo,
    // 使用map的方式替换数组元素
    hobby: user.personalInfo.hobby.map(h => '[' + h + ']')
  },
}

被操作对象为Object

  • 往Object中添加字段
// 往user.personalInfo.address中添加city字段
const newUser = {
  ...user,
  personalInfo: {
    ...user.personalInfo,
    address: {
      ...user.personalInfo.address,
      city: 'beijing',
    },
  },
}

console.log(user)
/*
{ id: 1,
  name: 'Mike',
  account: { phone: '18612345678', email: 'fake_email@gmail.com' },
  personalInfo:
   { address: { province: 'beijing', district: 'haidian' },
     hobby: [ 'reading', 'sports', 'movie' ] } }
*/

console.log(newUser)
/*
{ id: 1,
  name: 'Mike',
  account: { phone: '18612345678', email: 'fake_email@gmail.com' },
  personalInfo:
   { address:
      { province: 'beijing', district: 'haidian', city: 'beijing' } } }
*/
  • Object中删除字段
// 假设我们要删除user.personalInfo.address.district字段
const {  
	district,
  ...newAddress
} = user.personalInfo.address

const newUser = {
  ...user,
  personalInfo: {
    ...user.personalInfo,
    address: newAddress,
  },
}
console.log(user)
/*
输出:
{ id: 1,
  name: 'Mike',
  account: { phone: '18612345678', email: 'fake_email@gmail.com' },
  personalInfo:
   { address: { province: 'beijing', district: 'haidian' },
     hobby: [ 'reading', 'sports', 'movie' ] } }
*/

console.log(newUser)
/*
输出:
 { id: 1,
  name: 'Mike',
  account: { phone: '18612345678', email: 'fake_email@gmail.com' },
  personalInfo: { address: { province: 'beijing' } } }
*/
  • Object中替换字段
// 假设我们需要将user.personalInfo.address.district改为'chaoyang'
const newUser = {
  ...user,
  personalInfo: {
    ...user.personalInfo,
    address: {
      ...user.personalInfo.address,
      district: 'chaoyang'
    }
  }
}

console.log(user)
/*
输出:
{ id: 1,
  name: 'Mike',
  account: { phone: '18612345678', email: 'fake_email@gmail.com' },
  personalInfo:
   { address: { province: 'beijing', district: 'haidian' },
     hobby: [ 'reading', 'sports', 'movie' ] } }
*/
console.log(newUser)
/*
输出:
 { id: 1,
  name: 'Mike',
  account: { phone: '18612345678', email: 'fake_email@gmail.com' },
  personalInfo: { address: { province: 'beijing', district: 'chaoyang' } } }
*/

总结

本文在前一篇文章的基础上,总结了嵌套对象的非破坏性操作方式。如果对上一篇文章介绍的技巧都理解了,那么本篇文章是比较容易理解的。如果读文章理解起来比较吃力,建议将代码执行一遍,并且自己可以做一些修改再看一下执行结果,结合代码的执行结果再去看文字会理解更深刻。本文中的所有代码都经过验证,是可以执行的。

写文章不易,如果这篇文章帮助到了你,请帮忙关注和转发~ 谢谢

欢迎扫码关注公众号<前端时光机>:

cdbb0740d7e6a3286f2c78f1e241a396.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值