FE-Interview icon indicating copy to clipboard operation
FE-Interview copied to clipboard

第 14 题:实现 lodash 的_.get

Open lgwebdream opened this issue 5 years ago • 17 comments

欢迎在下方发表您的优质见解

lgwebdream avatar Jun 19 '20 12:06 lgwebdream

在 js 中经常会出现嵌套调用这种情况,如 a.b.c.d.e,但是这么写很容易抛出异常。你需要这么写 a && a.b && a.b.c && a.b.c.d && a.b.c.d.e,但是显得有些啰嗦与冗长了。特别是在 graphql 中,这种嵌套调用更是难以避免。 这时就需要一个 get 函数,使用 get(a, 'b.c.d.e') 简单清晰,并且容错性提高了很多。

1)代码实现

function get(source, path, defaultValue = undefined) {
  // a[3].b -> a.3.b -> [a,3,b]
 // path 中也可能是数组的路径,全部转化成 . 运算符并组成数组
  const paths = path.replace(/\[(\d+)\]/g, ".$1").split(".");
  let result = source;
  for (const p of paths) {
    // 注意 null 与 undefined 取属性会报错,所以使用 Object 包装一下。
    result = Object(result)[p];
    if (result == undefined) {
      return defaultValue;
    }
  }
  return result;
}
// 测试用例
console.log(get({ a: null }, "a.b.c", 3)); // output: 3
console.log(get({ a: undefined }, "a", 3)); // output: 3
console.log(get({ a: null }, "a", 3)); // output: 3
console.log(get({ a: [{ b: 1 }] }, "a[0].b", 3)); // output: 1

2)代码实现 不考虑数组的情况

const _get = (object, keys, val) => {
 return keys.split(/\./).reduce(
  (o, j)=>( (o || {})[j] ), 
  object
 ) || val
}
console.log(get({ a: null }, "a.b.c", 3)); // output: 3
console.log(get({ a: undefined }, "a", 3)); // output: 3
console.log(get({ a: null }, "a", 3)); // output: 3
console.log(get({ a: { b: 1 } }, "a.b", 3)); // output: 1

Genzhen avatar Jun 23 '20 03:06 Genzhen

//匹配'.'或者'[]'的深层属性
const reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/
//匹配普通属性:数字+字母+下划线
const reIsPlainProp = /^\w*$/
const reEscapeChar = /\\(\\)?/g
const rePropName = RegExp(
    //匹配没有点号和括号的字符串,'\\]'为转义']' 例如:'a'
    '[^.[\\]]+' + '|' +
    // Or match property names within brackets.
    '\\[(?:' +
    // 匹配一个非字符的表达式,例如 '[index]'
    '([^"\'][^[]*)' + '|' +
    // 匹配字符串,例如 '["a"]'
    '(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' +
    ')\\]' + '|' +
    '(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))'
    , 'g')

function get(object, path, defaultValue) {
    let result;
    if (object == null) return result;
    if (!Array.isArray(path)) {
        //是否为key
        const type = typeof path
        if (type == 'number' || type == 'boolean' || path == null || toString.call(path) == '[object Symbol]' || reIsPlainProp.test(path) || !reIsDeepProp.test(path)) {
            path = [path];
        } else {
            //字符串转化为数组
            const tmp = []
            //第一个字符是'.'
            if (path.charCodeAt(0) === '.') {
                tmp.push('')
            }
            path.replace(rePropName, (match, expression, quote, subString) => {
                let key = match
                if (quote) {
                    key = subString.replace(reEscapeChar, '$1')
                } else if (expression) {
                    key = expression.trim()
                }
                tmp.push(key)
            })
            path = tmp;
        }
    }
    //转化为数组后的通用部分
    let index = 0
    const length = path.length
    while (object != null && index < length) {
        let value = path[index++];
        object = object[value]
    }
    result = (index && index == length) ? object : undefined
    return result === undefined ? defaultValue : result
}

Genzhen avatar Jun 23 '20 04:06 Genzhen

function _get(obj, path, defaultVal) {
    let newPath = path;
    let result = { ...obj };

    if (typeof path === "string") {
        if (path.includes(".")) {
            // 如果含有数组选项
            if (path.includes("[")) {
                newPath = newPath.replace(/\[(\w)+\]/g, ".$1");
            }

            newPath = newPath.split(".");
        } else {
            newPath = [path];
        }
    }

    for (let i = 0; i < newPath.length; i++) {
        let currentKey = newPath[i];

        result = result[currentKey];

        if (typeof result === "undefined") {
            result = defaultVal;

            break;
        }
    }

    return result;
};

const data = {
    a: {
        b: [1, 2, 3]
    }
};

console.log(_get(data, "a.b[4]", 20));

ES2020 可以链式 & 空值合并运算

data?.b?.[4] ?? 20

huzedong2015 avatar Aug 28 '20 09:08 huzedong2015

在 js 中经常会出现嵌套调用这种情况,如 a.b.c.d.e,但是这么写很容易抛出异常。你需要这么写 a && a.b && a.b.c && a.b.c.d && a.b.c.d.e,但是显得有些啰嗦与冗长了。特别是在 graphql 中,这种嵌套调用更是难以避免。 这时就需要一个 get 函数,使用 get(a, 'b.c.d.e') 简单清晰,并且容错性提高了很多。

1)代码实现

function get(source, path, defaultValue = undefined) {
  // a[3].b -> a.3.b -> [a,3,b]
 // path 中也可能是数组的路径,全部转化成 . 运算符并组成数组
  const paths = path.replace(/\[(\d+)\]/g, ".$1").split(".");
  let result = source;
  for (const p of paths) {
    // 注意 null 与 undefined 取属性会报错,所以使用 Object 包装一下。
    result = Object(result)[p];
    if (result == undefined) {
      return defaultValue;
    }
  }
  return result;
}
// 测试用例
console.log(get({ a: null }, "a.b.c", 3)); // output: 3
console.log(get({ a: undefined }, "a", 3)); // output: 3
console.log(get({ a: null }, "a", 3)); // output: 3
console.log(get({ a: [{ b: 1 }] }, "a[0].b", 3)); // output: 1

2)代码实现 不考虑数组的情况

const _get = (object, keys, val) => {
 return keys.split(/\./).reduce(
  (o, j)=>( (o || {})[j] ), 
  object
 ) || val
}
console.log(get({ a: null }, "a.b.c", 3)); // output: 3
console.log(get({ a: undefined }, "a", 3)); // output: 3
console.log(get({ a: null }, "a", 3)); // output: 3
console.log(get({ a: { b: 1 } }, "a.b", 3)); // output: 1

path参数还有数组的可能

huzedong2015 avatar Aug 28 '20 09:08 huzedong2015

function _get(obj, path, defaultVal) {
    let newPath = path;
    let result = { ...obj };

    if (typeof path === "string") {
        if (path.includes(".")) {
            // 如果含有数组选项
            if (path.includes("[")) {
                newPath = newPath.replace(/\[(\w)+\]/g, ".$1");
            }

            newPath = newPath.split(".");
        } else {
            newPath = [path];
        }
    }

    for (let i = 0; i < newPath.length; i++) {
        let currentKey = newPath[i];

        result = result[currentKey];

        if (typeof result === "undefined") {
            result = defaultVal;

            break;
        }
    }

    return result;
};

const data = {
    a: {
        b: [1, 2, 3]
    }
};

console.log(_get(data, "a.b[4]", 20));

newPath.replace(/[(\w)+]/g, ".$1"); 这里是否应该是 newPath.replace(/[(\d+)]/g, ".$1");

sh-tengfei avatar Jan 13 '21 09:01 sh-tengfei

function get(target, path, defaultValue) {
  let keys = path.replace(/\[(\d+)\]/g, ".$1").split(".");
  return keys.reduce((t, key) => (t || {})[key], target) ?? defaultValue;
}

Xiaolong96 avatar Mar 22 '21 16:03 Xiaolong96

这道题还是非常不错的,可以锻炼正则和错误处理
function get(obj, path) {
  const keys = path.split('.');
  let res = obj;
  for(let i = 0; i < keys.length; i++) {
    const reg = /^(\w+)\[(\d+)\]$/;
    const key = keys[i];
    if(reg.test(key)) { // 数组索引
      const tmp = key.match(reg);
      const k = tmp[1]; // 键
      const i = Number(tmp[2]); // 索引
      let isError = false;
      try {
        res = res[k][i];
      } catch (error) {
        isError = true;
      } finally {
        if(res == undefined) isError = true;
      }
      if(isError) break;
    }else { // 对象键
      res = res[key];
      if(res == undefined) return '';;
    }
  }
  return res;
}
get({ 'a': [{ 'b': { 'c': 3 } }] }, 'a[0].b.c');

qzruncode avatar Apr 14 '21 07:04 qzruncode

function lensProp(obj = {}, path = '') {
  if (typeof obj !== 'object' || obj === null) {
    obj = {}
  }
  let props = path.replace(/\[/g, '.').replace(/\]/g, '').split('.')
  for (let i = 0; i < props.length; i++) {
    if (typeof obj[props[i]] === 'undefined') {
      return void 0;
    } else {
      // debugger
      if (typeof obj[props[i]] === 'object' && obj !== null) {
        obj = obj[props[i]]
      } else {
        return i === props.length - 1 ? obj[props[i]] : void 0;
      }
    }
  }

  return obj;
}
var obj6 = {
  name: 'yzf',
  children: [{
    name: 'yy',
    age: 1,
    children: [
      {
        name: 'yyy',
        age: 1,
        children: []
      }
    ]
  }, {
    name: 'yy1',
    age: 8,
    children: []
  }],
  other: {
    year: 29
  }
}
// console.log(lensProp(obj6, 'children.0.name'));
// console.log(lensProp(obj6, 'children[0].name'));

fanerge avatar May 17 '21 14:05 fanerge

const get = function (object, path, defaultValue = undefined) {
  const paths = Array.isArray(path) ? path : path.replace(/\[(\d+)\]/g, ".$1").split("."); 
  let result = object; 
  for (const key of paths) {
    result = Object(result)[key];
    if (result === undefined) {
      return defaultValue;
    }
  }
  return result;
};

jonny-wei avatar May 18 '21 11:05 jonny-wei

function get(obj, path, defaultValue){
        let data = obj
        path = path.replace(/\[(\d+)\]/ig, ".$1").split('.')
        for(let key of path){
            if(data[key]){
                data = data[key]
            }else{
                return defaultValue
            }
        }
        return data
    }
    // 测试用例
    console.log(get({ a: null }, "a.b.c", 3)); // output: 3
    console.log(get({ a: undefined }, "a", 3)); // output: 3
    console.log(get({ a: null }, "a.b", 3)); // output: 3
    console.log(get({ a: [{ b: 1 }] }, "a[0].b", 3)); // output: 1
    console.log(get({ a: { b : { c: 2 } } }, "a.b.c", 3)); // output: 2

Luoyuda avatar Jun 10 '21 02:06 Luoyuda

/**
 * @description 仿lodash的get方法,优化属性嵌套地址狱
 * @param { Object } source 要检测的源变量
 * @param { String } path 变量路径
 * @param { Any } dValue 如果未匹配到,则返回默认值
 * @return { Any } 返回目标值或者默认值
 */
const validationValue = (source, path, dValue = undefined) => {
    const paths = Object.prototype.toString.call(path) === '[object Array]'
                    ? path
                    : path.replace(/\[(\d+)\]/g, '.$1').split('.');
    for (let i of paths) {
        source = source?.[i];
        if (source === undefined) return dValue;
    }
    return source;
}

console.log(1111, validationValue({ a: null }, 'a.b.c', 3))
console.log(2222, validationValue({ a: undefined }, 'a', 3))
console.log(3333, validationValue({ a: null }, 'a.b', 3))
console.log(4444, validationValue({ a: [{ b: 1 }] }, 'a[0].b', 3))
console.log(5555, validationValue({ a: { b : { c: 2 } } }, 'a.b.c', 3))
console.log(6666, validationValue({ 'a': [{ 'b': { 'c': 3 } }] }, 'a[0].b.c', 8))

Evllis avatar Aug 11 '21 09:08 Evllis

在 js 中经常会出现嵌套调用这种情况,如 a.b.c.d.e,但是这么写很容易抛出异常。你需要这么写 a && a.b && a.b.c && a.b.c.d && a.b.c.d.e,但是显得有些啰嗦与冗长了。特别是在 graphql 中,这种嵌套调用更是难以避免。 这时就需要一个 get 函数,使用 get(a, 'b.c.d.e') 简单清晰,并且容错性提高了很多。

1)代码实现

function get(source, path, defaultValue = undefined) {
  // a[3].b -> a.3.b -> [a,3,b]
 // path 中也可能是数组的路径,全部转化成 . 运算符并组成数组
  const paths = path.replace(/\[(\d+)\]/g, ".$1").split(".");
  let result = source;
  for (const p of paths) {
    // 注意 null 与 undefined 取属性会报错,所以使用 Object 包装一下。
    result = Object(result)[p];
    if (result == undefined) {
      return defaultValue;
    }
  }
  return result;
}
// 测试用例
console.log(get({ a: null }, "a.b.c", 3)); // output: 3
console.log(get({ a: undefined }, "a", 3)); // output: 3
console.log(get({ a: null }, "a", 3)); // output: 3
console.log(get({ a: [{ b: 1 }] }, "a[0].b", 3)); // output: 1

2)代码实现 不考虑数组的情况

const _get = (object, keys, val) => {
 return keys.split(/\./).reduce(
  (o, j)=>( (o || {})[j] ), 
  object
 ) || val
}
console.log(get({ a: null }, "a.b.c", 3)); // output: 3
console.log(get({ a: undefined }, "a", 3)); // output: 3
console.log(get({ a: null }, "a", 3)); // output: 3
console.log(get({ a: { b: 1 } }, "a.b", 3)); // output: 1

改进版本

function get (source, path, defaultValue = undefined) {
  const paths = Array.isArray(path) ? path
    : path.replace(/\[(\d+)\]/g, '.$1')
      .split('.')
      .filter(Boolean)
  let result = source
  for (const p of paths) {
    result = Object(result)[p]
    if (result == undefined) {
      return defaultValue
    }
  }
  return result
}
console.log(
  get({ a: [{ b: 1 }] }, 'a[0].b', 3)
) // output: 1
console.log(
  get({ a: [{ b: 1 }] }, '.a[0].b', 3)
) // output: 1
console.log(
  get({ a: [{ b: 1 }] }, ['a',0,'b'], 3)
) // output: 1

Weibozzz avatar Sep 09 '21 06:09 Weibozzz

function get(obj, path, defaults) {
  try {
    const val = path.split('.')
      .map((item) => item.split(/\[(\d)\]/).filter(Boolean)).flat().reduce((acc, v) => acc[v], obj)

    if (typeof defaults !== 'undefined' && val === undefined) {
      return defaults
    }

    return val
  } catch (_) {
    return defaults
  }
}

safarishi avatar Oct 14 '21 11:10 safarishi

function get(target: object, propStr: string, defaultValue: any = undefined) {
    const props = propStr.split('.')
    let t: any = target
    for (const prop of props) {
        if (t == null) break
        t = t[prop]
    }
    if (t === undefined) return defaultValue
    return t
}

captain-kuan avatar Dec 14 '22 03:12 captain-kuan

const getPath = (path) => { const paths = [']', '[', '.'] if (Array.isArray(path)) return path; return Array.from(path).filter((str) => !paths.includes(str)) }; const getObjectValue = (object, path, defaultValue) => { const objectPath = getPath(path); let target = object let index = 0; while (index < objectPath.length) { target = target[objectPath[index++]]; if (target === undefined) break; } return index === objectPath.length ? target : defaultValue; };

yaohui-li avatar Aug 25 '23 03:08 yaohui-li

/**
 * 实现lodash的_.get
 * lodash官方文档
 * Gets the value at path of object. If the resolved value is undefined, 
 * the defaultValue is returned in its place.
 * 
 * Arguments
object (Object): The object to query.
path (Array|string): The path of the property to get.
[defaultValue] (*): The value returned for undefined resolved values.

 * Returns
(*): Returns the resolved value.

 * 官方示例
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'
 */

class Lodash {
  get(object, path, defaultValue = undefined) {
    if (typeof object !== "object") {
      throw new Error("first argument of get should be an object");
    }
    if (path instanceof Array) {
      return this._keysReduce(object, path, defaultValue);
    }
    if (typeof path === "string") {
      const keys = path.split(/[\.|\[|\]]/).filter((key) => Boolean(key));
      return this._keysReduce(object, keys, defaultValue);
    }
  }
  _keysReduce(object, path, defaultValue) {
    return path.reduce((pre, cur) => {
      if (typeof cur !== "string") {
        throw new Error("path should be an Array of Strings");
      }
      if (pre === undefined) {
        return defaultValue;
      }
      return pre[cur];
    }, object);
  }
}

const _ = new Lodash();
const object = { a: [{ b: { c: 3 } }] };

console.log(_.get(object, "a[0].b.c"));

console.log(_.get(object, ["a", "0", "b", "c"]));

console.log(_.get(object, "a.b.c", "default"));

Kisthanny avatar Mar 20 '24 10:03 Kisthanny