Skip to content

Instantly share code, notes, and snippets.

@jeneg
Last active December 21, 2023 17:00
  • Star 34 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
Star You must be signed in to star a gist
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);
}
}
@jeneg
Copy link
Author

jeneg commented Apr 1, 2016

Usage example

var prop = get(data, '[0].items[0].property', 'backupValue');

@chirvo
Copy link

chirvo commented Dec 1, 2016

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

@wxs77577
Copy link

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

@diegolealco
Copy link

diegolealco commented Apr 18, 2019

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

@A77AY
Copy link

A77AY commented Apr 28, 2019

@NoPause I would not advise to do so. It's very slow with errors

@alex-malyita
Copy link

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");
    });
});

@SupremeTechnopriest
Copy link

SupremeTechnopriest commented Aug 29, 2019

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.

@diegolealco
Copy link

I had forgotten about this.
Thanks @KrickRay you are right, it isn't such a good idea.

@mcissel
Copy link

mcissel commented Sep 13, 2019

It doesn't always return the default value though

get({r:5}, 't', 34)

returns 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