-
-
Save jeneg/9767afdcca45601ea44930ea03e0febf to your computer and use it in GitHub Desktop.
function get(obj, path, def) { | |
var fullPath = path | |
.replace(/\[/g, '.') | |
.replace(/]/g, '') | |
.split('.') | |
.filter(Boolean); | |
return fullPath.every(everyFunc) ? obj : def; | |
function everyFunc(step) { | |
return !(step && (obj = obj[step]) === undefined); | |
} | |
} |
@SupremeTechnopriest thanks for the benchmark, would be interesting to see lodash's one too to compare. Lodash has quite a lot of methods it uses behind the scenes which are needed to run on older browsers and overall will be the most robust, but would be interesting to see perf difference
@dominictobias you can already forget about the get
, browsers will add https://github.com/tc39/proposal-optional-chaining
@SupremeTechnopriest the ||
on line 4 needs to be a bit more specific:
const get = (value, path, defaultValue) =>
String(path)
.split('.')
.reduce((acc, v) => {
try {
acc = acc[v] === undefined ? defaultValue : acc[v];
} catch (e) {
return defaultValue;
}
return acc;
}, value);
And if you don't care about multiple returns:
const get = (value, path, defaultValue) =>
String(path)
.split('.')
.reduce((acc, v) => {
try {
return acc[v] === undefined ? defaultValue : acc[v];
} catch (e) {
return defaultValue;
}
}, value);
@SupremeTechnopriest It's nice to see your benchmark, but third solution doesn't cater array index in object.
const get3 = (value, path, defaultValue) => {
return String(path).split('.').reduce((acc, v) => {
try {
acc = acc[v] || defaultValue
} catch (e) {
return defaultValue
}
return acc
}, value)
For example: get(data, "[0].name"); (it shows nothing in my case)
I ended up using 2nd solution for my case.
@darrylsepeda Soon we will be able to replace get with nullish coalescing and optional chaining. https://arpitbatra.netlify.app/posts/lodash-new-js/
@SupremeTechnopriest It's nice to see your benchmark, but third solution doesn't cater array index in object.
const get3 = (value, path, defaultValue) => { return String(path).split('.').reduce((acc, v) => { try { acc = acc[v] || defaultValue } catch (e) { return defaultValue } return acc }, value)
For example: get(data, "[0].name"); (it shows nothing in my case)
I ended up using 2nd solution for my case.
You should access it using '0.name' (remove the square braces)
const get3 = (value, path, defaultValue) => { return String(path).split('.').reduce((acc, v) => { try { acc = acc[v] || defaultValue } catch (e) { return defaultValue } return acc }, value); }
get3({ a: { b: { c: 0 } } }, 'a.b.c', 'defaultVal') // => this will return defaultVal whereas it should actually be returning 0
get3({ a: { b: { c: "" } } }, 'a.b.c', 'defaultVal') // => same here
this is happening due to this statement
acc[v] || defaultValue
it would end up evaluating for this truthfulness, instead of just for undefined or null cases!
So, the better implementation (functionality-wise) would be as follows
get4 = (value, path, defaultValue) => {
return String(path).split('.').reduce((acc, v) => {
try {
acc = (acc[v] !== undefined && acc[v] !== null) ? acc[v] : defaultValue
} catch (e) {
return defaultValue
}
return acc
}, value);
}
Another enhacement we could do within the implementation would be
'a[0].b.c'.replace(/[/g, '.').replace(/]/g, '') // => results into "a.0.b.c"
['a', '0', 'b', 'c'].join('.'); // => results into "a.0.b.c"
Once we have all different formats converted to a simple dot notation, we could use the above implementation.
Hence covering all use cases supported by lodash.get as shown below
The main reason of developing it was that the get
lodash method was the only method I was using from it and no support no nullish operator on my working node version.
const get = (obj, path, defaultValue) => {
const result = path.split('.').reduce((r, p) => {
if (typeof r === 'object') {
p = p.startsWith("[") ? p.replace(/\D/g, "") : p;
return r[p];
}
return undefined;
}, obj);
return result !== undefined ? defaultValue : result;
};
Running this same benchmark test with the function named as get4
got this results:
get1 x 2,192,434 ops/sec ±1.10% (92 runs sampled)
get2 x 1,097,014 ops/sec ±0.84% (92 runs sampled)
get3 x 4,552,938 ops/sec ±0.56% (93 runs sampled)
get4 x 4,604,406 ops/sec ±0.33% (89 runs sampled)
Fastest is get4
just use obj?.param1?.param2 ?? defaultvalue
just use
obj?.param1?.param2 ?? defaultvalue
At the time this was being discussed optional chaining was not even a proposal yet. I would be curious to see this added to the benchmark if you want to take that on @haouarihk.
See my comment from May 29 2020
@darrylsepeda Soon we will be able to replace get with nullish coalescing and optional chaining.
Not the fastest one but working for me:
export const get = (value: any, path: string, defaultValue?: any) => {
return String(path)
.split('.')
.reduce((acc, v) => {
if (v.startsWith('[')) {
const [, arrPart] = v.split('[');
v = arrPart.split(']')[0];
}
if (v.endsWith(']') && !v.startsWith('[')) {
const [objPart, arrPart, ...rest] = v.split('[');
const [firstIndex] = arrPart.split(']');
const otherParts = rest
.join('')
.replaceAll('[', '')
.replaceAll(']', '.')
.split('.')
.filter(str => str !== '');
return [...acc, objPart, firstIndex, ...otherParts];
}
return [...acc, v];
}, [] as string[])
.reduce((acc, v) => {
try {
acc = acc[v] !== undefined ? acc[v] : defaultValue;
} catch (e) {
return defaultValue;
}
return acc;
}, value);
};
Checked with the next tests:
const data = {
x: {y: {z: 1, xx: [['xxx', 1]]}},
a: [{b: {c: ['d', 'e', {f: 2}]}}, {b: {c: ['d', 'e', {f: 3}]}}],
q: [{w: {e: ['r', 't', {y: 4}]}}],
nullable: null,
undefinedDefined: undefined,
};
expect(get(data, 'x.z')).toEqual(undefined);
expect(get(data, 'x.y.z')).toEqual(1);
expect(get(data, 'a')).toEqual(data.a);
expect(get(data, 'a.b')).toEqual(data.a.b);
expect(get(data, 'a.[0].b.c')).toEqual(data.a[0].b.c);
expect(get(data, 'a[0].b.c')).toEqual(data.a[0].b.c);
expect(get(data, 'q[0].w.e[3]')).toEqual(undefined);
expect(get(data, 'q[0].w.e[2]')).toEqual(data.q[0].w.e[2]);
expect(get(data, 'x.y.xx[0]')).toEqual(data.x.y.xx[0]);
expect(get(data, 'x.y.xx[0][1]')).toEqual(data.x.y.xx[0][1]);
expect(get(data, 'x.z.a', 'hehe')).toEqual('hehe');
expect(get(data, 'nullable', 'hehe')).toEqual(null);
expect(get(data, 'undefinedDefined', 'hehe')).toEqual('hehe');
I would just use optional chaining and nullish coalescing:
https://caniuse.com/?search=optional%20chaining
https://caniuse.com/?search=nullish%20coalescing
Its pretty well supported now.
@SupremeTechnopriest, out of interest, how would you go about using that in situations where you don't know what the path is going to be? e.g. you have an object and a counterpart configuration that targets different portions of it.
If you knew the depth you could do something like:
const obj = {
a: {
b: {
c: 1
}
}
}
const path = 'a.b.c'
const fallback = 2
const parts = path.split('.')
const value = obj[parts[0]]?.[parts[1]]?.[parts[2]] ?? fallback
console.log(value) // 1
If you don't know the depth, I think the get
method is going to be the best route. You could check the length of parts
and do a switch and handle all possible depths, but Im not sure if thats going to out perform the get
methods above.
Maybe I will maintain a benchmark for this problem.
@mcissel I noticed that too and was able to fix it with the following:
Performance didn't change.