As far as I know, there's no built-in method in JavaScript to deep filter a nested data structure like JSON.stringify()
does when given a replacer callback. That said, it's not too hard to write your own:
function filterClone (data, replacer) {
// return primitives unchanged
if ( !(data instanceof Object) ) return data;
// don't try to clone anything except plain objects and arrays
var proto = Object.getPrototypeOf(data);
if (proto !== Object.prototype && proto !== Array.prototype) return data;
// it's a "plain object" or an array; clone and filter it!
var clone = (proto === Object.prototype ? {} : []);
for (var prop in data) {
// safety: ignore inherited properties, even if they're enumerable
if (!data.hasOwnProperty(prop)) continue;
// call the replacer to let it modify or exclude the property
var value = replacer(prop, data[prop]);
if (value === undefined) continue;
if (value instanceof Object) value = filterClone(value, replacer);
clone[prop] = value;
}
return clone;
}
The recursive function above will deep-clone any "JSON-like" data structure (i.e. one that consists of only plain {}
objects, []
arrays and primitive types like numbers, strings and booleans), filtering it using exactly the same kind of replacer callback as JSON.stringify()
. That is, given a JSON-like object original
as in your question, you can create a filtered copy of it like this:
function replacer (key, value) {
if (key === "?xml" || key === "@xmlns") return undefined;
else return value;
}
var filtered = filterClone(original, replacer);
Note that the "deep clone" created by this function is not perfect (because it's hard to clone arbitrary objects in JavaScript), and there are a few corner cases to beware of:
This function only clones objects directly inheriting from Object
or Array
(including "plain objects" and arrays created using {}
and []
). Anything else, including primitive values and any objects of any other type, are simply copied to the output structure without being cloned.
For primitive values, this is harmless, since those are immutable anyway; but if your data structure happens to include, say, Date
objects (which are mutable), then those will not be automatically cloned. Thus, modifying the dates in the cloned data structure (using e.g. setTime()
) can affect the dates in the original, and vice versa:
var original = { "date" : new Date("1970-01-01T00:00:00.000Z") };
var clone = filterClone( original, function (key, val) { return val } );
console.log( original === clone ); // -> false
console.log( original.date === clone.date ); // -> true (!)
console.log( original.date.getTime() ); // -> 0
clone.date.setYear(2016);
console.log( original.date.getTime() ); // -> 1451606400000
Of course, you can work around this in your replacer callback, e.g. like this:
function replacer (key, value) {
// this is how you clone a date in JS:
if (value instanceof Date) value = new Date(value.getTime());
return value;
}
Also, the filterClone()
function above will not clone any non-enumerable properties in your objects, and any (enumerable) properties with a non-standard descriptor will be replaced by standard properties (with no getters, setters, write restrictions, etc.) in the clone. Now, normal plain objects created using the {}
object literal syntax should not have any such fancy property descriptors, but if you added any afterwards, be aware that they will not be cloned. (Obviously enough, symbols will also not be cloned.)
If your original object contains a reference to the same plain object or array twice, they will become separate objects / arrays in the clone. For example:
var sharedObject = {};
var original = { "foo" : sharedObject, "bar" : sharedObject };
var clone = filterClone( original, function (key, val) { return val } );
console.log( original.foo === original.bar ); // -> true
console.log( clone.foo === clone.bar ); // -> false
Also, if your objects are not well founded (e.g. if they contain a reference to themselves), it's possible for filterClone()
to get stuck in an infinite recursion forever (or until it hits the recursion limit, anyway). For example, here's one simple way to create an object that cannot be cloned using filterClone()
:
var foo = {};
foo.foo = foo;
Finally, due to the JSON.stringify()
replacer callback interface (which the code above follows faithfully) using undefined
as a special value meaning "omit this property", it's not possible to properly clone an object containing undefined
values using filterClone()
. Cloning false
or null
values works fine, though:
var original = { "foo" : undefined, "bar" : null };
var clone = filterClone( original, function (key, val) { return val } );
console.log( clone ); // -> Object { bar: null }
(While testing this, however, I did discover a bug in my original implementation: apparently, Object.getPrototypeOf(null)
throws a TypeError. Moving the instanceof
check before the prototype check fixed this issue.)
Still, except for the last one, most of these issues are shared by most other JS deep clone implementations, including JSON.parse( JSON.stringify( obj ) )
. As noted above, deep cloning arbitrary objects is hard, particularly in a language like JavaScript that has no standard way to mark an object as cloneable, and which is so flexible in allowing objects to contain all sorts of weird properties. Still, for "simple" objects (notably including anything returned by parsing a valid JSON string), this function should do the trick.
Ps. Of course, one way to side-step most of these problems is to do the filtering in place:
function filterInplace (data, replacer) {
// don't try to filter anything except plain objects and arrays
if ( !(data instanceof Object) ) return;
var proto = Object.getPrototypeOf(data);
if (proto !== Object.prototype && proto !== Array.prototype) return;
// it's a "plain object" or an array; filter it!
for (var prop in data) {
// safety: ignore inherited properties, even if they're enumerable
if (!data.hasOwnProperty(prop)) continue;
// call the replacer to let it modify or exclude the property
data[prop] = replacer(prop, data[prop]);
if (data[prop] instanceof Object) filterInplace(data[prop], replacer);
if (data[prop] === undefined) delete data[prop];
}
}
This function does not return anything; rather, it just modifies the data structure passed in as the first parameter. It does have a few quirks of its own:
- It still doesn't even try to filter anything but "plain" objects and arrays.
- It still breaks on non-well-founded structures (like any recursive solution that doesn't explicitly keep track of which objects it has visited already).
- For the same reason, if the same object is referenced twice from the data structure, it will be filtered twice. (However, since no cloning is involved, the two references will keep pointing to the same object.)
- It still doesn't filter non-enumerable properties (or symbols); they will just remain untouched as they are.
- It may also fail to properly filter other properties with a non-default descriptor (e.g. if they're non-writable, or have getters or setters that do something funny).
There's also a minor difference between the filterClone()
and filterInplace()
functions above regarding the removal of elements from the end of an array: filterClone()
will shorten the array, while filterInplace()
will always leave a null value where the removed element was. It's a bit debatable what the "correct" behavior in this case should be; FWIW, JSON.stringify()
doesn't shorten the array, either.