【问题标题】:JSON.stringify deep objectsJSON.stringify 深层对象
【发布时间】:2012-12-01 10:13:23
【问题描述】:

我需要一个从任何参数构建 JSON 有效字符串的函数,但是:

  • 通过不添加两次对象来避免递归问题
  • 通过截断超过给定深度来避免调用堆栈大小问题

通常它应该能够处理大对象,但会截断它们。

作为参考,此代码失败:

var json = JSON.stringify(window);

避免递归问题很简单:

var seen = [];
return JSON.stringify(o, function(_, value) {
    if (typeof value === 'object' && value !== null) {
        if (seen.indexOf(value) !== -1) return;
        else seen.push(value);
    }
    return value;
});

但是现在,除了复制和更改Douglas Crockford's code 以跟踪深度之外,我没有找到任何方法来避免在非常深的对象(如window 或任何event)上发生堆栈溢出。有没有简单的解决办法?

【问题讨论】:

  • “非常深的对象”是什么意思?真的存在超出堆栈大小的对象(没有“递归属性”)吗?
  • 是的:例如window。我的代码中可能存在错误,但真正的问题是递归性,因为window 既是递归的又是深度的(这就是我给出代码的原因)。
  • 嗯,在 window 上尝试您的脚本时,我收到了 out of (heap) memory :-/
  • 这就是我想要避免的。 try/catch 在我的测试中阻止了它,但我什么都没有。我希望至少有前 N 个级别。如有必要,我也会削减大数组/字符串,但这很简单。
  • 你的实现依赖于原生的JSON.stringify。而且我担心在“属性树”中执行 DFS,而不是 BFS。如果您可以逐级扩展可能会变得更好(虽然不确定 - 结果只是 is 一个太大的字符串)

标签: javascript json stringify


【解决方案1】:

我认为您使用的格式不适合做您想做的事。将包含在窗口对象中的所有数据转换为单个 JSON 字符串假设您在构建她的过程中将此字符串保留在内存中,从而导致您遇到的问题。

您需要一种格式,让您能够在从窗口对象解析数据时发送数据,以便动态释放内存。就此而言,您应该使用 CSV、Text 或 VarStream (https://github.com/nfroidure/VarStream) 之类的内容。

您还可以遍历对象并尝试 JSON.stringify 它们在 try ... catch 中。如果尝试成功,则发送 JSON 文件,如果失败,则使用相同的 try ... catch 等遍历对象属性...但这是一个丑陋的解决方法,我不鼓励您使用。

【讨论】:

    【解决方案2】:

    你可以保持你所处的深度:

    function stringify(obj, currentDepth, maxDepth) {
      if (currentDepth == maxDepth) return '[Warning: max level reached]'
      var str = '{';
      for (var key in obj) {
        str += key + ': ' + typeof obj == 'object' ?
            stringify(obj[key], currentDepth + 1, maxDepth) :
            obj[key];
      }
      return str + '}'
    }
    

    (只是示例——显然这个 sn-p 没有检测到递归)

    【讨论】:

    • 这不会从任何对象构建 JSON 字符串。
    【解决方案3】:

    我做了我最初担心我必须做的事情:我采用了 Crockford 的代码并根据我的需要对其进行了修改。现在它构建 JSON 但处理

    • 循环
    • 对象太深
    • 数组太长
    • 例外(无法合法访问的访问器)

    如果有人需要,我创建了一个 GitHub 存储库:JSON.prune on GitHub

    代码如下:

    // JSON.pruned : a function to stringify any object without overflow
    // example : var json = JSON.pruned({a:'e', c:[1,2,{d:{e:42, f:'deep'}}]})
    // two additional optional parameters :
    //   - the maximal depth (default : 6)
    //   - the maximal length of arrays (default : 50)
    // GitHub : https://github.com/Canop/JSON.prune
    // This is based on Douglas Crockford's code ( https://github.com/douglascrockford/JSON-js/blob/master/json2.js )
    (function () {
        'use strict';
    
        var DEFAULT_MAX_DEPTH = 6;
        var DEFAULT_ARRAY_MAX_LENGTH = 50;
        var seen; // Same variable used for all stringifications
    
        Date.prototype.toPrunedJSON = Date.prototype.toJSON;
        String.prototype.toPrunedJSON = String.prototype.toJSON;
    
        var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
            escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
            meta = {    // table of character substitutions
                '\b': '\\b',
                '\t': '\\t',
                '\n': '\\n',
                '\f': '\\f',
                '\r': '\\r',
                '"' : '\\"',
                '\\': '\\\\'
            };
    
        function quote(string) {
            escapable.lastIndex = 0;
            return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
                var c = meta[a];
                return typeof c === 'string'
                    ? c
                    : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
            }) + '"' : '"' + string + '"';
        }
    
        function str(key, holder, depthDecr, arrayMaxLength) {
            var i,          // The loop counter.
                k,          // The member key.
                v,          // The member value.
                length,
                partial,
                value = holder[key];
            if (value && typeof value === 'object' && typeof value.toPrunedJSON === 'function') {
                value = value.toPrunedJSON(key);
            }
    
            switch (typeof value) {
            case 'string':
                return quote(value);
            case 'number':
                return isFinite(value) ? String(value) : 'null';
            case 'boolean':
            case 'null':
                return String(value);
            case 'object':
                if (!value) {
                    return 'null';
                }
                if (depthDecr<=0 || seen.indexOf(value)!==-1) {
                    return '"-pruned-"';
                }
                seen.push(value);
                partial = [];
                if (Object.prototype.toString.apply(value) === '[object Array]') {
                    length = Math.min(value.length, arrayMaxLength);
                    for (i = 0; i < length; i += 1) {
                        partial[i] = str(i, value, depthDecr-1, arrayMaxLength) || 'null';
                    }
                    v = partial.length === 0
                        ? '[]'
                        : '[' + partial.join(',') + ']';
                    return v;
                }
                for (k in value) {
                    if (Object.prototype.hasOwnProperty.call(value, k)) {
                        try {
                            v = str(k, value, depthDecr-1, arrayMaxLength);
                            if (v) partial.push(quote(k) + ':' + v);
                        } catch (e) { 
                            // this try/catch due to some "Accessing selectionEnd on an input element that cannot have a selection." on Chrome
                        }
                    }
                }
                v = partial.length === 0
                    ? '{}'
                    : '{' + partial.join(',') + '}';
                return v;
            }
        }
    
        JSON.pruned = function (value, depthDecr, arrayMaxLength) {
            seen = [];
            depthDecr = depthDecr || DEFAULT_MAX_DEPTH;
            arrayMaxLength = arrayMaxLength || DEFAULT_ARRAY_MAX_LENGTH;
            return str('', {'': value}, depthDecr, arrayMaxLength);
        };
    
    }());
    

    可以做的一个例子:

    var json = JSON.pruned(window);
    

    注意:与此答案中的代码相反,GitHub repository 在需要时更新(文档、兼容性、在 commonjs 或节点中用作模块、特定序列化等)。如果您需要此修剪功能,最好从存储库开始。

    【讨论】:

    【解决方案4】:

    您可以简单地使用Censor 函数,如下例所示:

    function censor(key, value) {
      if (typeof(value) == "string") {
        return undefined;
      }
      return value;
    }
    
    var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
    var jsonString = JSON.stringify(foo, censor);
    

    输出是{"week":45,"month":7}

    所以对于你的例子,如果你有一个值对象,你必须返回 undefined,它是一个窗口。

    【讨论】:

      【解决方案5】:

      我已经修改了@dystroy 的答案,添加:

      • 子属性的缩进。
      • 循环引用指向的指示。
      /**
       * Returns the JSON representation of an object.
       *
       * @param {value} object the object
       * @param {number} objectMaxDepth for objects, the maximum number of times to recurse into descendants
       * @param {number} arrayMaxLength for arrays, the maximum number of elements to enumerate
       * @param {string} indent the string to use for indentation
       * @return {string} the JSON representation
       */
      var toJSON = function(object, objectMaxDepth, arrayMaxLength, indent)
      {
          "use strict";
      
          /**
           * Escapes control characters, quote characters, backslash characters and quotes the string.
           *
           * @param {string} string the string to quote
           * @returns {String} the quoted string
           */
          function quote(string)
          {
              escapable.lastIndex = 0;
              var escaped;
              if (escapable.test(string))
              {
                  escaped = string.replace(escapable, function(a)
                  {
                      var replacement = replacements[a];
                      if (typeof (replacement) === "string")
                          return replacement;
                      // Pad the unicode representation with leading zeros, up to 4 characters.
                      return "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
                  });
              }
              else
                  escaped = string;
              return "\"" + escaped + "\"";
          }
      
          /**
           * Returns the String representation of an object.
           * 
           * Based on <a href="https://github.com/Canop/JSON.prune/blob/master/JSON.prune.js">https://github.com/Canop/JSON.prune/blob/master/JSON.prune.js</a>
           *
           * @param {string} path the fully-qualified path of value in the JSON object
           * @param {type} value the value of the property
           * @param {string} cumulativeIndent the indentation to apply at this level
           * @param {number} depth the current recursion depth
           * @return {String} the JSON representation of the object, or "null" for values that aren't valid
           * in JSON (e.g. infinite numbers).
           */
          function toString(path, value, cumulativeIndent, depth)
          {
              switch (typeof (value))
              {
                  case "string":
                      return quote(value);
                  case "number":
                      {
                          // JSON numbers must be finite
                          if (isFinite(value))
                              return String(value);
                          return "null";
                      }
                  case "boolean":
                      return String(value);
                  case "object":
                      {
                          if (!value)
                              return "null";
                          var valueIndex = values.indexOf(value);
                          if (valueIndex !== -1)
                              return "Reference => " + paths[valueIndex];
                          values.push(value);
                          paths.push(path);
                          if (depth > objectMaxDepth)
                              return "...";
      
                          // Make an array to hold the partial results of stringifying this object value.
                          var partial = [];
      
                          // Is the value an array?
                          var i;
                          if (Object.prototype.toString.apply(value) === "[object Array]")
                          {
                              // The value is an array. Stringify every element
                              var length = Math.min(value.length, arrayMaxLength);
      
                              // Whether a property has one or multiple values, they should be treated as the same
                              // object depth. As such, we do not increment the object depth when recursing into an
                              // array.
                              for (i = 0; i < length; ++i)
                              {
                                  partial[i] = toString(path + "." + i, value[i], cumulativeIndent + indent, depth,
                                      arrayMaxLength);
                              }
                              if (i < value.length)
                              {
                                  // arrayMaxLength reached
                                  partial[i] = "...";
                              }
                              return "\n" + cumulativeIndent + "[" + partial.join(", ") + "\n" + cumulativeIndent +
                                  "]";
                          }
      
                          // Otherwise, iterate through all of the keys in the object.
                          for (var subKey in value)
                          {
                              if (Object.prototype.hasOwnProperty.call(value, subKey))
                              {
                                  var subValue;
                                  try
                                  {
                                      subValue = toString(path + "." + subKey, value[subKey], cumulativeIndent + indent,
                                          depth + 1);
                                      partial.push(quote(subKey) + ": " + subValue);
                                  }
                                  catch (e)
                                  {
                                      // this try/catch due to forbidden accessors on some objects
                                      if (e.message)
                                          subKey = e.message;
                                      else
                                          subKey = "access denied";
                                  }
                              }
                          }
                          var result = "\n" + cumulativeIndent + "{\n";
                          for (i = 0; i < partial.length; ++i)
                              result += cumulativeIndent + indent + partial[i] + ",\n";
                          if (partial.length > 0)
                          {
                              // Remove trailing comma
                              result = result.slice(0, result.length - 2) + "\n";
                          }
                          result += cumulativeIndent + "}";
                          return result;
                      }
                  default:
                      return "null";
              }
          }
      
          if (indent === undefined)
              indent = "  ";
          if (objectMaxDepth === undefined)
              objectMaxDepth = 0;
          if (arrayMaxLength === undefined)
              arrayMaxLength = 50;
          // Matches characters that must be escaped
          var escapable =
              /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
          // The replacement characters
          var replacements =
              {
                  "\b": "\\b",
                  "\t": "\\t",
                  "\n": "\\n",
                  "\f": "\\f",
                  "\r": "\\r",
                  "\"": "\\\"",
                  "\\": "\\\\"
              };
          // A list of all the objects that were seen (used to avoid recursion)
          var values = [];
          // The path of an object in the JSON object, with indexes corresponding to entries in the
          // "values" variable.
          var paths = [];
          return toString("root", object, "", 0);
      };
      

      【讨论】:

        【解决方案6】:

        这是我的字符串化器,用于剥离 JSON 以安全记录具有循环引用、DOM 元素、角度范围或窗口的对象。

        通过将循环引用替换为 '' 来防止 TypeError: Converting circular structure to JSON

        阻止RangeError: Maximum call stack size exceeded。 但是,无论如何还是建议使用 maxDepth 或 filterObjects,因为序列化非常深的对象会耗费时间和空间,这可能会降低其对一般日志记录的可用性,甚至在用于测试时会导致测试浏览器断开连接。

        可选:

        • 限制对象检查深度(尚未实现),
        • 过滤对象(如窗口、测试框架、测试运行器),
        • 过滤 DOM 元素,
        • 过滤角度对象$attributes。

        来源+cmets:https://gist.github.com/iki/9371373

        【讨论】:

          【解决方案7】:

          如果您使用的是 Node.js,则可以使用 util.inspect,它接受深度参数。

          【讨论】:

          【解决方案8】:
          (function (input, level) {
              if (!input)
                  return input;
          
              level = level || 4;
          
              var objectsAlreadySerialized = [input],
                  objDepth = [input];
          
              return JSON.stringify(input, function (key, value) {
                  if (key) {
                      if (typeof value === 'object') {
                          if (objectsAlreadySerialized.indexOf(value) !== -1)
                              return undefined;
          
                          objectsAlreadySerialized.push(value);
                      }
          
                      if (objDepth.indexOf(this) === -1)
                          objDepth.push(this);
                      else while(objDepth[objDepth.length-1] !== this)
                          objDepth.pop();
          
                      if (objDepth.length > level)
                          return undefined;
                  }
          
                  return value;
              });
          })(window, 6)
          

          【讨论】:

          • 虽然此代码可能会回答问题,但提供有关它如何和/或为什么解决问题的额外上下文将提高​​答案的长期价值。
          【解决方案9】:

          这是一个尊重内置 JSON.stringify() 规则同时也限制深度的函数。此版本通过将循环引用设为 null 或使用可选回调来获取对象 ID(例如 GUID)来处理循环引用。

          function stringify(val, depth, replacer, space, onGetObjID) {
              depth = isNaN(+depth) ? 1 : depth;
              var recursMap = new WeakMap();
              function _build(val, depth, o, a, r) { // (JSON.stringify() has it's own rules, which we respect here by using it for property iteration)
                  return !val || typeof val != 'object' ? val
                      : (r = recursMap.has(val), recursMap.set(val,true), a = Array.isArray(val),
                         r ? (o=onGetObjID&&onGetObjID(val)||null) : JSON.stringify(val, function(k,v){ if (a || depth > 0) { if (replacer) v=replacer(k,v); if (!k) return (a=Array.isArray(v),val=v); !o && (o=a?[]:{}); o[k] = _build(v, a?depth:depth-1); } }),
                         o===void 0 ? (a?[]:{}) : o);
              }
              return JSON.stringify(_build(val, depth), null, space);
          }
          
          var o = {id:'SOMEGUID',t:true};
          var value={a:[12,2,{y:3,z:{o1:o}}],s:'!',b:{x:1,o2:o,o3:o}};
          
          console.log(stringify(value, 0, (k,v)=>{console.log('key:'+k+';val:',v); return v}, 2));
          console.log(stringify(value, 1, (k,v)=>{console.log('key:'+k+';val:',v); return v}, 2));
          console.log(stringify(value, 2, (k,v)=>{console.log('key:'+k+';val:',v); return v}, 2));
          console.log(stringify(value, 3, (k,v)=>{console.log('key:'+k+';val:',v); return v}, 2));
          console.log(stringify(value, 4, (k,v)=>{console.log('key:'+k+';val:',v); return v}, 2, (v)=>{return v.id}));
          
          {}
          
          {
            "a": [
              12,
              2,
              {}
            ],
            "s": "!",
            "b": {}
          }
          
          {
            "a": [
              12,
              2,
              {
                "y": 3,
                "z": {}
              }
            ],
            "s": "!",
            "b": {
              "x": 1,
              "o2": {},
              "o3": null
            }
          }
          
          {
            "a": [
              12,
              2,
              {
                "y": 3,
                "z": {
                  "o1": {}
                }
              }
            ],
            "s": "!",
            "b": {
              "x": 1,
              "o2": null,
              "o3": null
            }
          }
          
          {
            "a": [
              12,
              2,
              {
                "y": 3,
                "z": {
                  "o1": {
                    "id": "SOMEGUID",
                    "t": true
                  }
                }
              }
            ],
            "s": "!",
            "b": {
              "x": 1,
              "o2": "SOMEGUID",
              "o3": "SOMEGUID"
            }
          

          (取自我在这里的帖子https://stackoverflow.com/a/57193068/1236397

          这是一个 TypeScript 版本:

          /** A more powerful version of the built-in JSON.stringify() function that uses the same function to respect the
           * built-in rules while also limiting depth and supporting cyclical references.
           */
          export function stringify(val: any, depth: number, replacer: (this: any, key: string, value: any) => any, space?: string | number, onGetObjID?: (val: object) => string): string {
              depth = isNaN(+depth) ? 1 : depth;
              var recursMap = new WeakMap();
              function _build(val: any, depth: number, o?: any, a?: boolean, r?: boolean) {
                  return !val || typeof val != 'object' ? val
                      : (r = recursMap.has(val),
                          recursMap.set(val, true),
                          a = Array.isArray(val),
                          r ? (o = onGetObjID && onGetObjID(val) || null) : JSON.stringify(val, function (k, v) { if (a || depth > 0) { if (replacer) v = replacer(k, v); if (!k) return (a = Array.isArray(v), val = v); !o && (o = a ? [] : {}); o[k] = _build(v, a ? depth : depth - 1); } }),
                          o === void 0 ? (a?[]:{}) : o);
              }
              return JSON.stringify(_build(val, depth), null, space);
          }
          

          注意: 数组被视为字符串 - 原始值数组;因此,任何嵌套的对象项都被视为下一级而不是数组对象本身(很像字符串可以是字符数组,但它是一个实体)。

          更新:修复了空数组呈现为空对象的错误。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2018-02-28
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2022-08-18
            • 2011-08-26
            • 2013-11-07
            相关资源
            最近更新 更多