Advertisement
Advertisement


Storing Objects in HTML5 localStorage


Question

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 spec, not a browser-specific bug or limitation.

I've tried to make sense of the structured clone algorithm described in http://www.w3.org/TR/html5/infrastructure.html. 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 https://www.w3.org/Bugs/Public/show_bug.cgi?id=12111. So this question is no longer 100% valid, but the answers still may be of interest.

2017/05/17
1
2571
5/17/2017 12:20:07 PM

Accepted Answer

Looking 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));
3244
6/15/2019 4:06:54 AM

A 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 return null if key is not in Storage. It also will not throw a SyntaxError exception if value is "" (the empty string; JSON.parse() cannot handle that).

2018/04/28

You 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.

2010/01/06

Extending the Storage object is an awesome solution. 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;
  }
}
2013/10/25

Stringify 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 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 a TypeError "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 SO dealing with this problem: Stringify (convert to JSON) a JavaScript object with circular reference

2017/05/23

There 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")
2018/03/27

Source: https://stackoverflow.com/questions/2010892
Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Email: [email protected]