前言
最近小伙伴遇到了取类似如下结构值的问题
// 例如ajax的返回值 res =
{
gk: {
r8: {
rs: {
resp:true
}
}
}
}
需要取到resp的值,其中任何一层都可能没有,那么传统的判断方法要写一串
if(gk && gk.r8 && gk.r8.rs){
// ...
}
写一个,还勉强能忍,但是如果和r8平级有好几个,代码看起来真的很不优雅...
下面找到了几个解决方法,简单记一下:
(2019.2.21更新 可选运算符)
可选运算符
目前处于stage-2阶段,是一个很有意思的新特性。值得期待
语法
obj?.prop // optional static property access
obj?.[expr] // optional dynamic property access
func?.(...args) // optional function or method call
原理
a?.b // undefined if `a` is null/undefined, `a.b` otherwise.
a == null ? undefined : a.b
a?.[x] // undefined if `a` is null/undefined, `a[x]` otherwise.
a == null ? undefined : a[x]
a?.b() // undefined if `a` is null/undefined
a == null ? undefined : a.b() // throws a TypeError if `a.b` is not a function
// otherwise, evaluates to `a.b()`
a?.() // undefined if `a` is null/undefined
a == null ? undefined : a() // throws a TypeError if `a` is neither null/undefined, nor a function
// invokes the function `a` otherwise
在这里不说太多了,有兴趣的可以去 TC39 / proposal-optional-chaining 查看
try...catch
这个方法最简单粗暴,简单的封装一个方法
function safeProps(fn, defaultVal) {
try {
return fn();
} catch (e) {
return defaultVal;
}
}
safeProps(() => res.gk.r8.rs.resp, -1);
lodash _.get
doc
使用方法
var object = { 'a': [{ 'b': { 'c': 3 } }] };
_.get(object, 'a[0].b.c');
// => 3
_.get(object, ['a', '0', 'b', 'c']);
// => 3
_.get(object, 'a.b.c', 'default');
// => 'default'
// 对于我们的例子
let b = _.get(res,'gk.r8.rs.resp')
调试一下源码,原理很简单 baseGet
是实现这个功能的核心代码。
它通过castPath
方法,把传入的字符串转为数组,然后在下面的while循环中一层层处理下去即可。
var castPath = require('./_castPath'),
toKey = require('./_toKey');
/**
* The base implementation of `_.get` without support for default values.
*
* @private
* @param {Object} object The object to query.
* @param {Array|string} path The path of the property to get.
* @returns {*} Returns the resolved value.
*/
function baseGet(object, path) {
path = castPath(path, object);
var index = 0,
length = path.length;
while (object != null && index < length) {
object = object[toKey(path[index++])];
}
return (index && index == length) ? object : undefined;
}
例如我们的例子:
let b = _.get(res,'gk.r8.rs.resp')
// castPath的返回值为 `["gk", "r8", "rs", "resp"]
下面直接一个while循环就把后面的取值搞定了。
如果想看他的实现原理
/**
* Casts `value` to a path array if it's not one.
*
* @private
* @param {*} value The value to inspect.
* @param {Object} [object] The object to query keys on.
* @returns {Array} Returns the cast property path array.
*/
function castPath(value, object) {
if (isArray(value)) {
return value;
}
return isKey(value, object) ? [value] : stringToPath(toString(value));
}
isKey
方法判断value是不是直接是object的属性,例如我们传入的'gk.r8.rs.resp'
,当然不算是value的key,于是调用了他内部的另一个方法 stringToPath
把传入的字符串转为数组["gk", "r8", "rs", "resp"]
。
stringToPath
实现较为复杂,例如当我们传入'gk[0].r8.rs.resp'
时,他会解析出数组["gk", "0", "r8", "rs", "resp"]
从而实现更复杂的解析。
如果有兴趣的话,源码如下,其中memoizeCapped
是其内部实现缓存的一个方法,可忽略。
var memoizeCapped = require('./_memoizeCapped');
/** Used to match property names within property paths. */
var rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;
/** Used to match backslashes in property paths. */
var reEscapeChar = /\\(\\)?/g;
/**
* Converts `string` to a property path array.
*
* @private
* @param {string} string The string to convert.
* @returns {Array} Returns the property path array.
*/
var stringToPath = memoizeCapped(function(string) {
var result = [];
if (string.charCodeAt(0) === 46 /* . */) {
result.push('');
}
string.replace(rePropName, function(match, number, quote, subString) {
result.push(quote ? subString.replace(reEscapeChar, '$1') : (number || match));
});
return result;
});
利用reduce
其实实际中的情况可能并不会出现需要解析类似'gk[0].r8.rs.resp'
这样对象&数组一起深层嵌套的结构,那么我们可以自己实现一个和lodash.get类似的简单的方法。
从前面的解析可以看到,我们实际上就是变化字符串为数组,然后循环解析下去。如果熟悉reduce
的使用(不太熟可以看我之前的文章【转载】Array.prototype.reduce 实用指南)
仔细看下下面的方法会感觉.... 看起来很吊!哈哈
const get = (path, o) => path.reduce((prev, cur)=> (prev && prev[cur]) ? prev[cur]:null , o);
//use
get(["gk", "r8", "rs", "resp"], res);
以上方法可以curry化,更方便,path总不能每次都写一次吧
const get = path => obj => path.reduce((prev, cur)=> (prev && prev[cur]) ? prev[cur]:null , o);
const getStr = get(['list','item','string']);
//固化参数,会方便很多
getStr(obj1)
getStr(obj2)
ES6 Proxy
/**
* @param target
* @param exec 取值属性
* @returns {*}
*/
function getter(target, exec = '_') {
return new Proxy({}, {
get: (o, n) => {
return n === exec ?
target :
getter(typeof target === 'undefined' ? target : target[n], exec)
}
});
}
console.log(getter(res).gk.r8.rs.resp._) // true
参考链接
js对象如何优雅的取一个深度的值?
前端中的Robust
immutable getIn
lodash get
三个值得期待的JavaScript新特性!