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);
}
}
@SupremeTechnopriest
Copy link

SupremeTechnopriest commented Sep 16, 2019

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

@sekoyo
Copy link

sekoyo commented Jan 25, 2020

@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

@A77AY
Copy link

A77AY commented Jan 27, 2020

@dominictobias you can already forget about the get, browsers will add https://github.com/tc39/proposal-optional-chaining

@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