如何将对象存储在HTML5的localStorage/sessionStorage中

3057

我想在HTML5 localStorage中存储一个JavaScript对象,但是我的对象似乎被转换为了字符串。

我可以使用localStorage来存储和检索原始的JavaScript类型和数组,但是对象似乎不起作用。它们应该吗?

这是我的代码:

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);

控制台输出为

typeof testObject: object
testObject properties:
  one: 1
  two: 2
  three: 3
typeof retrievedObject: string
Value of retrievedObject: [object Object]

据我看,setItem方法在存储数据之前将其转换为字符串。

我在Safari、Chrome和Firefox中都看到了这种行为,因此我认为这是我对HTML5 Web Storage规范的误解,而不是浏览器特定的错误或限制。

我尝试理解描述在2 Common infrastructure结构化克隆算法。我没有完全理解它的意思,但也许我的问题与我对象的属性不可枚举有关(???)。

有没有简单的解决方法?


更新:W3C最终改变了他们对结构化克隆规范的看法,并决定更改规范以匹配实现。请参见12111 - Storage对象getItem(key)方法的规范与实现行为不匹配 。因此,这个问题不再100%有效,但答案仍然可能会引起兴趣。


23
顺便说一下,您对“结构化克隆算法”的阅读是正确的,只是规范在实现发布后从仅字符串值更改为现在的形式。我向 Mozilla 提交了错误报告 https://bugzilla.mozilla.org/show_bug.cgi?id=538142 以跟踪此问题。 - Nickolay
2
这似乎是indexedDB的工作... - markasoftware
1
如何在localStorage中存储对象数组?我遇到了同样的问题,即它被转换为字符串。 - Jayant Pareek
1
你能否考虑直接将数组序列化,例如使用 JSON.stringify 存储,然后在加载时再解析? - brandito
1
你可以使用 localDataStorage 来透明地存储 JavaScript 数据类型(Array、Boolean、Date、Float、Integer、String 和 Object)。 - Mac
24个回答

3776

AppleMozillaMozilla的文档中可以看出,这些功能只能处理字符串键值对。

一种解决方法是在存储对象之前对其进行stringify操作,然后在检索时对其进行解析:

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));

203
请注意,任何元数据都将被删除。你只会得到一个具有键值对的对象,因此任何具有行为的对象需要重新构建。 - oligofren
7
如果数据超出容量,@CMS的setItem方法是否会抛出异常? - Ashish Negi
3
这种方法的问题在于性能方面存在问题,特别是当你需要处理大型数组或对象时。 - Mark
4
@oligofren,确实如此,但正如 maja所建议的那样,使用eval()是其中一个好的用法,您可以轻松地检索函数代码 => 将其存储为字符串,然后再次使用eval()。 :) - jave.web
3
@oligofren 不仅元数据或行为将被删除,所有不能适应JSON格式的数据都将被删除或修改:例如undefinedInfinityNaNDate对象等等很多其他东西,在使用JSON.parse解析时,它们无法被表示成JSON格式或者至少无法按预期进行解析。基本上,你只能使用JSON支持的基本类型。 - Flimm
显示剩余2条评论

678

一个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);
}

由于短路评估,如果存储中没有keygetObject()立即返回null。如果value是空字符串""JSON.parse()无法处理),它也不会抛出SyntaxError异常。


55
我会尽快介绍如何使用,因为对我来说一开始并不清楚:var userObject = { userId: 24, name: 'Jack Bauer' };设置它: localStorage.setObject('user', userObject);然后从存储中获取它: userObject = localStorage.getObject('user');如果需要,您甚至可以存储对象数组。 - zuallauz
9
这只是布尔表达式。仅当左侧为真时,才会评估第二部分。在这种情况下,整个表达式的结果将来自右侧部分。这是一种基于布尔表达式计算方式的流行技术。 - Guria
5
这里对于本地变量和快捷评估的使用并不明确(除了一些细微的性能改进)。如果key不在本地存储中,window.localStorage.getItem(key)将返回null - 它不会抛出“非法访问”异常 - 并且JSON.parse(null)也将返回null - 它也不会引发异常,无论是在Chromium 21还是根据ES 5.1第15.12.2节。这是因为String(null) === "null"可以被解释为JSON字面量之一。 - PointedEars
9
本地存储(Local Storage)中的值始终为原始字符串值。因此,这种快捷评估处理的是之前有人存储了“”(空字符串)的情况。因为空字符串会转换为false,而且不会调用JSON.parse(""),后者会抛出SyntaxError异常。 - PointedEars
2
如果你需要支持IE8,那么这种方法行不通,最好使用确认答案中的函数。 - Ezeke
显示剩余5条评论

246

你可能会发现使用下列这些方便的方法扩展Storage对象会很有用:

Storage.prototype.setObject = function(key, value) {
    this.setItem(key, JSON.stringify(value));
}

Storage.prototype.getObject = function(key) {
    return JSON.parse(this.getItem(key));
}

即使API底层只支持字符串,但通过这种方式,您可以获得真正想要的功能。


15
将CMS的方法封装成一个函数是个好主意,只需要添加三个特性测试:一个用于JSON.stringify,一个用于JSON.parse,还有一个测试localStorage是否能够设置和检索对象。修改宿主对象不是一个好主意;我更倾向于将其作为单独的方法而不是localStorage.setObject - Garrett
4
如果存储的值是 "",这个 getObject() 函数将抛出一个 SyntaxError 异常,因为 JSON.parse() 无法处理它。有关详细信息,请参阅我对 Guria 回答的编辑。 - PointedEars
15
我只是提供一点意见,但我相当确定这样扩展供应商提供的对象并不是一个好主意。 - Sethen
1
我完全同意@Sethen的观点。请不要像这样猴子补丁浏览器实现的全局变量。这可能会破坏代码,并且在未来,如果浏览器在此全局中提供了“setObject”方法,则不兼容。 - Flimm

84
创建 Storage 对象的外观是一个很棒的解决方案。这样,您可以实现自己的 getset 方法。对于我的 API,我已经为 localStorage 创建了一个外观,然后在设置和获取时检查它是否为对象。
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;
  }
}

2
这其实很酷。同意@FrancescoFrapporti的看法,你需要在那里加一个if来处理null值。我还添加了一个“|| value [0] ==“ [”'的测试,以防里面有一个数组。 - rob_james
3
对于像我这样的非忍者,能否有人提供一个使用示例来解释这个答案?例如:data.set('username': 'ifedi', 'fullname': { firstname: 'Ifedi', lastname: 'Okonkwo'}); - Ifedi Okonkwo
1
然后有人尝试存储以 { 开头的字符串 - Jimmy T.
1
你可以将所有内容转换为字符串。 - Jimmy T.
1
如果您想将键设置为0、""或任何其他转换为false的值,则您的设置函数将无法正常工作。 相反,您应该编写: if (!key || value === undefined) return; 这也将允许您为键存储一个“null”值。 - Fredrik_Borgstrom
显示剩余5条评论

83

JSON.stringify并不能解决所有问题

看起来这里的答案并没有涵盖JavaScript中可能存在的所有类型,所以以下是一些简短的例子,展示如何正确地处理它们:

// 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"

我不建议将函数存储,因为eval()是有问题的,可能会导致安全、优化和调试方面的问题。

一般来说,在JavaScript代码中不应该使用eval()

私有成员

使用JSON.stringify()来存储对象的问题在于该函数无法序列化私有成员。

可以通过重写.toString()方法(在Web存储中存储数据时隐式调用)来解决此问题:

// 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);

循环引用

stringify无法处理的另一个问题是循环引用:

var obj = {};
obj["circular"] = obj;
localStorage.object = JSON.stringify(obj);  // Fails

在此示例中,JSON.stringify()将抛出一个TypeError错误,信息为"Converting circular structure to JSON"

如果需要支持存储循环引用,可以使用JSON.stringify()的第二个参数:

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;
    }
});

然而,为存储循环引用找到高效的解决方案高度取决于需要解决的任务,而恢复此类数据也并不容易。

已经有一些关于这个问题的Stack Overflow问题:将具有循环引用的JavaScript对象字符串化(转换为JSON)


4
因此,毋庸置疑 - 将数据存储到存储器中应该基于简单数据的副本。不是活动对象。 - Roko C. Buljan
现今的做法可能会使用自定义toJSON而不是toString()。不幸的是,解析没有对称的等效方法。 - oligofren
toJSON 不支持那些没有直接 JSON 表示的类型,例如日期、正则表达式、函数以及许多其他在我写这篇答案之后添加到 JavaScript 中的新类型。 - maja
为什么在 localStorage.num 前面加上"+" (num = +localStorage.num)? - Peter Mortensen
@PeterMortensen 将存储的字符串转换回数字。 - maja

57

有一个很棒的库,它包装了许多解决方案,因此甚至支持较旧的浏览器,称为jStorage

你可以设置一个对象

$.jStorage.set(key, value)

并轻松地检索它

value = $.jStorage.get(key)
value = $.jStorage.get(key, "default value")

4
jStorage需要Prototype、MooTools或jQuery。 - JProgrammer

38

在浏览另一个被关闭为本帖的重复帖子“如何将数组存储在localStorage中”后,我来到了这篇文章。虽然这很好,但是两个帖子都没有提供关于如何在localStorage中维护数组的完整答案 - 但我已经根据两个帖子中包含的信息制定了一个解决方案。

因此,如果有其他人想要能够在数组中推入/弹出/移动项目,并且他们希望将该数组存储在localStorage或sessionStorage中,那么请看这里:

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);
}

示例用法-在localStorage数组中存储简单字符串:

localStorage.pushArrayItem('myArray','item one');
localStorage.pushArrayItem('myArray','item two');

示例用法-将对象存储在sessionStorage数组中:

var item1 = {}; item1.name = 'fred'; item1.age = 48;
sessionStorage.pushArrayItem('myArray',item1);

var item2 = {}; item2.name = 'dave'; item2.age = 22;
sessionStorage.pushArrayItem('myArray',item2);

常见的操作数组的方法:

.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

3
这是一组非常实用的方法,用于操作存储在localStorage或sessionStorage中的数组,它值得比它目前获得的更多的赞誉。@Andy Lorenz感谢您抽出时间分享! - Velojet
通常不建议像这样猴子补丁浏览器提供的全局变量。这可能会导致其他代码出现问题,并且它不向前兼容未来可能希望在全局中提供其自己命名方法的浏览器。 - Flimm
2
@Flimm我同意这通常不是一个好主意,但这个观点更多地基于理论而不是实践。例如,自从我在2014年发帖以来,localStorage或sessionStorage实现中没有任何被破坏的变化。而且我想说,它们永远也不会有。但如果这种可能性对某人构成了关注——这是一个考虑风险的个人决定,而不是“你应该/不应该”——我的回答可以很容易地用作实现包装实际localStorage/sessionStorage的自定义数组类的蓝图。 - Andy Lorenz

32
在理论上,可以将带有函数的对象进行存储:
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;
}

然而,函数的序列化/反序列化是不可靠的,因为它受实现方式的影响


1
函数序列化/反序列化不可靠,因为它取决于实现(http://ecma-international.org/ecma-262/5.1/#sec-15.3.4)。此外,您需要将`c.f[k] = escape(a[k]);替换为Unicode安全的c.f[k] = encodeURIComponent(a[k]);,并将eval('b.' + k + ' = ' + unescape(data.f[k]));替换为b[k] = eval("(" + decodeURIComponent(data.f[k]) + ")");。括号是必需的,因为如果正确序列化,则您的函数很可能是匿名的,这不是有效的/语句/(因此eval())否则会抛出SyntaxError`异常)。 - PointedEars
typeof 是一个 运算符,不要将其写成函数的形式。请用 typeof a[k] 替换 typeof(a[k]) - PointedEars
除了采纳我的建议并强调该方法的不可靠性外,我还修复了以下错误:1. 并未声明所有变量。2. for-in 未过滤自有属性。3. 代码风格,包括引用方式,不一致。 - PointedEars
@PointedEars 这对实际有什么影响呢?规范上说“空格,行终止符和分号在表示字符串内的使用和放置取决于实现”,我没有看到任何功能差异。 - Michael
@Michael,你引用的部分以“Note in particular that…”开头。但是返回值规范以“返回函数的实现相关表示。该表示具有FunctionDeclaration的语法。”开头。如果实现符合要求,则返回值可以是function foo() {} - PointedEars

23

如果没有字符串格式,您将无法存储键值。

LocalStorage仅支持键/值的字符串格式。

这就是为什么您应该将数据转换为字符串,无论它是数组还是对象。

要在localStorage中存储数据,首先使用JSON.stringify()方法对其进行字符串化。

var myObj = [{name:"test", time:"Date 2017-02-03T08:38:04.449Z"}];
localStorage.setItem('item', JSON.stringify(myObj));

当您想要检索数据时,您需要再次将字符串解析为对象。

var getObj = JSON.parse(localStorage.getItem('item'));

1
谢谢,我明白了本地存储的概念。 - Ashish Kamble

17

请修改以包括为什么推荐使用抽象库,尤其是当存在一种非常简单且受支持的方法来对字符串进行编码/解码(即JSON.stringify/parse),该方法可以充分支持99%的使用情况。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接