Возможно, вы уже знакомы с функцией JSON.stringify, которая может быть полезна при сравнении объектов, реализации RESTFUL API или просто глубоком клонировании объекта javascript (хотя это не рекомендуется).

В этой статье мы поговорим о том, как создать нашу собственную упрощенную версию этого метода, и узнаем, как шаг за шагом улучшать его реализацию, охватывая все больше и больше случаев по мере нашего продвижения.

Если вы не знакомы с этой функцией, давайте посмотрим, что говорит о ней MDN:

Метод JSON.stringify() преобразует объект или значение JavaScript в строку JSON, при необходимости заменяя значения, если указана функция-заменитель, или, возможно, включая только указанные свойства, если указан массив-заменитель.

Его синтаксис можно записать так:

JSON.stringify(value[, replacer[, space]])

Где «значение» - это объект или значение, которое мы хотим преобразовать в строку. Мы можем опустить остальные 2 параметра в этой статье, чтобы упростить задачу.

Данные тестирования

Рассмотрим этот случай:

const sampleObj = {
   "name": "Juan",
   "age": 29,
   "address": {
      "street": "Street 1",
      "number": 3
   }
}

Если мы применим к этому объекту функцию origin JSON.stringify (), мы получим следующее:

{"name":"Juan","age":29,"address":{"street":"Street 1","number":3}}

Как видите, это довольно просто. Он добавляет к атрибутам двойные кавычки, и, если значение является строкой, добавляет и их. В этом конкретном примере мы рассмотрим только эти 3 типа данных: Number, String и Object. Мы оставим функции, даты, неопределенные значения и т. Д., Просто для простоты, хотя я рекомендую вам прочитать документацию JSON.stringify, чтобы увидеть, как она ведет себя с этими и другими типами.

Функция

Начнем с этого:

function stringify(obj) {
    let objString = '';
    // We add the opening curly brace
    objString += '{';
    for (const key in obj) {
        const value = obj[key];
        
        objString += `"${key}":`;
        
        if (typeof obj[key] === 'object') {
            objString += `${stringify(value)}`;
        } else if (typeof value === 'string') {
            objString += `"${value}"`;
        } else if (typeof obj[key] === 'number') {
            objString += `${value}`;
        }
        
        // We add the comma
        objString += `,`;
    }
    // We add the closing curly brace
    objString += '}';
    return objString;
}

Как видите, мы вручную добавляем фигурные скобки, а затем перебираем каждый из ключей в объекте и, в зависимости от его типа, печатаем его значение после печати ключа, заключенного в двойные кавычки. Если ключ содержит объект, мы используем рекурсию, чтобы снова вызвать функцию stringify и повторить процесс.

Если мы выполним эту функцию с нашим sampleObj, результат будет следующим:

{"name":"Juan","age":29,"address":{"street":"Street 1","number":3,},}

Это хорошо для начала, но если вы присмотритесь, вы увидите, что все запятые в конце портят красоту строки JSON. Плохой!

Попробуем исправить это:

function stringify(obj) {
    let objString = '';
    
    // We get the last key of this object
    const lastKey = Object.keys(obj).pop();
    // We add the first curly brace
    objString += '{';
    for (const key in obj) {
        const value = obj[key];
        
        objString += `"${key}":`;
        
        if (typeof obj[key] === 'object') {
            objString += `${stringify(value)}`;
        } else if (typeof value === 'string') {
            objString += `"${value}"`;
        } else if (typeof obj[key] === 'number') {
            objString += `${value}`;
        }
        
        // We add the comma
        if (key !== lastKey) {
            objString += ',';
        }
    }
    // We add the last curly brace
    objString += '}';
    return objString;
}

Ого, это было легко. Мы просто получаем последний ключ для объекта, и при итерации, если текущий элемент принадлежит последнему ключу, мы не добавляем запятую. Вы можете придумать лучший способ сделать это? Пока этого будет достаточно.

Давайте посмотрим на результат этой функции, снова используя наш sampleObj:

{"name":"Juan","age":29,"address":{"street":"Street 1","number":3}}

Отлично! Похоже, что это уже сделано, но ... Что произойдет, если вы отправите строку или число вместо объекта? Посмотрим, как себя ведет JSON.stringify:

> JSON.stringify("test");
"test"

> JSON.stringify(42);
42

Если бы мы выполняли нашу функцию с этими значениями, мы бы получили другой результат, давайте посмотрим:

> stringify("test");
{"0":"t","1":"e","2":"s","3":"t"}
> stringify(42);
{}

Давайте попробуем исправить это, нам нужно будет проверить тип перед повторением объекта. Вот как бы это выглядело:

function stringify(value) {
    const lastKey = Object.keys(value).pop();
    let objString = '';
    if (typeof value === 'object') {
        // We add the first curly brace
        objString += '{';
        for (const key in value) {
            objString += `"${key}":${stringify(value[key])}`;
            
            // We add the comma
            if (key !== lastKey) {
                objString += ',';
            }
        }
        // We add the last curly brace
        objString += '}';
    } else if (typeof value === 'string') {
        objString += `"${value}"`;
    } else if (typeof value === 'number') {
        objString += `${value}`;
    }
    return objString;
}

Выполнено. Как видите, мы переместили проверку типа за пределы for итератора для текущего объекта. Мы также изменили имя параметра с obj на value, потому что теперь оно может быть строкой или числом.

Давайте попробуем это на нескольких примерах:

> stringify("test");
"test"
> stringify(42);
42
> stringify(sampleObj)
{"name":"Juan","age":29,"address":{"street":"Street 1","number":3}}

Похоже, теперь это работает с простыми объектами, числами и строками, и мы подошли к концу этой статьи, но я оставлю вам несколько вопросов, чтобы вы подумали и попытались реализовать свои собственные решения:

Проблема круговой ссылки

Если вы знакомы с методами сериализации, возможно, вы слышали о «циклической ссылке» или, возможно, «бесконечном цикле». Это происходит, когда вы пытаетесь сериализовать объект, у которого есть ссылка на другой объект, у которого есть ссылка на первый! Звучит безумно? Давай увидим это.

const sampleObj = {
    name: 'Juan',
    age: 29,
    address: {
        street: 'Street 1',
        number: 3,
    }
};
const objTwo = {
     name: 'Two',
     inside: sampleObj
};
sampleObj.outside = objTwo;

Если мы попытаемся отправить sampleObj сейчас нашей stringify функции, мы получим ошибку. Большинство браузеров скажут:

Uncaught RangeError: Maximum call stack size exceeded

Это происходит потому, что мы используем рекурсию при нахождении объекта, и, следовательно, это создаст бесконечный цикл. Вы можете придумать какое-нибудь решение для этого? Что бы вы сделали? Мы можем рассказать об этом в другой статье, но было бы неплохо, если бы вы могли предложить решение этой проблемы или даже поделиться своей собственной функцией Stringify.

Следующие шаги

  • Решите проблему с круговой ссылкой.
  • Добавьте поддержку «логических» значений.
  • Добавьте поддержку Date и других объектов, реализующих функцию toJson().

Заключение

Несмотря на то, что JSON.stringify поддерживается большинством браузеров, вам будет полезно поиграть и попытаться увидеть, как реализованы собственные методы. Это может помочь вам понять, что происходит за кулисами, и, может быть, однажды вы улучшите одну из существующих функций, и ваш код будет включен в следующий выпуск EcmaScript, кто знает?

Далее читает