-
-
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); | |
} | |
} |
Hey, thanks for this. I wrote this other one based on yours:
const get = (obj, path, def) => (() => typeof path === 'string' ? path.replace(/\[(\d+)]/g,'.$1') : path.join('.'))()
.split('.')
.filter(Boolean)
.every(step => ((obj = obj[step]) !== undefined)) ? obj : def
Thanks for share, I use reduce
, it supports dot notation .
only.
const get = (value, path, defaultValue) => {
return String(path).split('.').reduce((acc, v) => {
try {
acc = acc[v]
} catch (e) {
return defaultValue
}
return acc
}, value)
}
console.log(get([[[[120]]]], '0.0.0.0', 0))
NOTE disregard this, it has very poor performance.
I made a different function for the same purpose.
function attempt(getter, fallback) {
try {
return getter();
} catch (error) {
return fallback;
}
}
and here is how I use it.
const prop = attempt(() => some.deeply['nested'].value, 'fallback value')
@NoPause I would not advise to do so. It's very slow with errors
import {expect} from "chai";
import { get } from './helpers';
describe("Helpers - get", function () {
it("should return fallback value", function () {
const input = {
test: 'test'
};
expect(get(input, 'noexist', null)).to.deep.equal(null);
});
it("should return fallback value for inner level", function () {
const input = {
test: 'test'
};
expect(get(input, 'noexist.nested.noexist', null)).to.deep.equal(null);
});
it("should return value first level", function () {
const input = {
test: 'test'
};
expect(get(input, "test", null)).to.deep.equal("test");
});
it("should return value nested level", function () {
const input = {
test: {
nested: {
test: "test"
}
}
};
expect(get(input, 'test.nested.test', null)).to.deep.equal("test");
});
});
Hey all! I did a little benchmarking on the methods provided here. Here are the results:
const { Suite } = require('benchmark')
const suite = new Suite()
const get1 = function get (obj, path, def) {
const fullPath = path
.replace(/\[/g, '.')
.replace(/]/g, '')
.split('.')
.filter(Boolean)
return fullPath.every(everyFunc) ? obj : def
function everyFunc (step) {
return !(step && (obj = obj[step]) === undefined)
}
}
const get2 = (obj, path, def) => (() => typeof path === 'string' ? path.replace(/\[(\d+)]/g, '.$1') : path.join('.'))()
.split('.')
.filter(Boolean)
.every(step => ((obj = obj[step]) !== undefined)) ? obj : def
const get3 = (value, path, defaultValue) => {
return String(path).split('.').reduce((acc, v) => {
try {
acc = acc[v]
} catch (e) {
return defaultValue
}
return acc
}, value)
}
const obj = { a: { b: { c: 'd' } } }
// add tests
suite
.add('get1', function () {
get1(obj, 'a.b.c', undefined)
})
.add('get2', function () {
get2(obj, 'a.b.c', undefined)
})
.add('get3', function () {
get3(obj, 'a.b.c', undefined)
})
.on('cycle', function (event) {
console.log(String(event.target))
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'))
})
.run({ 'async': true })
get1 x 2,971,925 ops/sec ±0.60% (90 runs sampled)
get2 x 1,743,194 ops/sec ±2.77% (87 runs sampled)
get3 x 5,272,977 ops/sec ±2.20% (84 runs sampled)
Fastest is get3
The best implementation is @wxs77577's. I omitted @diegolealco submission because it didn't pass my requirement test.
I had forgotten about this.
Thanks @KrickRay you are right, it isn't such a good idea.
It doesn't always return the default value though
get({r:5}, 't', 34)
returns undefined
@mcissel I noticed that too and was able to fix it with the following:
const get3 = (value, path, defaultValue) => {
return String(path).split('.').reduce((acc, v) => {
try {
acc = acc[v] || defaultValue
} catch (e) {
return defaultValue
}
return acc
}, value)
Performance didn't change.
@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.
Usage example