手写lodash中的get

448 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第19天,点击查看活动详情

前言

当不确定某个对象上是否存在指定的属性时,每次读取该属性的值时,就需要沿着路径逐层判断。

在如今的 JS 中,可以借助可选链 ?. 进行遍历,十分方便。

当在这之前,开发者如果有导入 lodash 这个工具包,通常会使用它的 get 方法进行访问。

看到某份面经上有手写 get 方法的面试题,于是有了今天这篇文章。

测试数据

测试数据使用文档给出的例子:

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'

代码

首先,定义函数,然后确定参数及其返回值。

参数按照官网有三个,分别是要检索的对象 obj,要获取属性的路径 path,找不到属性的值时的返回值 defaultValue

至于返回值,可以时任何值,但是有一个默认值,最终定义如下:

/**
 * 获取对象执行属性的值
 * @param { Object } obj 取值对象
 * @param { string | Array<string> } path 键值
 * @param { ang } defaultValue 找不到目标属性时的返回值
 * @return { any }
 */
function myGet (obj: {}, path: string | Array<string>, defaultValue: any = undefined): any {
  return defaultValue
}

接下来就是对参数进行检测,但是因为,path 的值有两种类型,为了方便参数的检测,优先把 path 处理成 Array(方便后续读取属性的值):

  // 路径解析,转换成为数组
  // 首先进行类型判断,如果时字符串,则处理为数组
  if (typeof path === 'string') {
    // 正则表达式配合 replace,将 `[` 替换为 `.` ,把 `]` 替换为空字符串
    // 如`a[0].b.c[1]` 处理为 `a.0.b.b.1`
    path = path.replace(/([\[\]])/g, ($1) => {
      return $1 === '[' ? '.' : ''
    }).split('.') // 然后按照 `.` 切割为数组
  }

然后就是参数检测,一共有四个判断条件:

  1. obj 不能为 null;
  2. obj 必须是对象;
  3. path 必须为数组;
  4. path 的长度不能为 0;
  5. path.length === 0 时,说明不读取属性,直接返回 obj,其他返回 defaultValue; 代码如下:
  // 参数检测
  if (!obj || typeof obj !== 'object' || !Array.isArray(path) || path.length === 0) {\
    // 当 `path.length === 0` 时,说明不读取属性,直接返回 `obj`,其他返回 `defaultValue`
    return path.length === 0 ? obj : defaultValue
  }

最后就是按照 path 逐层读取属性,如果某一层不存在,则返回 defaultValue

而按照 path 逐层读取属性,无非就是循环或者递归实现,代码如下:

  // 按照路径顺序取值
  let target = obj
  for (const item of path) {
    target = target[item]
    // target 为undefined,结束循环
    if (!target) break
  }

  // target 为假值时,返回 defaultValue
  return target || defaultValue

完整代码如下:

function myGet (obj: {}, path: string | Array<string>, defaultValue: any = undefined): any {
  if (typeof path === 'string') {
    path = path.replace(/([\[\]])/g, ($1) => {
      return $1 === '[' ? '.' : ''
    }).split('.')
  }

  if (!obj || typeof obj !== 'object' || !Array.isArray(path) || path.length === 0) {
    return path.length === 0 ? obj : defaultValue
  }

  let target = obj
  for (const item of path) {
    target = target[item]
    if (!target) break
  }

  return target || defaultValue
}

测试一把:

console.log('result 1:', myGet(testObject, 'a[0].b.c'))
// result 1: 3
console.log('result 2:', myGet(testObject, ['a', '0', 'b']))
// result 2: {c: 3}
console.log('result 3:', myGet(testObject, 'a.b.c', null))
// result 3: null

至此,基本完成。