- This topic is empty.
-
AuthorPosts
-
January 6, 2010 at 11:05 am #9136David HoangKeymaster
I’d like to store a JavaScript object in HTML5
localStorage
, but my object is apparently being converted to a string.I can store and retrieve primitive JavaScript types and arrays using
localStorage
, but objects don’t seem to work. Should they?Here’s my code:
var testObject = { 'one': 1, 'two': 2, 'three': 3 }; console.log('typeof testObject: ' + typeof testObject); console.log('testObject properties:'); for (var prop in testObject) { console.log(' ' + prop + ': ' + testObject[prop]); } // Put the object into storage localStorage.setItem('testObject', testObject); // Retrieve the object from storage var retrievedObject = localStorage.getItem('testObject'); console.log('typeof retrievedObject: ' + typeof retrievedObject); console.log('Value of retrievedObject: ' + retrievedObject);
The console output is
typeof testObject: object testObject properties: one: 1 two: 2 three: 3 typeof retrievedObject: string Value of retrievedObject: [object Object]
It looks to me like the
setItem
method is converting the input to a string before storing it.I see this behavior in Safari, Chrome, and Firefox, so I assume it’s my misunderstanding of the HTML5 Web Storage specification, not a browser-specific bug or limitation.
I’ve tried to make sense of the structured clone algorithm described in 2 Common infrastructure. I don’t fully understand what it’s saying, but maybe my problem has to do with my object’s properties not being enumerable (???).
Is there an easy workaround?
Update: The W3C eventually changed their minds about the structured-clone specification, and decided to change the spec to match the implementations. See 12111 – spec for Storage object getItem(key) method does not match implementation behavior. So this question is no longer 100% valid, but the answers still may be of interest.
January 6, 2010 at 11:25 am #9160David HoangKeymasterLooking at the Apple, Mozilla and Mozilla again documentation, the functionality seems to be limited to handle only string key/value pairs.
A workaround can be to stringify your object before storing it, and later parse it when you retrieve it:
var testObject = { 'one': 1, 'two': 2, 'three': 3 }; // Put the object into storage localStorage.setItem('testObject', JSON.stringify(testObject)); // Retrieve the object from storage var retrievedObject = localStorage.getItem('testObject'); console.log('retrievedObject: ', JSON.parse(retrievedObject));
January 6, 2010 at 11:42 am #9158David HoangKeymasterYou might find it useful to extend the Storage object with these handy methods:
Storage.prototype.setObject = function(key, value) { this.setItem(key, JSON.stringify(value)); } Storage.prototype.getObject = function(key) { return JSON.parse(this.getItem(key)); }
This way you get the functionality that you really wanted even though underneath the API only supports strings.
June 30, 2010 at 1:45 am #9159David HoangKeymasterA minor improvement on a variant:
Storage.prototype.setObject = function(key, value) { this.setItem(key, JSON.stringify(value)); } Storage.prototype.getObject = function(key) { var value = this.getItem(key); return value && JSON.parse(value); }
Because of short-circuit evaluation,
getObject()
will immediately returnnull
ifkey
is not in Storage. It also will not throw aSyntaxError
exception ifvalue
is""
(the empty string;JSON.parse()
cannot handle that).January 22, 2011 at 1:29 am #9157David HoangKeymasterCreating a facade for the Storage object is an awesome solution. That way, you can implement your own
get
andset
methods. For my API, I have created a facade for localStorage and then check if it is an object or not while setting and getting.var data = { set: function(key, value) { if (!key || !value) {return;} if (typeof value === "object") { value = JSON.stringify(value); } localStorage.setItem(key, value); }, get: function(key) { var value = localStorage.getItem(key); if (!value) {return;} // assume it is an object that has been stringified if (value[0] === "{") { value = JSON.parse(value); } return value; } }
April 6, 2011 at 4:20 am #9153David HoangKeymasterIn theory, it is possible to store objects with functions:
function store (a) { var c = {f: {}, d: {}}; for (var k in a) { if (a.hasOwnProperty(k) && typeof a[k] === 'function') { c.f[k] = encodeURIComponent(a[k]); } } c.d = a; var data = JSON.stringify(c); window.localStorage.setItem('CODE', data); } function restore () { var data = window.localStorage.getItem('CODE'); data = JSON.parse(data); var b = data.d; for (var k in data.f) { if (data.f.hasOwnProperty(k)) { b[k] = eval("(" + decodeURIComponent(data.f[k]) + ")"); } } return b; }
However, function serialization/deserialization is unreliable because it is implementation-dependent.
August 23, 2011 at 10:52 am #9155David HoangKeymasterThere is a great library that wraps many solutions so it even supports older browsers called jStorage
You can set an object
$.jStorage.set(key, value)
And retrieve it easily
value = $.jStorage.get(key) value = $.jStorage.get(key, "default value")
May 7, 2014 at 6:35 am #9154David HoangKeymasterI arrived at this post after hitting on another post that has been closed as a duplicate of this – titled ‘how to store an array in localstorage?’. Which is fine except neither thread actually provides a full answer as to how you can maintain an array in localStorage – however I have managed to craft a solution based on information contained in both threads.
So if anyone else is wanting to be able to push/pop/shift items within an array, and they want that array stored in localStorage or indeed sessionStorage, here you go:
Storage.prototype.getArray = function(arrayName) { var thisArray = []; var fetchArrayObject = this.getItem(arrayName); if (typeof fetchArrayObject !== 'undefined') { if (fetchArrayObject !== null) { thisArray = JSON.parse(fetchArrayObject); } } return thisArray; } Storage.prototype.pushArrayItem = function(arrayName,arrayItem) { var existingArray = this.getArray(arrayName); existingArray.push(arrayItem); this.setItem(arrayName,JSON.stringify(existingArray)); } Storage.prototype.popArrayItem = function(arrayName) { var arrayItem = {}; var existingArray = this.getArray(arrayName); if (existingArray.length > 0) { arrayItem = existingArray.pop(); this.setItem(arrayName,JSON.stringify(existingArray)); } return arrayItem; } Storage.prototype.shiftArrayItem = function(arrayName) { var arrayItem = {}; var existingArray = this.getArray(arrayName); if (existingArray.length > 0) { arrayItem = existingArray.shift(); this.setItem(arrayName,JSON.stringify(existingArray)); } return arrayItem; } Storage.prototype.unshiftArrayItem = function(arrayName,arrayItem) { var existingArray = this.getArray(arrayName); existingArray.unshift(arrayItem); this.setItem(arrayName,JSON.stringify(existingArray)); } Storage.prototype.deleteArray = function(arrayName) { this.removeItem(arrayName); }
example usage – storing simple strings in localStorage array:
localStorage.pushArrayItem('myArray','item one'); localStorage.pushArrayItem('myArray','item two');
example usage – storing objects in sessionStorage array:
var item1 = {}; item1.name = 'fred'; item1.age = 48; sessionStorage.pushArrayItem('myArray',item1); var item2 = {}; item2.name = 'dave'; item2.age = 22; sessionStorage.pushArrayItem('myArray',item2);
common methods to manipulate arrays:
.pushArrayItem(arrayName,arrayItem); -> adds an element onto end of named array .unshiftArrayItem(arrayName,arrayItem); -> adds an element onto front of named array .popArrayItem(arrayName); -> removes & returns last array element .shiftArrayItem(arrayName); -> removes & returns first array element .getArray(arrayName); -> returns entire array .deleteArray(arrayName); -> removes entire array from storage
October 1, 2014 at 6:14 am #9146David HoangKeymasterhttps://github.com/adrianmay/rhaboo is a localStorage sugar layer that lets you write things like this:
var store = Rhaboo.persistent('Some name'); store.write('count', store.count ? store.count+1 : 1); store.write('somethingfancy', { one: ['man', 'went'], 2: 'mow', went: [ 2, { mow: ['a', 'meadow' ] }, {} ] }); store.somethingfancy.went[1].mow.write(1, 'lawn');
It doesn’t use JSON.stringify/parse because that would be inaccurate and slow on big objects. Instead, each terminal value has its own localStorage entry.
You can probably guess that I might have something to do with rhaboo.
November 19, 2014 at 4:51 am #9156David HoangKeymasterStringify doesn’t solve all problems
It seems that the answers here don’t cover all types that are possible in JavaScript, so here are some short examples on how to deal with them correctly:
// Objects and Arrays: var obj = {key: "value"}; localStorage.object = JSON.stringify(obj); // Will ignore private members obj = JSON.parse(localStorage.object); // Boolean: var bool = false; localStorage.bool = bool; bool = (localStorage.bool === "true"); // Numbers: var num = 42; localStorage.num = num; num = +localStorage.num; // Short for "num = parseFloat(localStorage.num);" // Dates: var date = Date.now(); localStorage.date = date; date = new Date(parseInt(localStorage.date)); // Regular expressions: var regex = /^No\.[\d]*$/i; // Usage example: "No.42".match(regex); localStorage.regex = regex; var components = localStorage.regex.match("^/(.*)/([a-z]*)$"); regex = new RegExp(components[1], components[2]); // Functions (not recommended): function func() {} localStorage.func = func; eval(localStorage.func); // Recreates the function with the name "func"
I do not recommend to store functions, because
eval()
is evil and can lead to issues regarding security, optimisation and debugging.In general,
eval()
should never be used in JavaScript code.Private members
The problem with using
JSON.stringify()
for storing objects is, that this function can not serialise private members.This issue can be solved by overwriting the
.toString()
method (which is called implicitly when storing data in web storage):// Object with private and public members: function MyClass(privateContent, publicContent) { var privateMember = privateContent || "defaultPrivateValue"; this.publicMember = publicContent || "defaultPublicValue"; this.toString = function() { return '{"private": "' + privateMember + '", "public": "' + this.publicMember + '"}'; }; } MyClass.fromString = function(serialisedString) { var properties = JSON.parse(serialisedString || "{}"); return new MyClass(properties.private, properties.public); }; // Storing: var obj = new MyClass("invisible", "visible"); localStorage.object = obj; // Loading: obj = MyClass.fromString(localStorage.object);
Circular references
Another problem
stringify
can’t deal with are circular references:var obj = {}; obj["circular"] = obj; localStorage.object = JSON.stringify(obj); // Fails
In this example,
JSON.stringify()
will throw aTypeError
"Converting circular structure to JSON".If storing circular references should be supported, the second parameter of
JSON.stringify()
might be used:var obj = {id: 1, sub: {}}; obj.sub["circular"] = obj; localStorage.object = JSON.stringify(obj, function(key, value) { if(key == 'circular') { return "$ref" + value.id + "$"; } else { return value; } });
However, finding an efficient solution for storing circular references highly depends on the tasks that need to be solved, and restoring such data is not trivial either.
There are already some question on Stack Overflow dealing with this problem: Stringify (convert to JSON) a JavaScript object with circular reference
February 11, 2015 at 3:25 am #9139David HoangKeymasterHere is some extended version of the code posted by danott:
It’ll also implement a delete value from localstorage and shows how to adds a Getter and Setter layer so instead of,
localstorage.setItem(preview, true)
you can write
config.preview = true
Okay, here were go:
var PT=Storage.prototype if (typeof PT._setItem >='u') PT._setItem = PT.setItem; PT.setItem = function(key, value) { if (typeof value >='u') //..undefined this.removeItem(key) else this._setItem(key, JSON.stringify(value)); } if (typeof PT._getItem >='u') PT._getItem = PT.getItem; PT.getItem = function(key) { var ItemData = this._getItem(key) try { return JSON.parse(ItemData); } catch(e) { return ItemData; } } // Aliases for localStorage.set/getItem get = localStorage.getItem.bind(localStorage) set = localStorage.setItem.bind(localStorage) // Create ConfigWrapperObject var config = {} // Helper to create getter & setter function configCreate(PropToAdd){ Object.defineProperty( config, PropToAdd, { get: function () { return (get(PropToAdd) )}, set: function (val) { set(PropToAdd, val)} }) } //------------------------------ // Usage Part // Create properties configCreate('preview') configCreate('notification') //... // Configuration Data transfer // Set config.preview = true // Get config.preview // Delete config.preview = undefined
Well, you may strip the aliases part with
.bind(...)
. However, I just put it in since it’s really good to know about this. I took me hours to find out why a simpleget = localStorage.getItem;
don’t work.March 12, 2015 at 2:59 am #9151David HoangKeymasterIt is recommended using an abstraction library for many of the features discussed here, as well as better compatibility. There are lots of options:
- jStorage or simpleStorage ↠my preference
- localForage
- alekseykulikov/storage
- Lawnchair
- Store.js ↠another good option
- OMG
- localDataStorage
November 29, 2015 at 3:39 am #9140David HoangKeymasterI made a thing that doesn’t break the existing Storage objects, but creates a wrapper so you can do what you want. The result is a normal object, no methods, with access like any object.
If you want 1
localStorage
property to be magic:var prop = ObjectStorage(localStorage, 'prop');
If you need several:
var storage = ObjectStorage(localStorage, ['prop', 'more', 'props']);
Everything you do to
prop
, or the objects insidestorage
will be automatically saved intolocalStorage
. You’re always playing with a real object, so you can do stuff like this:storage.data.list.push('more data'); storage.another.list.splice(1, 2, {another: 'object'});
And every new object inside a tracked object will be automatically tracked.
The very big downside: it depends on
Object.observe()
so it has very limited browser support. And it doesn’t look like it’ll be coming for Firefox or Edge anytime soon.June 17, 2016 at 12:10 pm #9147David HoangKeymasterAnother option would be to use an existing plugin.
For example persisto is an open source project that provides an easy interface to localStorage/sessionStorage and automates persistence for form fields (input, radio buttons, and checkboxes).
(Disclaimer: I am the author.)
July 28, 2016 at 10:41 am #9149David HoangKeymasterYou can use ejson to store the objects as strings.
EJSON is an extension of JSON to support more types. It supports all JSON-safe types, as well as:
- Date (JavaScript
Date
) - Binary (JavaScript
Uint8Array
or the result of EJSON.newBinary) - User-defined types (see EJSON.addType. For example, Mongo.ObjectID is implemented this way.)
All EJSON serializations are also valid JSON. For example an object with a date and a binary buffer would be serialized in EJSON as:
{ "d": {"$date": 1358205756553}, "b": {"$binary": "c3VyZS4="} }
Here is my localStorage wrapper using ejson
https://github.com/UziTech/storage.js
I added some types to my wrapper including regular expressions and functions
-
AuthorPosts
- You must be logged in to reply to this topic.