JSON.stringify replacer - how to get full path

后端 未结 5 1944
太阳男子
太阳男子 2021-01-07 07:35

Replacer in below code write on console current processed field name

相关标签:
5条回答
  • 2021-01-07 08:14

    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.com/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

    0 讨论(0)
  • 2021-01-07 08:17

    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.

    0 讨论(0)
  • 2021-01-07 08:20

    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.

    0 讨论(0)
  • 2021-01-07 08:22

    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 coming 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;
    }
    
    0 讨论(0)
  • 2021-01-07 08:23

    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)

    0 讨论(0)
提交回复
热议问题