高级前端手写代码【Q198】如何实现类似 lodash.get 函数

如何实现类似 lodash.get 函数

更多描述 使用 get 函数可避免长链的 key 时获取不到属性而出现问题,此时进行异常避免时及其服务,如 o.a && o.a.b && o.a.b.c && o.a.b.c.d

实现类似lodash.get,有以下测试用例:

const object = { a: [{ b: { c: 3 } }] };
 
//=> 3
get(object, "a[0].b.c");
 
//=> 3
get(object, 'a[0]["b"]["c"]');
 
//=> 10086
get(object, "a[100].b.c", 10086);

问题追问:

1. 如何使用 ts 写法来实现 lodash.get 函数?

Issue 欢迎在 Gtihub Issue 中回答此问题: Issue 199

Author 回答者: miaooow

function lodashGet(obj,exps){ if(typeof exps !== ‘string’) return obj if(typeof obj !== ‘object’) return obj let res = obj const arr = exps.split(’.’) for(let i=0;i<arr.length;i++){ const exp = arr[i] if(res[exp]){ res = res[exp] } else{ return undefined } } return res }

var obj = {test:{arr:[{name:1}]}}

lodashGet(obj,‘test.arr.0.name’)

Author 回答者: shfshanyue

代码见 如何实现类似 lodash.get 函数 - codepen

function get(source, path, defaultValue = undefined) {
  // a[3].b -> a.3.b -> [a, 3, b]
  const paths = path
    .replace(/\[(\w+)\]/g, ".$1")
    .replace(/\["(\w+)"\]/g, ".$1")
    .replace(/\['(\w+)'\]/g, ".$1")
    .split(".");
  let result = source;
  for (const p of paths) {
    result = result?.[p];
  }
  return result === undefined ? defaultValue : result;
}
const object = { a: [{ b: { c: 3 } }] };
const result = _.get(object, "a[0].b.c", 1);

Author 回答者: haotie1990

function getValue(context, path, defaultValue) {
  if (
    Object.prototype.toString.call(context) !== "[object Object]" &&
    Object.prototype.toString.call(context) !== "[object Array]"
  ) {
    return context;
  }
  let paths = [];
  if (Array.isArray(path)) {
    paths = [...path];
  } else if (Object.prototype.toString.call(path) === "[object String]") {
    paths = path
      .replace(/\[/g, ".")
      .replace(/\]/g, "")
      .split(".")
      .filter(Boolean);
  } else {
    paths = [String(path)];
  }
  let result = undefined;
  for (let i = 0; i < paths.length; i++) {
    const key = paths[i];
    result = result ? result[key] : context[key];
    if (result !== null && typeof result !== "undefined") {
      continue;
    }
    return defaultValue || undefined;
  }
  return result;
}

Author 回答者: heretic-G

// 其实原本是按照lodash实现的 但是这里有个差异是如果属性存在就返回其实没有把目标元素是`undefined`的时候设置回default
function get(arm, params = "", defaultVal) {
  if (typeof params !== "string" && !Array.isArray(params)) {
    throw new Error(`${params} is not string or array`);
  }
  if (!Array.isArray(params)) {
    params = params.split(/\].|[\[.]/);
  }
  for (let i = 0; i < params.length; i++) {
    if (Object.prototype.hasOwnProperty.call(arm, params[i])) {
      arm = arm[params[i]];
    } else {
      return defaultVal;
    }
  }
  return arm;
}
 
function get(obj, keyStr, defVal = undefined) {
  let matchArr = Array.from(
    keyStr.matchAll(/(\[).*?(\])|(?<=\.).*?(?=\.)|(?<=\.).*?$/g),
  );
  let val = obj;
  for (let i = 0; i < matchArr.length; i++) {
    if (
      (typeof val === "object" && val !== null) ||
      typeof val === "function"
    ) {
      let key = matchArr[i][0];
      if (key[0] === "[") {
        key = key.slice(1, key.length - 1);
      }
      val = obj[key];
    } else {
      return defVal;
    }
  }
  if (val === undefined) {
    return defVal;
  } else {
    return val;
  }
}
type strToPoint<S> = S extends `${infer F}["${infer M}`
  ? strToPoint<`${F}.${M}`>
  : S extends `${infer F}"]${infer M}`
    ? strToPoint<`${F}${M}`>
    : S extends `${infer F}['${infer M}`
      ? strToPoint<`${F}.${M}`>
      : S extends `${infer F}']${infer M}`
        ? strToPoint<`${F}${M}`>
        : S extends `${infer F}[${infer M}`
          ? strToPoint<`${F}.${M}`>
          : S extends `${infer F}]${infer M}`
            ? strToPoint<`${F}${M}`>
            : S;
 
type strPointToArr<
  S,
  A extends string[] = [],
> = S extends `${infer F}.${infer M}`
  ? strPointToArr<M, [...A, F]>
  : S extends ""
    ? A
    : [...A, S];
 
type getReturnType<
  O extends unknown,
  K extends string[],
  D extends unknown = undefined,
> = K extends []
  ? O extends undefined
    ? D
    : O
  : O extends Record<string, any>
    ? getReturnType<
        K[0] extends keyof O ? O[K[0]] : undefined,
        K extends [first: infer F, ...args: infer L] ? L : [],
        D
      >
    : D;
 
let obj = {
  a: [
    1,
    "lisi",
    {
      b: {
        c: 4,
      },
      f: {
        g: "wangwu",
      },
    },
  ],
} as const;
 
type get<
  O extends Record<string, any>,
  K extends string,
  Def extends unknown = undefined,
> = (
  obj: O,
  keyStr: K,
  defVal: Def,
) => getReturnType<O, strPointToArr<strToPoint<K>>, Def>;
 
type zz = get<typeof obj, "a[2][b].c", "123">;
type zzz = get<typeof obj, "d[e]", "defaultVal">;

Author 回答者: hwb2017

const lodashGet = (
  object: { [key: string]: any },
  path: Array<string> | string,
  defaultValue?: any,
): any => {
  let result: any;
  const findArrayPath = (path: Array<string>): any => {
    if (path.length === 0) {
      return (result = defaultValue);
    }
    result = object;
    for (const p of path) {
      if (p in result) {
        result = result[p];
      } else {
        result = defaultValue;
        break;
      }
    }
    return result;
  };
  if (Array.isArray(path)) {
    result = findArrayPath(path);
  } else {
    path.replace;
    let normalizedPath = path.replace(/\.|\[|\]/g, " ").split(/\s+/);
    result = findArrayPath(normalizedPath);
  }
  return result;
};
 
const object = { a: [{ b: { c: 3 } }] };
 
console.log(lodashGet(object, "a[0].b.c"));
console.log(lodashGet(object, ["a", "0", "b", "c"]));
console.log(lodashGet(object, "a.b.c", "default"));

Author 回答者: yazhouio

function get(obj, keys, defaultValue) {
  let tempObj = obj;
  let arr = [];
  if (typeof keys === "string") {
    let key = "";
    let index = 0;
 
    while (index < keys.length) {
      const k = keys[index];
      if (["[", "'", '"', ".", "]"].includes(k)) {
        if (key.length) {
          arr.push(key);
        }
        key = "";
      } else {
        key = key + k;
      }
      index = index + 1;
    }
    key && arr.push(key);
  } else {
    arr = keys;
  }
 
  while (arr.length) {
    tempObj = tempObj[arr.shift()];
    if (tempObj === undefined || tempObj === null) {
      return defaultValue;
    }
  }
  return tempObj;
}

Author 回答者: 4may-mcx

function get(obj, oriPath, defaultVal) {
  const paths = oriPath.split(".");
  const keys = [];
  for (const path of paths) {
    keys.push(...pathHandler(path));
  }
 
  let res = obj;
  for (const key of keys) {
    if (res[key] === undefined || res[key] === null) return defaultVal;
    res = res[key];
  }
 
  return res;
}
 
// 将 path 处理为能用的 keys
function pathHandler(path) {
  const res = [];
  path = path.split("");
 
  let left = 0;
  let right = 0;
  let str = "";
 
  while (left < path.length && right < path.length) {
    if (path[left] === "[") {
      right = left + 1;
      while (path[right] !== "]") {
        right++;
      }
      temp = path.slice(left + 1, right).join("");
 
      left = right + 1;
      temp && res.push(temp);
    } else {
      str += path[left++];
    }
  }
  res.unshift(str);
 
  return res.map((item) => {
    if (item[0] >= "0" && item[0] <= "9") {
      // 转化为整数
      return parseInt(item);
    }
    if (item.indexOf('"') !== -1 || item.indexOf("'") !== -1) {
      // 去除两边的引号
      return item.slice(1, item.length - 1);
    }
    return item;
  });
}