Skip to content

Instantly share code, notes, and snippets.

@jeneg
Last active December 21, 2023 17:00
Show Gist options
  • Star 34 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save jeneg/9767afdcca45601ea44930ea03e0febf to your computer and use it in GitHub Desktop.
Save jeneg/9767afdcca45601ea44930ea03e0febf to your computer and use it in GitHub Desktop.
Alternative to lodash get method _.get()
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);
}
}
@roddds
Copy link

roddds commented Jan 29, 2020

@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);

@darrylsepeda
Copy link

@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.

@SupremeTechnopriest
Copy link

@darrylsepeda Soon we will be able to replace get with nullish coalescing and optional chaining. https://arpitbatra.netlify.app/posts/lodash-new-js/

@MalikBagwala
Copy link

@da

@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)

@nischithbm
Copy link

nischithbm commented Nov 4, 2020

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
image

@gabrieljmj
Copy link

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

@haouarihk
Copy link

just use obj?.param1?.param2 ?? defaultvalue

@SupremeTechnopriest
Copy link

SupremeTechnopriest commented Oct 21, 2021

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.

@hmelenok
Copy link

hmelenok commented Oct 13, 2022

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');

@SupremeTechnopriest
Copy link

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.

@JamesGardnerKT
Copy link

@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.

@SupremeTechnopriest
Copy link

@JamesGardnerKT

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.

@SupremeTechnopriest
Copy link

Maybe I will maintain a benchmark for this problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment