admin管理员组

文章数量:1240597

Replacer in below code write on console current processed field name

let a = { a1: 1, a2:1 }
let b = { b1: 2, b2: [1,a] }
let c = { c1: 3, c2: b }


let s = JSON.stringify(c, function (field,value) {
  console.log(field); // full path... ???
  return value;
});

Replacer in below code write on console current processed field name

let a = { a1: 1, a2:1 }
let b = { b1: 2, b2: [1,a] }
let c = { c1: 3, c2: b }


let s = JSON.stringify(c, function (field,value) {
  console.log(field); // full path... ???
  return value;
});

However I would like to get full "path" to field (not only its name) inside replacer function - something like this


c1
c2
c2.b1
c2.b2
c2.b2[0]
c2.b2[1]
c2.b2[1].a1
c2.b2[1].a2

How to do it?

Share Improve this question edited May 9, 2020 at 19:00 Kamil Kiełczewski asked May 8, 2020 at 14:14 Kamil KiełczewskiKamil Kiełczewski 92.5k34 gold badges395 silver badges370 bronze badges 2
  • "As a function, it takes two parameters: the key and the value being stringified. The object in which the key was found is provided as the replacer's this parameter." - You will have to find yourself a way how to generate the path from the key and this. – Andreas Commented May 8, 2020 at 14:28
  • stackoverflow./questions/49125398/… – Slai Commented May 8, 2020 at 14:55
Add a ment  | 

5 Answers 5

Reset to default 7

Decorator

replacerWithPath in snippet determine path using this (thanks @Andreas for this tip ), field and value and some historical data stored during execution (and this solution support arrays)

JSON.stringify(c, replacerWithPath(function(field,value,path) {
  console.log(path,'=',value); 
  return value;
}));

function replacerWithPath(replacer) {
  let m = new Map();

  return function(field, value) {
    let path= m.get(this) + (Array.isArray(this) ? `[${field}]` : '.' + field); 
    if (value===Object(value)) m.set(value, path);  
    return replacer.call(this, field, value, path.replace(/undefined\.\.?/,''))
  }
}


// Explanation fo replacerWithPath decorator:
// > 'this' inside 'return function' point to field parent object
//   (JSON.stringify execute replacer like that)
// > 'path' contains path to current field based on parent ('this') path
//   previously saved in Map
// > during path generation we check is parent ('this') array or object
//   and chose: "[field]" or ".field"
// > in Map we store current 'path' for given 'field' only if it 
//   is obj or arr in this way path to each parent is stored in Map. 
//   We don't need to store path to simple types (number, bool, str,...)
//   because they never will have children
// > value===Object(value) -> is true if value is object or array
//   (more: https://stackoverflow./a/22482737/860099)
// > path for main object parent is set as 'undefined.' so we cut out that
//   prefix at the end ad call replacer with that path


// ----------------
// TEST
// ----------------

let a = { a1: 1, a2: 1 };
let b = { b1: 2, b2: [1, a] };
let c = { c1: 3, c2: b };

let s = JSON.stringify(c, replacerWithPath(function(field, value, path) {
  // "this" has same value as in replacer without decoration
  console.log(path);
  return value;
}));

BONUS: I use this approach to stringify objects with circular references here

You can use custom walk function inside your replacer. Here's an example using a generator walk function:

const testObject = {a: 1, b: {a: 11, b: {a: 111, b: 222, c: 333}}, c: 3};

function* walk(object, path = []) {
    for (const [key, value] of Object.entries(object)) {
        yield path.concat(key);

        if (typeof value === 'object') yield* walk(value, path.concat(key));
    }
}

const keyGenerator = walk(testObject);

JSON.stringify(testObject, (key, value) => {
    const fullKey = key === '' ? [] : keyGenerator.next().value;

    // fullKey contains an array with entire key path
    console.log(fullKey, value);

    return value;
});

Console output:

  fullKey          |  value
-------------------|------------------------------------------------------------
  []               |  {"a":1,"b":{"a":11,"b":{"a":111,"b":222,"c":333}},"c":3}
  ["a"]            |  1
  ["b"]            |  {"a":11,"b":{"a":111,"b":222,"c":333}}
  ["b", "a"]       |  11
  ["b", "b"]       |  {"a":111,"b":222,"c":333}
  ["b", "b", "a"]  |  111
  ["b", "b", "b"]  |  222
  ["b", "b", "c"]  |  333
  ["c"]            |  3

This works, assuming the algorithm in replacer is depth-first and consistent across all browsers.

Based on the other answers I have this function which adds a third path argument to the call of replacer:

function replacerWithPath(replacer) {
  const m = new Map();

  return function (field, value) {
    const pathname = m.get(this);
    let path;

    if (pathname) {
      const suffix = Array.isArray(this) ? `[${field}]` : `.${field}`;

      path = pathname + suffix;
    } else {
      path = field;
    }

    if (value === Object(value)) {
      m.set(value, path);
    }

    return replacer.call(this, field, value, path);
  }
}

// Usage

function replacer(name, data, path) {
  // ...
}

const dataStr = JSON.stringify(data, replacerWithPath(replacer));

BONUS:

I also created this function to iterate through an object in depth and to be able to use the replace function like with JSON.stringify. The third argument to true will keep undefined values and empty objects.

It can be handy to modify and ignore values while iterating through an object, it returns the new object (without stringify).

function walkWith(obj, fn, preserveUndefined) {
  const walk = objPart => {
    if (objPart === undefined) {
      return;
    }

    let result;

    // TODO other types than object
    for (const key in objPart) {
      const val = objPart[key];
      let modified;

      if (val === Object(val)) {
        modified = walk(fn.call(objPart, key, val));
      } else {
        modified = fn.call(objPart, key, val);
      }

      if (preserveUndefined || modified !== undefined) {
        if (result === undefined) {
          result = {};
        }

        result[key] = modified;
      }
    }

    return result;
  };

  return walk(fn.call({ '': obj }, '', obj));
}

BONUS 2:

I use it to transform a data object ing from a form submission and containing files / arrays of files in mixed multipart, files + JSON.

function toMixedMultipart(data, bodyKey = 'data', form = new FormData()) {
  const replacer = (name, value, path) => {
    // Simple Blob
    if (value instanceof Blob) {
      form.append(path, value);

      return undefined;
    }

    // Array of Blobs
    if (Array.isArray(value) && value.every(v => (v instanceof Blob))) {
      value.forEach((v, i) => {
        form.append(`${path}[${i}]`, v);
      });

      return undefined;
    }

    return value;
  };

  const dataStr = JSON.stringify(data, replacerWithPath(replacer));
  const dataBlob = new Blob([dataStr], { type: 'application/json' });

  form.append(bodyKey, dataBlob);

  return form;
}

There's just not enough information available in the replacer. These two objects have different shapes but produce the same sequence of calls:

let c1 = { c1: 3, c2: 2 };
let c2 = { c1: { c2: 3 } };

const replacer = function (field, value) {
  console.log(field); // full path... ???
  return value;
};

JSON.stringify(c1, replacer);
// logs c1, c2
JSON.stringify(c2, replacer);
// logs c1, c2

You'll have to write something yourself.

Something like that. You need to adjust it for arrays. I think that you can do it yourself. The idea is clear.

let a = { a1: 1, a2:1 }
let b = { b1: 2, b2: [1,a] }
let c = { c1: 3, c2: b }

function iterate(obj, path = '') {
    for (var property in obj) {
        if (obj.hasOwnProperty(property)) {
            if (typeof obj[property] == "object") {
                iterate(obj[property], path + property + '.');
            }
            else {
                console.log(path + property);
            }
        }
    }
}

iterate(c)

本文标签: javascriptJSONstringify replacerhow to get full pathStack Overflow