FE-Interview
FE-Interview copied to clipboard
第 14 题:实现 lodash 的_.get
欢迎在下方发表您的优质见解
在 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
//匹配'.'或者'[]'的深层属性
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
}
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
在 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: 12)代码实现 不考虑数组的情况
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参数还有数组的可能
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");
function get(target, path, defaultValue) {
let keys = path.replace(/\[(\d+)\]/g, ".$1").split(".");
return keys.reduce((t, key) => (t || {})[key], target) ?? defaultValue;
}
这道题还是非常不错的,可以锻炼正则和错误处理
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');
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'));
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;
};
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
/**
* @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))
在 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: 12)代码实现 不考虑数组的情况
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
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
}
}
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
}
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; };
/**
* 实现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"));