Превращение строк JSON в объекты с помощью методов - PullRequest
25 голосов
/ 13 ноября 2011

У меня есть приложение, которое позволяет пользователям создавать объекты и сохранять их (в виде таблицы MySQL в виде строк) для последующего использования.Объектом может быть:

function Obj() {
    this.label = "new object";
}

Obj.prototype.setLabel = function(newLabel) {
    this.label = newLabel;
}

Если я использую JSON.stringify для этого объекта, я получу информацию только о Obj.label (строковый объект будет иметь такую ​​строку, как {label: "new object"}.Эта строка, и я хочу, чтобы мой пользователь мог получить объект позже, метод setLabel будет потерян.

Поэтому мой вопрос: как я могу создать экземпляр объекта, чтобы он сохранил свойствахранится благодаря JSON.stringify, но также возвращает различные методы, которые должны принадлежать его прототипу. Как бы вы это сделали? Я думал о чем-то вроде «создать пустой объект» и «объединить его с сохраненными свойствами»,но я не могу заставить его работать.

Ответы [ 7 ]

20 голосов
/ 13 ноября 2011

Для этого вам понадобится функция «reviver» при разборе строки JSON (и функция «replacer» или функция toJSON в прототипе вашего конструктора при его создании). См. Раздел 15.12.2 и 15.12.3 спецификации. Если ваша среда еще не поддерживает собственный синтаксический анализ JSON, вы можете использовать один из парсеров Крокфорда (Крокфорд является изобретателем JSON), которые также поддерживают функции "reviver".

Вот простой сделанный на заказ пример, который работает с браузерами, совместимыми с ES5 (или библиотеками, имитирующими поведение ES5) ( живая копия , работает в Chrome или Firefox или аналогичных), но посмотрите на пример для более обобщенное решение.

// Our constructor function
function Foo(val) {
  this.value = val;
}
Foo.prototype.nifty = "I'm the nifty inherited property.";
Foo.prototype.toJSON = function() {
  return "/Foo(" + this.value + ")/";
};

// An object with a property, `foo`, referencing an instance
// created by that constructor function, and another `bar`
// which is just a string
var obj = {
  foo: new Foo(42),
  bar: "I'm bar"
};

// Use it
display("obj.foo.value = " + obj.foo.value);
display("obj.foo.nifty = " + obj.foo.nifty);
display("obj.bar = " + obj.bar);

// Stringify it with a replacer:
var str = JSON.stringify(obj);

// Show that
display("The string: " + str);

// Re-create it with use of a "reviver" function
var obj2 = JSON.parse(str, function(key, value) {
  if (typeof value === "string" &&
      value.substring(0, 5) === "/Foo(" &&
      value.substr(-2) == ")/"
     ) {
    return new Foo(value.substring(5, value.length - 2));
  }
  return value;
});

// Use the result
display("obj2.foo.value = " + obj2.foo.value);
display("obj2.foo.nifty = " + obj2.foo.nifty);
display("obj2.bar = " + obj2.bar);

Обратите внимание на toJSON на Foo.prototype и функцию, которую мы передаем в JSON.parse.

Тем не менее, проблема в том, что reviver тесно связан с конструктором Foo. Вместо этого вы можете использовать универсальный каркас в вашем коде, где любая функция-конструктор может поддерживать fromJSON (или аналогичную) функцию, и вы можете использовать только один обобщенный reviver.

Вот пример обобщенного реверификатора, который ищет свойство ctor и свойство data и вызывает ctor.fromJSON, если найден, передавая полное полученное значение ( живой пример ) :

// A generic "smart reviver" function.
// Looks for object values with a `ctor` property and
// a `data` property. If it finds them, and finds a matching
// constructor that has a `fromJSON` property on it, it hands
// off to that `fromJSON` fuunction, passing in the value.
function Reviver(key, value) {
  var ctor;

  if (typeof value === "object" &&
      typeof value.ctor === "string" &&
      typeof value.data !== "undefined") {
    ctor = Reviver.constructors[value.ctor] || window[value.ctor];
    if (typeof ctor === "function" &&
        typeof ctor.fromJSON === "function") {
      return ctor.fromJSON(value);
    }
  }
  return value;
}
Reviver.constructors = {}; // A list of constructors the smart reviver should know about  

Чтобы избежать необходимости повторять общую логику в функциях toJSON и fromJSON, у вас могут быть общие версии:

// A generic "toJSON" function that creates the data expected
// by Reviver.
// `ctorName`  The name of the constructor to use to revive it
// `obj`       The object being serialized
// `keys`      (Optional) Array of the properties to serialize,
//             if not given then all of the objects "own" properties
//             that don't have function values will be serialized.
//             (Note: If you list a property in `keys`, it will be serialized
//             regardless of whether it's an "own" property.)
// Returns:    The structure (which will then be turned into a string
//             as part of the JSON.stringify algorithm)
function Generic_toJSON(ctorName, obj, keys) {
  var data, index, key;

  if (!keys) {
    keys = Object.keys(obj); // Only "own" properties are included
  }

  data = {};
  for (index = 0; index < keys.length; ++index) {
    key = keys[index];
    data[key] = obj[key];
  }
  return {ctor: ctorName, data: data};
}

// A generic "fromJSON" function for use with Reviver: Just calls the
// constructor function with no arguments, then applies all of the
// key/value pairs from the raw data to the instance. Only useful for
// constructors that can be reasonably called without arguments!
// `ctor`      The constructor to call
// `data`      The data to apply
// Returns:    The object
function Generic_fromJSON(ctor, data) {
  var obj, name;

  obj = new ctor();
  for (name in data) {
    obj[name] = data[name];
  }
  return obj;
}

Преимущество здесь в том, что вы откладываете реализацию определенного «типа» (из-за отсутствия лучшего термина) для того, как он сериализует и десериализует. Таким образом, у вас может быть «тип», который просто использует дженерики:

// `Foo` is a constructor function that integrates with Reviver
// but doesn't need anything but the generic handling.
function Foo() {
}
Foo.prototype.nifty = "I'm the nifty inherited property.";
Foo.prototype.spiffy = "I'm the spiffy inherited property.";
Foo.prototype.toJSON = function() {
  return Generic_toJSON("Foo", this);
};
Foo.fromJSON = function(value) {
  return Generic_fromJSON(Foo, value.data);
};
Reviver.constructors.Foo = Foo;

... или тот, который по какой-то причине должен сделать что-то более нестандартное:

// `Bar` is a constructor function that integrates with Reviver
// but has its own custom JSON handling for whatever reason.
function Bar(value, count) {
  this.value = value;
  this.count = count;
}
Bar.prototype.nifty = "I'm the nifty inherited property.";
Bar.prototype.spiffy = "I'm the spiffy inherited property.";
Bar.prototype.toJSON = function() {
  // Bar's custom handling *only* serializes the `value` property
  // and the `spiffy` or `nifty` props if necessary.
  var rv = {
    ctor: "Bar",
    data: {
      value: this.value,
      count: this.count
    }
  };
  if (this.hasOwnProperty("nifty")) {
    rv.data.nifty = this.nifty;
  }
  if (this.hasOwnProperty("spiffy")) {
    rv.data.spiffy = this.spiffy;
  }
  return rv;
};
Bar.fromJSON = function(value) {
  // Again custom handling, for whatever reason Bar doesn't
  // want to serialize/deserialize properties it doesn't know
  // about.
  var d = value.data;
      b = new Bar(d.value, d.count);
  if (d.spiffy) {
    b.spiffy = d.spiffy;
  }
  if (d.nifty) {
    b.nifty = d.nifty;
  }
  return b;
};
Reviver.constructors.Bar = Bar;

А вот как мы можем проверить, что Foo и Bar работают как положено ( живая копия ):

// An object with `foo` and `bar` properties:
var before = {
  foo: new Foo(),
  bar: new Bar("testing", 42)
};
before.foo.custom = "I'm a custom property";
before.foo.nifty = "Updated nifty";
before.bar.custom = "I'm a custom property"; // Won't get serialized!
before.bar.spiffy = "Updated spiffy";

// Use it
display("before.foo.nifty = " + before.foo.nifty);
display("before.foo.spiffy = " + before.foo.spiffy);
display("before.foo.custom = " + before.foo.custom + " (" + typeof before.foo.custom + ")");
display("before.bar.value = " + before.bar.value + " (" + typeof before.bar.value + ")");
display("before.bar.count = " + before.bar.count + " (" + typeof before.bar.count + ")");
display("before.bar.nifty = " + before.bar.nifty);
display("before.bar.spiffy = " + before.bar.spiffy);
display("before.bar.custom = " + before.bar.custom + " (" + typeof before.bar.custom + ")");

// Stringify it with a replacer:
var str = JSON.stringify(before);

// Show that
display("The string: " + str);

// Re-create it with use of a "reviver" function
var after = JSON.parse(str, Reviver);

// Use the result
display("after.foo.nifty = " + after.foo.nifty);
display("after.foo.spiffy = " + after.foo.spiffy);
display("after.foo.custom = " + after.foo.custom + " (" + typeof after.foo.custom + ")");
display("after.bar.value = " + after.bar.value + " (" + typeof after.bar.value + ")");
display("after.bar.count = " + after.bar.count + " (" + typeof after.bar.count + ")");
display("after.bar.nifty = " + after.bar.nifty);
display("after.bar.spiffy = " + after.bar.spiffy);
display("after.bar.custom = " + after.bar.custom + " (" + typeof after.bar.custom + ")");

display("(Note that after.bar.custom is undefined because <code>Bar</code> specifically leaves it out.)");
5 голосов
/ 13 ноября 2011

Вы действительно можете создать пустой экземпляр и затем объединить экземпляр с данными. Я рекомендую использовать библиотечную функцию для простоты использования (например, jQuery.extend).

У вас были ошибки (function ... = function(...), а JSON требует, чтобы ключи были окружены ").

http://jsfiddle.net/sc8NU/1/

var data = '{"label": "new object"}';  // JSON
var inst = new Obj;                    // empty instance
jQuery.extend(inst, JSON.parse(data)); // merge

Обратите внимание, что при таком слиянии свойства устанавливаются напрямую, поэтому, если setLabel выполняет какие-то проверки, этого не произойдет.

1 голос
/ 13 ноября 2011

Если вы хотите использовать сеттеры Obj:

Obj.createFromJSON = function(json){
   if(typeof json === "string") // if json is a string
      json = JSON.parse(json); // we convert it to an object
   var obj = new Obj(), setter; // we declare the object we will return
   for(var key in json){ // for all properties
      setter = "set"+key[0].toUpperCase()+key.substr(1); // we get the name of the setter for that property (e.g. : key=property => setter=setProperty
      // following the OP's comment, we check if the setter exists :
      if(setter in obj){
         obj[setter](json[key]); // we call the setter
      }
      else{ // if not, we set it directly
         obj[key] = json[key];
      }
   }
   return obj; // we finally return the instance
};

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

var instance = Obj.createFromJSON({"label":"MyLabel"});
var instance2 = Obj.createFromJSON('{"label":"MyLabel"}');
1 голос
/ 13 ноября 2011

Насколько я знаю, это означает уход от JSON; теперь вы настраиваете его, и поэтому вы берете на себя все потенциальные головные боли, которые это влечет за собой. Идея JSON состоит в том, чтобы включать только данные, а не код, чтобы избежать всех проблем безопасности, которые возникают, когда вы разрешаете включать код. Разрешение кода означает, что вы должны использовать eval для запуска этого кода, а eval - зло.

0 голосов
/ 13 ноября 2011

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

Сериализация JSON будет учитывать любые методы, например, если у вас есть объект

var x = {
    a: 4
    getText: function() {
       return x.a;
    }
};

Вы получите только { a:4 }, где метод getText пропущен сериализатором.

Я столкнулся с этой же проблемой год назад, и мне пришлось поддерживать отдельный вспомогательный класс для каждого объекта моего домена и использовать $.extend() для своего десериализованного объекта, когда нужно, просто как методы для базового класса для объектов домена .

0 голосов
/ 13 ноября 2011

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

0 голосов
/ 13 ноября 2011

Попробуйте использовать метод toString для метода.

Обновление:

Выполните итерации по методам в obj и сохраните их как строку, а затем создайте их экземпляр с новой функцией.

storedFunc = Obj.prototype.setLabel.toString();
Obj2.prototype['setLabel'] = new Function("return (" + storedFunc + ")")();
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...