1

is there any function or any fast way to check if some value in our object startsWith e.g asd

Example:

let obj = { 
   'child' : {
      'child_key': 'asdfghhj'
    },
    'free': 'notasd',
    'with': 'asdhaheg'
  }

// check here if our obj has value that startsWith('asd')

Regards

crotoan
  • 173
  • 1
  • 13

4 Answers4

1

Here is a function with mild ES6 usage:

function startsWithRecursive(obj, needle) {
    return obj != null && 
        (typeof obj === "object"
            ? Object.keys(obj).some( key => startsWithRecursive(obj[key], needle) )
            : String(obj).startsWith(needle));
}

// Sample data
let obj = { 
    'child' : {
      'child_key': 'asdfghhj'
    },
    'free': 'notasd',
    'with': 'asdhaheg'
};
// Requests
console.log( 'obj, "asd":',    startsWithRecursive(obj, 'asd'    ) );
console.log( 'obj, "hello":',  startsWithRecursive(obj, 'hello'  ) );
console.log( 'null, "":',      startsWithRecursive(null, ''      ) );
console.log( 'undefined, "":', startsWithRecursive(undefined, '' ) );
console.log( '"test", "te":',  startsWithRecursive('test', 'te'  ) );
console.log( '12.5, 1:',       startsWithRecursive(12.5, 1       ) );

Explanation:

The function is recursive: it calls itself as it goes through a nested object structure. The value passed as obj can fall in one of the following three categories:

  1. It is equivalent to null (like also undefined): in that case neither a recursive call, nor a call of the startsWith method can be made: the result is false as this value obviously does not start with the given search string;

  2. It is an object: in that case that object's property values should be inspected. This will be done through recursive calls. The some method makes sure that as soon a match has been found, the iteration stops, and no further property values are inspected. In that case some returns true. If none of the property values matched, some returns false;

  3. It is none of the above. In that case we cast it to string (by applying the String function) and apply startsWith on it.

The value calculated in the applicable step will be returned as function result. If this was a recursive call, it will be treated as return value in the some callback, ...etc.

Note that this function also returns the correct result when you call it on a string, like so:

startsWithRecursive('test', 'te'); // true

Non-Recursive Alternative

In answer to comments about potential stack limitations, here is an alternative non-recursive function which maintains a "stack" in a variable:

function startsWithRecursive(obj, needle) {
    var stack = [obj];
    while (stack.length) {
        obj = stack.pop();
        if (obj != null) {
            if (typeof obj === "object") {
                stack = stack.concat(Object.keys(obj).map( key => obj[key] ));
            } else {
                if (String(obj).startsWith(needle)) return true;
            }
        }
    }
    return false;
}
trincot
  • 317,000
  • 35
  • 244
  • 286
  • Very nice that this works with Array child nodes although I'm not sure if it was intentional or just a lucky side effect ^_^ – Mulan Sep 12 '16 at 19:37
  • In JavaScript arrays are objects (`typeof` "object") with some additional features (like a `length` property), so yes, it is intended to work for arrays. – trincot Sep 12 '16 at 19:40
  • One thing to note about recursion, the stack is finite and with very large objects you can hit a limit. – Xotic750 Sep 12 '16 at 19:54
  • 1
    @Xotic750 to be more specific, objects with very deep levels of nesting could hit a limit. A flat object, or even an object with hundreds of levels of nesting with thousands of keys would be fine. – Mulan Sep 12 '16 at 19:56
  • Very nice. I wasn't suggesting that it was a must, just a note. But very good. ;) +1 – Xotic750 Sep 12 '16 at 20:14
1

Use @trincot's solution if you really don't care about which node/value matched. It's straightforward, well-written, and solves your problem very effectively.

If you want more than just a Boolean value as the result of your digging, read along ...


I really doubt your need for this, but if your object is significantly large, you will want an early exit behaviour – what this means is that as soon as a match is found, iteration through your input data will stop and true/false result will be returned immediately. @trincot's solution offers early exit, but solutions that using map, filter, or reduce offer no such behaviour.

findDeep is much more useful than just checking if a string value starts with another string value – it takes a higher-order function that is applied for each leaf node in your data.

This answer uses my findDeep procedure to define a generic anyStartsWith procedure by checking if findDeep returns undefined (no match)

It will work any any input type and it will traverse Object and Array child nodes.

const isObject = x=> Object(x) === x
const isArray = Array.isArray
const keys = Object.keys
const rest = ([x,...xs]) => xs

const findDeep = f => x => {
  
  let make = (x,ks)=> ({node: x, keys: ks || keys(x)})
  
  let processNode = (parents, path, {node, keys:[k,...ks]})=> {
    if (k === undefined)
      return loop(parents, rest(path))
    else if (isArray(node[k]) || isObject(node[k]))
      return loop([make(node[k]), make(node, ks), ...parents], [k, ...path])
    else if (f(node[k], k))
      return {parents, path: [k,...path], node}
    else
      return loop([{node, keys: ks}, ...parents], path)
  }
  
  let loop = ([node,...parents], path) => {
    if (node === undefined)
      return undefined
    else
      return processNode(parents, path, node)
  }
  
  return loop([make(x)], [])
}

const startsWith = x => y => y.indexOf(x) === 0
const anyStartsWith = x => xs => findDeep (startsWith(x)) (xs) !== undefined

let obj = { 
  'child' : {
    'child_key': 'asdfghhj'
  },
  'free': 'notasd',
  'with': 'asdhaheg'
}

console.log(anyStartsWith ('asd') (obj))   // true
console.log(anyStartsWith ('candy') (obj)) // false

You'll see this is kind of a waste of findDeep's potential, but if you don't need it's power then it's not for you.

Here's the real power of findDeep

findDeep (startsWith('asd')) (obj)

// =>     
{
  parents: [
    {
      node: {
        child: {
          child_key: 'asdfghhj'
        },
        free: 'notasd',
        with: 'asdhaheg'
      },
      keys: [ 'free', 'with' ]
    }
  ],
  path: [ 'child_key', 'child' ],
  node: {
    child_key: 'asdfghhj'
  }
} 

The resulting object has 3 properties

  • parents – the full object reference to each node in the matched value's lineage
  • path – the path of keys to get to the matched value (stack reversed)
  • node – the key/value pair that matched

You can see that if we take the parent object as p and reverse the path stack, we get to the matched value

p['child']['child_key']; //=> 'asdfghhj'
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • As you referred to my answer, I wonder: are you suggesting the solution I propose does not make an early exit when a match is found? – trincot Sep 12 '16 at 19:46
  • @trincot no, not at all. I will make an update to be more explicit about that. – Mulan Sep 12 '16 at 19:49
  • Ah, sorry for my misunderstanding... I read it again, and indeed I made a wrong connection there ;-) – trincot Sep 12 '16 at 19:50
  • @trincot my original answer didn't mention yours. When I added the bit at the top, I wasn't careful enough to separate the two distinct things I was trying to communicate. <3 – Mulan Sep 12 '16 at 19:51
0

You can recursively iterate object properties and check if property starts with prefix using find function:

function hasPropertyStartingWith(obj, prefix) {
  return !!Object.keys(obj).find(key => {
    if (typeof obj[key] === 'object') {
      return hasPropertyStartingWith(obj[key], prefix)
    }
    if (typeof obj[key] === 'string') {
      return obj[key].startsWith(prefix)
    }
    return false
  })
}

console.log(hasPropertyStartingWith(obj, 'asd'))
madox2
  • 49,493
  • 17
  • 99
  • 99
  • 2
    @crotoan, maybe it is not applicable to your actual case, but this code generates an error when one of the property values happens to be `null` or `undefined`, nor does it detect that a numerical value can start with something like '1' or '-'. – trincot Sep 12 '16 at 20:24
0

You may get away with something as simple as using a RegExp on a JSON string, something like

var obj = {
  'child': {
    'child_key': 'asdfghhj'
  },
  'free': 'notasd',
  'with': 'asdhaheg'
};

function customStartsWith(obj, prefix) {
  return new RegExp(':"' + prefix + '[\\s\\S]*?"').test(JSON.stringify(obj));
}

console.log('obj, "asd":', customStartsWith(obj, 'asd'));
console.log('obj, "hello":', customStartsWith(obj, 'hello'));
console.log('null, "":', customStartsWith(null, ''));
console.log('undefined, "":', customStartsWith(undefined, ''));
console.log('"test", "te":', customStartsWith('test', 'te'));
console.log('12.5, 1:', customStartsWith(12.5, 1));
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.9/es5-shim.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/json3/3.3.2/json3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.1/es6-shim.js"></script>

Update: Another recursive object walker that will work in a shimmed environment. This is just an example and it is easily customised.

var walk = returnExports;
var obj = {
  'child': {
    'child_key': 'asdfghhj'
  },
  'free': 'notasd',
  'with': 'asdhaheg'
};

function customStartsWith(obj, prefix) {
  var found = false;
  walk(obj, Object.keys, function(value) {
    if (typeof value === 'string' && value.startsWith(prefix)) {
      found = true;
      walk.BREAK;
    }
  });
  return found;
}

console.log('obj, "asd":', customStartsWith(obj, 'asd'));
console.log('obj, "hello":', customStartsWith(obj, 'hello'));
console.log('null, "":', customStartsWith(null, ''));
console.log('undefined, "":', customStartsWith(undefined, ''));
console.log('"test", "te":', customStartsWith('test', 'te'));
console.log('12.5, 1:', customStartsWith(12.5, 1));
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.9/es5-shim.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/json3/3.3.2/json3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.1/es6-shim.js"></script>
<script src="https://rawgithub.com/Xotic750/object-walk-x/master/lib/object-walk-x.js"></script>
Xotic750
  • 22,914
  • 8
  • 57
  • 79
  • Nice idea! You'd need to escape special regex characters in `prefix` though, and deal with backslashes or unicode escapes in the JSON string. – trincot Sep 12 '16 at 19:35
  • Yeah, this is certainly not production ready code, more the idea. :) – Xotic750 Sep 12 '16 at 19:36