这是我面试字节碰到的一道题
题目:实现lodash.get()方法
function _get(obj, path, defaultValue) {
// Write code here
}
const obj = { a: [{ b: 1 }] };
console.log(_get(obj, 'a[0].b', 3)) // 1
console.log(_get(obj, 'a[0].c', 3)) // 3
只针对示例来写是挺简单的,并且这也不是面试官所需要的。
先来看看lodash库中的get()方法如何实现的吧~
lodash库中的get()实现
在 src/get.ts 即可看到源码
// get.ts
import baseGet from './.internal/baseGet.js';
/**
* Gets the value at `path` of `object`. If the resolved value is
* `undefined`, the `defaultValue` is returned in its place.
*
* @since 3.7.0
* @category Object
* @param {Object} object The object to query.
* @param {Array|string} path The path of the property to get.
* @param {*} [defaultValue] The value returned for `undefined` resolved values.
* @returns {*} Returns the resolved value.
* @see has, hasIn, set, unset
* @example
*
* const 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'
*/
function get(object, path, defaultValue) {
const result = object == null ? undefined : baseGet(object, path);
return result === undefined ? defaultValue : result;
}
export default get;
可以看到具体处理的 baseGet 中
// baseGet.ts
import castPath from './castPath.js'
import toKey from './toKey.js'
/**
* The base implementation of `get` without support for default values.
*
* @private
* @param {Object} object The object to query.
* @param {Array|string} path The path of the property to get.
* @returns {*} Returns the resolved value.
*/
function baseGet(object, path) {
path = castPath(path, object)
let index = 0
const length = path.length
while (object != null && index < length) {
object = object[toKey(path[index++])]
}
return (index && index === length) ? object : undefined
}
export default baseGet
可以看到在 baseGet 中又引入了 castPath 和 toKey 两个方法,在实现这个方法时需要对path进行处理,所以 castPath 方法就是对path进行处理的,toKey 方法是将path转换为string字符串的,具体描述在这里。toKey 方法不是关键,去看看castPath 方法。
// castPath.ts
import isKey from './isKey.js'
import stringToPath from './stringToPath.js'
/**
* Casts `value` to a path array if it's not one.
*
* @private
* @param {*} value The value to inspect.
* @param {Object} [object] The object to query keys on.
* @returns {Array} Returns the cast property path array.
*/
function castPath(value, object) {
if (Array.isArray(value)) {
return value
}
return isKey(value, object) ? [value] : stringToPath(value)
}
export default castPath
可以看到在 castPath 文件中,直接判断了path是不是一个数组,如果是则直接返回(这里是一个重要的点);
随后有一个 isKey 方法,这个方法是通过使用正则表达式去匹配path中的属性名的,相当于做个校验,具体描述在这里。还有一个 stringToPath 方法,这个方法做的是将path字符串转换为一个数组的,方便后期工作取出每一个key(这个文件也是一个重要的点)。
// stringToPath.ts
import memoizeCapped from './memoizeCapped.js'
const charCodeOfDot = '.'.charCodeAt(0)
const reEscapeChar = /\\(\\)?/g
const rePropName = RegExp(
// Match anything that isn't a dot or bracket.
'[^.[\\]]+' + '|' +
// Or match property names within brackets.
'\\[(?:' +
// Match a non-string expression.
'([^"\'][^[]*)' + '|' +
// Or match strings (supports escaping characters).
'(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' +
')\\]'+ '|' +
// Or match "" as the space between consecutive dots or empty brackets.
'(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))'
, 'g')
/**
* Converts `string` to a property path array.
*
* @private
* @param {string} string The string to convert.
* @returns {Array} Returns the property path array.
*/
const stringToPath = memoizeCapped((string) => {
const result = []
if (string.charCodeAt(0) === charCodeOfDot) {
result.push('')
}
string.replace(rePropName, (match, expression, quote, subString) => {
let key = match
if (quote) {
key = subString.replace(reEscapeChar, '$1')
}
else if (expression) {
key = expression.trim()
}
result.push(key)
})
return result
})
export default stringToPath
我们可以看到在 stringToPath 方法中,有一行代码 key = substring.replace(reEscapeChar, '$1') ,这一行代码是什么意思?
首先需要知道的是在正则表达式中,使用括号()创建捕获组,可以将匹配到的子串进行捕获。每个捕获组都有一个编号,从左到右依次递增,从1开始。因此,$1 表示第一个捕获组, $2 表示第二个捕获组,如此我们便可以对捕获到的子串进行处理了。
基本上
get()方法的过程就是这些了,来捋一捋大致思路:
- 首先判断传入的path是否是一个数组,如果是则直接返回,不进行下一步处理
- 如果不是一个数组,就会需要进行一些处理,这个处理实际上是对path进行处理,取出路径中的每一个key。具体的处理方式可以通过使用正则表达式或暴力循环。
- 取出每一个key后,再用这些key去获取对象中对应层级的值即可,如果通过指定的路径获取不到具体的值,返回指定的默认值defaultValue即可。
编码实现
基于上面的思路可以写出以下实现(当然只是一种简单实现,情况并没有完全考虑完)
function _get(obj, path, defaultValue) {
let key = Array.isArray(path) ? path : path.replace(/(\[(\d)\])/g, '.$2').split('.');
obj = obj[key[0]];
if (obj && key.length > 1) {
return _get(obj, key.slice(1), defaultValue);
}
return obj ? obj : defaultValue;
}
const obj = { a: [{ b: 1 }] };
console.log(_get(obj, "a[0].b", 3)); // 1
console.log(_get(obj, "a[0].c", 3)); // 3