8

What's the best way to search through a JavaScript object of unknown depth and properties and replace all instances of given string?

This works, but is it the best way?

var obj = {
   'a' : 'The fooman poured the drinks.',
   'b' : {
      'c' : 'Dogs say fook, but what does the fox say?'
   }
}

console.log (JSON.parse(JSON.stringify(obj).replace(/foo/g, 'bar')));

Fiddle: http://jsfiddle.net/93Uf4/3/

Andrew Downes
  • 547
  • 7
  • 13
  • 1
    I would do it the same way as you do. – bobthedeveloper Apr 13 '14 at 19:22
  • 3
    Do you want it to apply to both keys and values? Can the object contain functions? – Ingo Bürk Apr 13 '14 at 19:23
  • 3
    Using this method to replace `:`, `'`, `"`, `{` or any such thing is likely to cause a complete meltdown. I'd write a function that does this using recursion. – mzedeler Apr 13 '14 at 19:31
  • Also, using the regex `/some string$/` won't behave the way most people would expect. – mzedeler Apr 13 '14 at 19:32
  • In my particular case, just values but the keys will never contain the search term. The object does not contain functions. – Andrew Downes Apr 13 '14 at 21:11
  • Ignoring other issues mentioned here, and if your use case is simple you can now use `replaceAll` instead of just `replace(/foo/g, 'bar')` to do a global replace all – maxshuty Apr 19 '21 at 12:43

1 Answers1

8

Next to the way you proposed yourself, here is a classic loop approach. As mentioned by someone in a comment, it's more stable because you don't risk screwing the object up and throwing an error when trying to parse it back. On the other hand, some questions arise (see bottom).

Be careful, though, as the needle will be used as a regular expression. You may want to consider adding some sort of quoting to it.

I hope I didn't overlook anything, so test it and play around with it. Here you can find a fiddle.

/**
  * Replaces all occurrences of needle (interpreted as a regular expression with replacement and returns the new object.
  * 
  * @param entity The object on which the replacements should be applied to
  * @param needle The search phrase (as a regular expression)
  * @param replacement Replacement value
  * @param affectsKeys[optional=true] Whether keys should be replaced
  * @param affectsValues[optional=true] Whether values should be replaced
  */
Object.replaceAll = function (entity, needle, replacement, affectsKeys, affectsValues) {
    affectsKeys = typeof affectsKeys === "undefined" ? true : affectsKeys;
    affectsValues = typeof affectsValues === "undefined" ? true : affectsValues;

    var newEntity = {},
        regExp = new RegExp( needle, 'g' );
    for( var property in entity ) {
        if( !entity.hasOwnProperty( property ) ) {
            continue;
        }

        var value = entity[property],
            newProperty = property;

        if( affectsKeys ) {
            newProperty = property.replace( regExp, replacement );
        }

        if( affectsValues ) {
            if( typeof value === "object" ) {
                value = Object.replaceAll( value, needle, replacement, affectsKeys, affectsValues );
            } else if( typeof value === "string" ) {
                value = value.replace( regExp, replacement );
            }
        }

        newEntity[newProperty] = value;
    }

    return newEntity;
};

The last two parameters are optional, so it's perfectly fine to just call it like this:

var replaced = Object.replaceAll( { fooman: "The dog is fooking" }, "foo", "bar" );

However, there are still edge cases where it's unclear what should happen. For example:

// do you expect it to stay undefined or change type and become "undebazed"?
console.log( Object.replaceAll( { x: undefined }, "fin", "baz" ) );

// null or "nalala"?
console.log( Object.replaceAll( { x: null }, "ull", "alala" ) );

Or

// true or false?
console.log( Object.replaceAll( { x: true }, "true", "false" ) );

// true or "foo"?
console.log( Object.replaceAll( { x: true }, "true", "foo" ) );

And the same for numbers

// 1337 or 1007?
console.log( Object.replaceAll( { x: 1337 }, "33", "00" ) );

// 1337 or "1foo7"
console.log( Object.replaceAll( { x: 1337 }, "33", "foo" ) );

None of these cases are currently handled in my method – only objects (for nesting) and strings will be touched.

Red2678
  • 3,177
  • 2
  • 29
  • 43
Ingo Bürk
  • 19,263
  • 6
  • 66
  • 100
  • Thank you for this. I should have said, I only want to replace within strings so null/undefined/numbers should be untouched as you've done. – Andrew Downes Apr 13 '14 at 21:17
  • @AndrewDownes Then this should do :) If it works for you, please remember to mark the answer as accepted. – Ingo Bürk Apr 13 '14 at 21:18
  • Related question: if I know the string to replace is always 'foo' (so not a number or something that will break the JSON) which method is 'best'. – Andrew Downes Apr 13 '14 at 21:19
  • 1
    I would still prefer my method because it feels cleaner and isn't abusing JSON parsing, so it's also more obvious what's being done. Besides, it's a more future-proof solution (can you ensure your requirements won't change?). At the end of the day, it's an opinion and someone else might prefer the JSON method. But I don't. – Ingo Bürk Apr 13 '14 at 21:22