# 一、题目
实现 lodash 中的 get 函数【难度⭐,对标百度 T4,阿里 P5,腾讯T2.2】
// var object = { 'a': [{ 'b': { 'c': 3 } }] };
// get(object, 'a[0].b.c'); // 3
// get(object, ['a', '0', 'b', 'c']); // 3
const get = (data, path, defaultValue = void 0)
=> {
// todo
}
# 二、题目情景与分析
| 情景 | 效果 |
|---|---|
| object.a.b.c | ❌ |
| _get(object,'a[0].b.c',null) | ✅ |
| object?.a?.b?.c | ✅ |
# 三、手写
const get = (data, path, defaultValue = 0)
=> {
// 'a[0].b.c' ==> 'a.0.b.c' ==> ['a', '0', 'b', 'c']
const regPath = path.replace(/\[(\d+)\]/g, '.$1').split('.');
let result = data;
for(const path of regPath){
result = Object(result)[path];
if(result == null){
return defaultValue;
}
}
return result;
}
- 考察点1:处理数组情况,使用正则表达式将数组元素
[num]替换成.num - 考察点2:正则表达式书写,
- 考察点3:处理 data 未定义状况,之所以要包一层Object,因为null 与 undefined 取属性会报错,所以使用 Object 包装一下,另外可以借助可选链操作简化
result = result?.[path]
# 四、lodash源码分析 - 环境准备
A modern JavaScript utility library delivering modularity, performance & extras.
lodash是一个一致性、模块化、高性能的JavaScript实用工具库
lodash版本v4.0.0通过
github1s网页可以 查看 (opens new window)lodash - get源码调试测试用例可以
clone到本地
git clone https://github.com/lodash/lodash.git
cd axios
npm install
npm run test
# 五、lodash源码分析 - 结构分析

这是一张 get 依赖引用路径图,相对复杂一些,按照功能划分,大致包括 tokey 模块、 castPath 模块,分别用于检验 key 值是否合法、计算 path。整体流程和我们手写的相差不大,主要是功能方面做了扩展,比如不仅可以实现 a[0] 还可以实现 a[0][0] 这样的操作,并且深度过大时会开启 Map 缓存,下面挑其中的重点讲一下。
# 六、lodash源码分析 - 函数研读
# 1. castPath 模块
如果值不是数组,则将其强制转换路径数组
import isKey from './isKey.js'
import stringToPath from './stringToPath.js'
/**
* @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
isKey判断path是不是当前对象的key- stringToPath 如果传入的 path 不是当前对象的 key,就调用该方法把字符串转成对应的数组
# 2. isKey 模块
检查'value'是否是属性名而不是属性路径
import isSymbol from '../isSymbol.js'
/** 用于匹配属性路径中的属性名称 **/
const reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/
const reIsPlainProp = /^\w*$/
/**
* @private
* @param {*} value The value to check.
* @param {Object} [object] The object to query keys on.
* @returns {boolean} Returns `true` if `value` is a property name, else `false`.
*/
function isKey(value, object) {
if (Array.isArray(value)) {
return false
}
const type = typeof value
if (type === 'number' || type === 'boolean' || value == null || isSymbol(value)) {
return true
}
return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
(object != null && value in Object(object))
}
export default isKey
reIsDeepProp用于界定分隔内容,比如会匹配到a.b[1].c[2]中的.、[1]、[2]reIsPlainProp用于匹配0-n个字符,目的是匹配属性名
# 3. stringToPath 模块
import memoizeCapped from './memoizeCapped.js'
const charCodeOfDot = '.'.charCodeAt(0)
const reEscapeChar = /\\(\\)?/g
const rePropName = RegExp(
// 匹配任何不是点或括号的内容
'[^.[\\]]+' + '|' +
// 或者在括号内匹配属性名称
'\\[(?:' +
// 匹配非字符串表达式
'([^"\'][^[]*)' + '|' +
// 或匹配字符串(支持转义字符)
'(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' +
')\\]'+ '|' +
// 或将""匹配为连续点或空括号之间的间距
'(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))'
, 'g')
/**
* 将'string'转换为属性路径数组
* @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
- 关键是看懂
rePropName这个正则表达式 [^.[\\]]+匹配任何不是点或括号的内容,其中\\]用于对]转义,可以理解为[^.]+或[^[\\]]+\\[(?:匹配[(其中?:表示非捕获性分组(不保存子组)(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2,这个比较复杂了,其中(?!)表示Case-Insensitive即不区分大小写模式,\2表示分组引用,比如(1)(2)(3)\2表示引用第二个分组(2)🐶