获取嵌套对象属性的多种方法

6,434 阅读6分钟

引子

获取一个深层次的对象,对大家来说都是家常便饭的事情了。像自己封装的、三方库的以及 es2020提供的,总之和大家分享探讨一下

取用户的所在的城市地址

const user = {
 name: 'xxx',
 address: {
  city: 'hangzhou'
 }
};

最直接的

const ctiy = user && user.address && user.address.city;
// or
const city = !user ? undefined : !user.address ? undefined : user.address.city;

当对象每增加一行的时候,判断就得多需要一层 在我明确知道路径的情况下 user.name.address.city, 能不能自己写个解析的函数去获取值呢

手动实现版

var user = {
 name: 'xxx',
 address: {
  city: 'hangzhou'
 }
};
const str = 'address.city';
const get = (obj, path, defaultValue) => {
 let result = obj, i = 0;
 const paths = path.split('.');
 while (i < paths.length) {
        result = Object(result)[paths[i]];
  if (result === undefined) {
   return defaultValue;
        }
        i++
 }
 return result;
};
console.log(get(user, str), '--');
// hangzhou

我们这里其实每考虑数组下标的情况,要加上也简单只需要加上一行就行

优化版

var user = {
 name: 'xxx',
 address: {
  cities: [{city: 'hangzhou'}]
 }
};
const str = 'address.cities[0].city';
const get = (obj, path, defaultValue) => {
    let result = obj, i = 0;
    //新增这一行————————————————————————————
    const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.')
    console.log(paths, '--')
 while (i < paths.length) {
  result = Object(result)[paths[i]];
  if (result === undefined) {
   return defaultValue;
  }
  i++;
 }
 return result;
};
console.log(get(user, str), '--');
// 杭州

大佬一行版

var mb=p=>o=>p.map(c=>o=(o||{})[c])&&o

有个大佬写了一个 mb.js,其实就一行通过数组的方式来实现 get

var getHello = mb(["a""b"0"hello"]);
var getHelloLength = mb(["a""b"0"hello""length"]);

var obj1 = {
  a: {
    b: [{ hello: "world" }]
  }
};
var obj2 = {
  c: {
    d: "e"
  }
};
getHello(obj1); // world
getHelloLength(obj1); // 5

getHello(obj2); // undefined
getHelloLength(obj2); // undefined

上面这么多骚操作,只要你定义好自己的解析器都可以实现

lodash/get

const city  = get(user, 'address.cities[0].city'))

让我们瞅瞅源码 ./get.js

import baseGet from './.internal/baseGet.js'
function get(object, path, defaultValue) {
  // 过程比较简单,对undefined值判断取默认值defaultValue,所有取值逻辑都在`baseGet`模块内。
  const result = object == null ? undefined : baseGet(object, path)
  return result === undefined ? defaultValue : result
}

export default get

看看 baseGet.js

import castPath from './castPath.js'
import toKey from './toKey.js'
function baseGet(object, path) {
  // 所有路径转为数组形式
  path = castPath(path, object)
  let index = 0
  const length = path.length
  while (object != null && index < length) {
    // 听名字就知道是转为 唯一的 key
    object = object[toKey(path[index++])]
  }
  // 判断一下 key 数组 
  return (index && index == length) ? object : undefined
}
export default baseGet
/*------------------------------*/
//分割线 castPath.js
import isKey from './isKey.js'
import stringToPath from './stringToPath.js'
function castPath(value, object) {
  if (Array.isArray(value)) {
    return value
  }
  // 转为key 数组
  return isKey(value, object) ? [value] : stringToPath(value)
}
export default castPath
// isKey.js
function isKey(value, object) {
  if (isArray(value)) { // 数组,直接返回false
    return false;
  }
  if (type == 'number' || type == 'symbol' || type == 'boolean' ||
      value == null || isSymbol(value)) {
    return true;
  }
  return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
    (object != null && value in Object(object));
}

看完以后是不是感觉 lodash 边界情况下考虑的特别好,比如这里 isKey(value, object) 用来判断value是object的key, 像我自己写的直接 object[value] 来判断了

Ramda

lodash 的 get 是一步到位,Ramda 就是打组合拳的形式,毕竟 ramda 是一个函数式编程库,让我们来看看实现

// 一步到位版本,这个没有默认值选项
const city = R.path(['address''cities'0'city'])(user)
// 带默认参数版
R.pathOr([], ['user''posts'0'comments'])
//组合
const city = R.compose(
    R.prop('city'),
    R.path(['address, cities'0,])(user)

类的方式

const user = {
    address: {
        city: 'hangzhou'
    }
}
class GetValue {
 constructor(val) {
  this.__val = val;
 }
 static set(val) {
  return new GetValue(val);
 }
 isNothing() {
  return this.__val === null || this.__val === undefined;
 }
 map(f) {
  return this.isNothing() ? new GetValue(null) : new GetValue(f(this.__val));
 }
}
const prop = key => obj => obj[key]
const city = GetValue.set(user).map(prop('address')).map(prop('city'))
console.log(city, 'city')
// GetValue { __val: 'hangzhou' } city

我们上面折腾了这么久,其实在 es2020 早就提供了一种新的方法帮我们做了

可选链操作符(Optional Chaining)

可选链 我们在查询多层对象时,不需要在各种判空

const city = address?.cities[0]?.city

当左边为空值时,就取右边的值,否则取左边的值。这里的空值指的是 null 和 undefined ,这是它跟假值不同的地方,也是使用空值运算符和逻辑或运算符的区别之处

兼容性

前置条件

  • nodejs v>= 14.0.0
  • TypeScript v>=3.7
  • babel 插件支持
// install
yarn add @babel/plugin-proposal-optional-chaining
 --dev //可选链
yarn add @babel/plugin-proposal-nullish-coalescing-operato --save //双问号
 // use 
 {
"plugins": ["@babel/,plugin-proposal-optional-chaining"],
"@babel/plugin-proposal-nullish-coalescing-operator"
}
  • 框架主要不支持 vue template

更多🌰

动态 key

 const obj = { a: { b'b' } }
 const name = 'b'
 const b = obj?.a?.[name]

三元

const city = user ? user.name : 'xxx' // 之前
const city = user ?.name ?? 'xxx' //之后

更多的用法大家自行挖掘~~ 用了以后我的代码就变成了各种问号

最后

虽然我们折腾了这么久,估计也就写爬虫的场景用的到比较深对象,要是工作中你遇到后端给你返回这么深的数据结构,该喷就喷,别整这些没用的