递归JSON.stringify实现

时间:2014-03-11 18:10:26

标签: javascript recursion

我正在尝试学习Javascript中的递归,所以我想我会使用递归作为对自己的挑战来重写本地JSON.stringify函数。我几乎让我的代码工作了:

var my_stringify = function(obj){        
  value = obj[ Object.keys(obj)[0] ];
  index = Object.keys(obj)[0];

  delete obj[ Object.keys(obj)[0] ];

  // The value is just a simple string, not a nested object
  if (typeof value === 'string'){
    if (Object.keys(obj).length !== 0){
      // Continue recursion ..
      return '"' + index + '":"' + value + '",' + my_stringify(obj);
    }

    // This would be the base case with a string at the end. Stop recursion.
    return '"' + index + '":"' + value + '"}';
  }
  // The value is actually a nested object
  else{     
    if (Object.keys(obj).length !== 0){
    // Continue recursion ..
      return '"' + index + '":{' + my_stringify(value) + ',' + my_stringify(obj);
    }
    // This is the base case with a nested object at the end. Stringify it and end recursion.
    return '"' + index + '":{' + my_stringify(value) + '}';  
  }
}

除了我的答案中第一个{丢失的事实,我无法弄清楚如何解决这个错误。

E.g。 my_stringify({foo: 'bar'})返回"foo":"bar"}而不是{"foo":"bar"}

另外,我知道我正在完全破坏原始对象,有没有办法发送到递归缩小版本的原始对象而不删除任何东西(类似obj.slice(1))?

任何建议都将不胜感激!

7 个答案:

答案 0 :(得分:5)

旧问题的新答案

这里有一些非常糟糕的答案,即使是最简单的例子也是如此。这个答案的目的是详尽地回答这个问题,并演示即使在处理各种数据类型时,这种方法如何扩展......

转角案件

此函数对非空数据的constructor属性进行简单的案例分析,并进行相应编码。它设法涵盖了您不太可能考虑的许多极端案例,例如

  • JSON.stringify(undefined)返回undefined
  • JSON.stringify(null)返回'null'
  • JSON.stringify(true)返回'true'
  • JSON.stringify([1,2,undefined,4])返回'[1,2,null,4]'
  • JSON.stringify({a: undefined, b: 2})返回'{ "b": 2 }'
  • JSON.stringify({[undefined]: 1})返回'{ "undefined": 1 }'
  • JSON.stringify({a: /foo/})返回{ "a": {} }

因此,为了验证我们的stringifyJSON函数实际上是否正常工作,我不会直接测试它的输出。相反,我将编写一个小test方法,以确保我们编码的JSON的JSON.parse实际返回我们的原始输入值

// we really only care that JSON.parse can work with our result
// the output value should match the input value
// if it doesn't, we did something wrong in our stringifier
const test = data => {
  return console.log(JSON.parse(stringifyJSON(data)))
}

test([1,2,3])     // should return [1,2,3]
test({a:[1,2,3]}) // should return {a:[1,2,3]}
  

免责声明:很明显,我要分享的代码并不打算用作JSON.stringify的实际替代 - 我们可能无法解决的无数角落案例。相反,这个代码是共享的,以提供我们如何进行此类任务的演示。可以轻松地将其他角落案例添加到此功能中。

Runnable演示

不用多说,这里有一个可运行的演示中的stringifyJSON,可以验证几种常见情况的出色兼容性



const stringifyJSON = data => {
  if (data === undefined)
    return undefined
  else if (data === null)
    return 'null'
  else if (data.constructor === String)
    return '"' + data.replace(/"/g, '\\"') + '"'
  else if (data.constructor === Number)
    return String(data)
  else if (data.constructor === Boolean)
    return data ? 'true' : 'false'
  else if (data.constructor === Array)
    return '[ ' + data.reduce((acc, v) => {
      if (v === undefined)
        return [...acc, 'null']
      else
        return [...acc, stringifyJSON(v)]
    }, []).join(', ') + ' ]'
  else if (data.constructor === Object)
    return '{ ' + Object.keys(data).reduce((acc, k) => {
      if (data[k] === undefined)
        return acc
      else
        return [...acc, stringifyJSON(k) + ':' + stringifyJSON(data[k])]
    }, []).join(', ') + ' }'
  else
    return '{}'
}

// round-trip test and log to console
const test = data => {
  return console.log(JSON.parse(stringifyJSON(data)))
}

test(null)                               // null
test('he said "hello"')                  // 'he said "hello"'
test(5)                                  // 5
test([1,2,true,false])                   // [ 1, 2, true, false ]
test({a:1, b:2})                         // { a: 1, b: 2 }
test([{a:1},{b:2},{c:3}])                // [ { a: 1 }, { b: 2 }, { c: 3 } ]
test({a:[1,2,3], c:[4,5,6]})             // { a: [ 1, 2, 3 ], c: [ 4, 5, 6 ] }
test({a:undefined, b:2})                 // { b: 2 }
test({[undefined]: 1})                   // { undefined: 1 }
test([[["test","mike",4,["jake"]],3,4]]) // [ [ [ 'test', 'mike', 4, [ 'jake' ] ], 3, 4 ] ]




答案 1 :(得分:4)

您需要将递归视为更深入到对象而不实际更改对象。看起来你正在尝试使用递归来横向移动到对象内部。

我已经编写了一个处理基本对象(没有数组或函数)的stringify版本。

以下是fiddle

以下是代码:

var my_stringify2 = function (obj) {
    var objKeys = Object.keys(obj);
    var keyValueArray = new Array();
    for (var i = 0; i < objKeys.length; i++) {
        var keyValueString = '"' + objKeys[i] + '":';
        var objValue = obj[objKeys[i]];
        keyValueString = (typeof objValue == "string") ? 
            keyValueString = keyValueString + '"' + objValue + '"' : 
            keyValueString = keyValueString + my_stringify2(objValue);
        keyValueArray.push(keyValueString);
    }
    return "{" + keyValueArray.join(",") + "}";
}

您希望递归为您完成大部分工作,您只需要处理基本条件(您已经拥有)。在我的函数中,两个可接受的条件是字符串和对象。

现场处理字符串,并以递归方式将对象传递给函数。

这是关键。您将相同的对象重复传递给函数,删除处理过的元素,直到到达对象完全消失的位置。

我所做的是传递特定属性的值,如果它是一个对象。如果它是一个字符串,只需将其添加到字符串中并继续移动。

查看代码,如果您有任何疑问,请与我们联系。请注意,我传入的对象有一个嵌套对象。

my_stringify2({
    foo: 'bar',
    bar: 'foo',
    foobar: {
        foo: 'bar',
        bar: 'foo'
    }
});

,结果是正确的json

{"foo":"bar","bar":"foo","foobar":{"foo":"bar","bar":"foo"}} 

如果您希望完全避免使用for循环,可以执行以下操作

jsfiddle

在这一个中你像普通一样传递对象,但递归地传递一个键数组,从每个属性的键数组中删除一个元素。

有点复杂,所以我添加了评论

var my_stringify2 = function (obj, objKeys) {
    var str = "";
    // keys haven't been loaded, either first pass, or processing a value of type object
    if (objKeys == undefined) { 
        objKeys = Object.keys(obj);
        str = "{"
    } else {
        // if keys array exists and is empty, no more properties to evaluate, return the end bracket
        if (objKeys.length == 0) {
            return "}";
        // array exists and isn't empty, that means it's a property and not the first property, add a comma    
        } else {
            str = ",";
        }
    }
    // add the property name
    str += '"' + objKeys[0] + '":';
    // get the value
    var objValue = obj[objKeys[0]];
    // if the value type is string, add the string, if it's an object, call this function again, but leave the objKeys undefined
    str +=
        (typeof objValue == "string") ? 
        '"' + objValue + '"' : 
         my_stringify2(objValue);    
    // remove the first element fromt the keys array
    objKeys.splice(0,1);
    //call the function for the next property
    return str + my_stringify2(obj, objKeys);
}

答案 2 :(得分:2)

这个问题已经被回答了好几次了,但这是另一个解决方案:

使用es6:

let oldStringify = JSON.stringify;
JSON.stringify = (obj, replacer, space) => oldStringify(obj, replacer || ((key, value) => {if(key && value === obj) return "[recursive]"; return value;}), space)

答案 3 :(得分:1)

我在面试问题中被问过这个问题,这就是我提出的问题。 一种可理解的递归方法:

​
function stringify(input) {
  var arrVals = [];
  Object.keys(input).forEach(function(keyName) {
    let val = input[keyName];
    if (typeof val !== 'undefined' && typeof val !== 'function') {
      arrVals.push(getQuotedString(keyName) + ":" + getString(val));
    }
  });
  return '{' + arrVals.join(',') + '}';
}

function getString(val) {
  switch (typeof val) {
    case 'string':
      return getQuotedString(val);
      break;
    
    case 'number':
    case 'boolean':
      return val;
      break;
    
    case 'object':
      if (val === null) {
        return "null";
      }
      
      if (Array.isArray(val)) {
        let arrString = []
        for (let i = 0; i < val.length; i++) {
          arrString.push(getString(val[i]));
        }
        return "[" + arrString.join(',') + "]";
      }
      
      return stringify(val);
      break;
  }
}

function getQuotedString(str) {
  return '"' + str + '"';
}

使用以下obj进行测试:

​
var input = {
  "a": 1,
  "b": 'text',
  "c": {
    "x": 1,
    "y": {
      "x": 2
    }
  },
  "d": false,
  "e": null,
  "f": undefined,
  "g": [1, "text", {
    a: 1,
    b: 2
  }, null]
};

答案 4 :(得分:0)

从根本上说,你通过切断第一个属性进行字符串化,然后对其进行字符串化,然后递归对象的其余部分。恕我直言这不是要走的路,递归的唯一原因是当有嵌套对象时,否则你应该遍历属性。在你完成它之后,你已经让你更难以判断你是否在对象的开头并且应该用你的字符串返回缺少{的内容。

在半伪代码中(让你自己做一些工作),你需要这样的东西:

var my_stringify = function(obj) {
    // check first for null / undefined / etc and return
    var myJSON = "{";
    // iterate through all the properties of the object
    for (var p in obj) {
        if (obj.hasOwnProperty(p)) {
            // check to see if this property is a string, number, etc
            if (//...) {
                myJSON += // the JSON representation of this value using p and obj[p]
            }
            if (// test for nested object) {
                myJSON += my_stringify(obj[p]);    // this is recursion!
            }
            if (// test for arrays) {
                // arrays also need special handling and note that they might
                // include objects or other arrays - more chances for recursion!
            }
            // note: functions should be ignored, they aren't included in JSON
        }
    }
    return myJSON + "}";
}

答案 5 :(得分:0)

我不同意@ Bergi断言常规的旧递归不适合这个。就像我在评论中所说,你可以通过将索引作为参数传递给函数来避免使用for循环。这是一种非常常见的技术,可以防止您需要复制或修改数据结构。

这是我对这种实现的尝试。正如你所看到的那样,它非常简单(令我惊讶的是,它有效!):

function jsonify(obj, idx) {
  var json, objStr = toString.call(obj);

  // Handle strings
  if(objStr == '[object String]') { return '"' + obj + '"' }

  idx = idx || 0

  // Handle arrays
  if(objStr == '[object Array]') {
    if(idx >= obj.length) {
      // The code below ensures we'll never go past the end of the array,
      // so we can assume this is an empty array
      return "[]"
    }

    // JSONify the value at idx
    json = jsonify( obj[idx] )

    if(idx < obj.length - 1) {
      // There are items left in the array, so increment the index and
      // JSONify the rest
      json = json + "," + jsonify( obj, idx + 1 )
    }

    // If this is the first item in the array, wrap the result in brackets
    if(idx === 0) { return "[" + json + "]" }

    return json
  }

  // Handle objects
  if(obj === Object(obj)) {
    var keys = Object.keys(obj)
    var key = keys[idx]

    // JSONify the key and value
    json = '"' + key + '":' + jsonify( obj[key] )

    if(idx < keys.length - 1) {
      // There are more keys, so increment the index and JSONify the rest
      return json + "," + jsonify( obj, idx + 1 )
    }

    // If this is the first key, wrap the result in curly braces
    if(idx === 0) { return "{" + json + "}" }

    return json
  }

  return obj.toString() // Naively handle everything else
}

var items = [ 9, "nine", { "key": [], "key2": { "subkey": 3.333 } } ]

console.log("OUTPUT", jsonify(items))
// => OUTPUT [9,"nine","key":[],"key2":{"subkey":3.333}]

有很多方法可以收紧(而且我确定也有一些错误),但你明白了。

答案 6 :(得分:0)

我以递归方式创建了 JSON.stringify() 方法的整个实现。链接在这里: https://javascript.plainenglish.io/create-your-own-implementation-of-json-stringify-simiplied-version-8ab6746cdd1

Github 链接:https://github.com/siddharth-sunchu/native-methods/blob/master/JSONStringfy.js


const JSONStringify = (obj) => {

  const isArray = (value) => {
    return Array.isArray(value) && typeof value === 'object';
  };

  const isObject = (value) => {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
  };

  const isString = (value) => {
    return typeof value === 'string';
  };

  const isBoolean = (value) => {
    return typeof value === 'boolean';
  };

  const isNumber = (value) => {
    return typeof value === 'number';
  };

  const isNull = (value) => {
    return value === null && typeof value === 'object';
  };

  const isNotNumber = (value) => {
    return typeof value === 'number' && isNaN(value);
  };

  const isInfinity = (value) => {
    return typeof value === 'number' && !isFinite(value);
  };

  const isDate = (value) => {
    return typeof value === 'object' && value !== null && typeof value.getMonth === 'function';
  };

  const isUndefined = (value) => {
    return value === undefined && typeof value === 'undefined';
  };

  const isFunction = (value) => {
    return typeof value === 'function';
  };

  const isSymbol = (value) => {
    return typeof value === 'symbol';
  };

  const restOfDataTypes = (value) => {
    return isNumber(value) || isString(value) || isBoolean(value);
  };

  const ignoreDataTypes = (value) => {
    return isUndefined(value) || isFunction(value) || isSymbol(value);
  };

  const nullDataTypes = (value) => {
    return isNotNumber(value) || isInfinity(value) || isNull(value);
  }

  const arrayValuesNullTypes = (value) => {
    return isNotNumber(value) || isInfinity(value) || isNull(value) || ignoreDataTypes(value);
  }

  const removeComma = (str) => {
    const tempArr = str.split('');
    tempArr.pop();
    return tempArr.join('');
  };


  if (ignoreDataTypes(obj)) {
    return undefined;
  }

  if (isDate(obj)) {
    return `"${obj.toISOString()}"`;
  }

  if(nullDataTypes(obj)) {
    return `${null}`
  }

  if(isSymbol(obj)) {
    return undefined;
  }


  if (restOfDataTypes(obj)) {
    const passQuotes = isString(obj) ? `"` : '';
    return `${passQuotes}${obj}${passQuotes}`;
  }

  if (isArray(obj)) {
    let arrStr = '';
    obj.forEach((eachValue) => {
      arrStr += arrayValuesNullTypes(eachValue) ? JSONStringify(null) : JSONStringify(eachValue);
      arrStr += ','
    });

    return  `[` + removeComma(arrStr) + `]`;
  }

  if (isObject(obj)) {
      
    let objStr = '';

    const objKeys = Object.keys(obj);

    objKeys.forEach((eachKey) => {
        const eachValue = obj[eachKey];
        objStr +=  (!ignoreDataTypes(eachValue)) ? `"${eachKey}":${JSONStringify(eachValue)},` : '';
    });
    return `{` + removeComma(objStr) + `}`;
  }
};