JS笔试题:实现lodash的get方法

336 阅读2分钟

FD2731E138E71E78E1F169D5327AF527.webp
最近题主遇到了一个业务需求,需要从多个多层对象结构(即formilyJSON Schema)中查找一个已确定属性名但是不确定层级的一个数组值,由业务端提供属性的层级路径。这个需求跟lodash的get方法不能说毫无关系,只能说是一模一样。

lodash官方解释:

_.get(object, path, [defaultValue])

根据 object对象的path路径获取值。 如果解析 value 是 undefined 会以 defaultValue 取代。

参数
  1. object  (Object) : 要检索的对象。
  2. path  (Array|string) : 要获取属性的路径。
  3. [defaultValue]  ()* : 如果解析值是 undefined ,这值会被返回。
返回

()* : 返回解析的值。

例子
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'

为了不在已经过于臃肿的依赖结构里添加lodash拖慢上线速度,决定手写该函数在项目中引用。 由于实现逻辑集中于第二个参数,这里我就直接以lodash的get中的第二个参数入手进入思考。 例子中表明第二个参数会有两种形式

  • 字符
    不做直接处理,转数组
  • 数组
    对数组进行循环,使用reduce函数
const object = { 'a': [{ 'b': { 'c': 3 } }] };

const myGet = (object, path, defaultValue) => {
    if (typeof path === 'string') {
        const arrPath = path.split(',').reduce((total, currentValue) => {
            if (currentValue.includes('[') && currentValue.includes(']')) {
                const startIndex = currentValue.indexOf('[');
                const endIndex = currentValue.indexOf(']');
                return [
                    ...total,
                    currentValue.substring(0, startIndex),
                    currentValue.substring(startIndex + 1, endIndex)
                ];
            }
            return [...total, currentValue];
        }, []);
        return myGet(object, path.split(','))
    }
    
    const result = path.reduce((total, currentValue, currentIndex) => {
        if (currentIndex <= 0) {
            return object[currentValue];
        }
        if (total === undefined) {
            return undefined;
        }
        return total[currentValue];
    });
    return result || defaultValue;
}

myGet(object, 'a[0].b.c');
// => 3
 
myGet(object, ['a', '0', 'b', 'c']);
// => 3
 
myGet(object, 'a.b.c', 'default');
// => 'default'

写完后我发现还是不太满意,主要在于reduce循环不能停止,即使在判断获取不到值的情况下仍然会继续运行。其次就是遇到[]时需要用循环处理进行判断仍然不是性能的最优解。 针对这两种情况,我打算用for循环代替reduce,用字符串原型上的replace结合正则表达式,同时添加对意外情况的拦截。

const myGet = (object, path, defaultValue) => {
    if (!Array.isArray(path) && typeof path !== 'string') {
        throw new Error('传入的path参数类型不正确');
    }
    if (Array.isArray(path)) {
        return myGet(object, path.join('.'), defaultValue);
    }
    const paths = path.replace(/\[(\d+)\]/g, ".$1").split(".");
    let result = object;
    for (const [index, p] of Object.entries(paths)) {
        result = Object(result)[p];
        if (
            (result === undefined)
            || (Number(index) !== paths.length - 1 && typeof result !== 'object')
        ) {
          return defaultValue;
        }
    }
    return result;
}

这里面有一个小点需要注意,因为习惯问题我在遍历的时候会使用for...of,在同时需要获得key和value值的情况下需要将待处理值转换为类Map结构即[['a', '我是a'], ['b', '我是b']],转换成该结构所使用Object.entries是属于对象原型方法,而在对象中属性名只能为字符串,故Object.entries转换后的数组的索引都会变成字符串,entries内部石油Object.keys上再封装成的,故keys方法同理。