如何優雅地鏈式取值

灰風GreyWind發表於2019-02-28

開發中,鏈式取值是非常正常的操作,如:

res.data.goods.list[0].price
複製程式碼

但是對於這種操作報出類似於Uncaught TypeError: Cannot read property `goods` of undefined 這種錯誤也是再正常不過了,如果說是res資料是自己定義,那麼可控性會大一些,但是如果這些資料來自於不同端(如前後端),那麼這種資料對於我們來說我們都是不可控的,因此為了保證程式能夠正常執行下去,我們需要對此校驗:

if (res.data.goods.list[0] && res.data.goods.list[0].price) {
// your code
}
複製程式碼

如果再精細一點,對於所有都進行校驗的話,就會像這樣:

if (res && res.data && res.data.goods && res.data.goods.list && res.data.goods.list[0] && res.data.goods.list[0].price){
// your code
}
複製程式碼

不敢想象,如果資料的層級再深一點會怎樣,這種實現實在是非常不優雅,那麼如果優雅地來實現鏈式取值呢?

一、optional chaining

這是一個出於stage 2的ecma新語法,目前已經有了babel的外掛 babel-plugin-transform-optional-chaining,這種語法在swift中有,可以看下官方給的例項

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
複製程式碼

二、通過函式解析字串

我們可以通過函式解析字串來解決這個問題,這種實現就是lodash的 _.get 方法

var object = { a: [{ b: { c: 3 } }] };
var result = _.get(object, `a[0].b.c`, 1);
console.log(result);
// output: 3
複製程式碼

實現起來也非常簡單,只是簡單的字串解析而已:

function get (obj, props, def) {
    if((obj == null) || obj == null || typeof props !== `string`) return def;
    const temp = props.split(`.`);
    const fieldArr = [].concat(temp);
    temp.forEach((e, i) => {
        if(/^(w+)[(w+)]$/.test(e)) {
            const matchs = e.match(/^(w+)[(w+)]$/);
            const field1 = matchs[1];
            const field2 = matchs[2];
            const index = fieldArr.indexOf(e);
            fieldArr.splice(index, 1, field1, field2);
        }
    })
    return fieldArr.reduce((pre, cur) => {
        const target = pre[cur] || def;

        if(target instanceof Array) {
            return [].concat(target);
        }
        if(target instanceof Object) {
            return Object.assign({}, target)
        }
        return target;
    }, obj)
}
複製程式碼
var c = {a: {b : [1,2,3] }}
get(c ,`a.b`)     // [1,2,3]
get(c, `a.b[1]`)  // 2
get(c, `a.d`, 12)  // 12
複製程式碼

三、使用解構賦值

這個思路是來自github上 You-Dont-Need-Lodash-Underscore 這個倉庫,看到這個的時候真的佩服

const c = {a:{b: [1,2,3,4]}}

const { a: result } = c;
// result : {b: [1,2,3,4]}
const {a: { c: result = 12 }} = c
// result: 12
複製程式碼

當然,這個時候為了保證不報uncaught Typeerror,我們仍然需要定義預設值, 就像這樣, 貌似如果不加lint可讀性堪憂

const {a: {c: {d: result2} = {}}} = c
複製程式碼

四、使用Proxy

這個是組內同事提到的,一個簡單實現如下:

function pointer(obj, path = []) {
    return new Proxy(() => {}, {
        get (target, property) {
            return pointer(obj, path.concat(property))
        },
        apply (target, self, args) {
            let val = obj;
            let parent;
            for(let i = 0; i < path.length; i++) {
                if(val === null || val === undefined) break;
                parent = val;
                val = val[path[i]]    
            }
            if(val === null || val === undefined) {
                val = args[0]
            }
            return val;
        }
    })
}
複製程式碼

我們可以這樣使用:

let c = {a: {b: [1, ,2 ,3]}}

pointer(c).a();   // {b: [1,2,3]}

pointer(c).a.b(); // [1,2,3]

pointer(d).a.b.d(`default value`);  // default value
複製程式碼

這差不多就是心中所謂的優雅了。

綜上,在實際工作中,使用方法四會是最優雅,可讀性也非常強,但考慮到瀏覽器的話,可能方法二會更加常用,當然,如果你所要取的值層級不是太深,你組內的同事要嚴格的lint,方法三也不失為一種好的選擇。

相關文章