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

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

技术背景

HTML5的localStoragesessionStorage提供了在浏览器中存储数据的功能,但它们原生仅支持存储字符串类型的键值对。当需要存储对象、数组等复杂数据类型时,就需要对数据进行处理。

实现步骤

基本方法:使用JSON.stringify()和JSON.parse()

将对象转换为字符串进行存储,取出时再将字符串解析为对象。

1
2
3
4
5
6
7
8
var testObject = { 'one': 1, 'two': 2, 'three': 3 };

// 存储对象
localStorage.setItem('testObject', JSON.stringify(testObject));

// 读取对象
var retrievedObject = localStorage.getItem('testObject');
console.log('retrievedObject: ', JSON.parse(retrievedObject));

扩展Storage对象的方法

可以通过扩展Storage对象的原型来简化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
};

// 使用示例
var obj = { name: 'John' };
localStorage.setObject('user', obj);
var retrievedObj = localStorage.getObject('user');

处理不同数据类型

不同的数据类型在存储和读取时需要不同的处理方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 对象和数组
var obj = { key: "value" };
localStorage.object = JSON.stringify(obj);
obj = JSON.parse(localStorage.object);

// 布尔值
var bool = false;
localStorage.bool = bool;
bool = (localStorage.bool === "true");

// 数字
var num = 42;
localStorage.num = num;
num = +localStorage.num;

// 日期
var date = Date.now();
localStorage.date = date;
date = new Date(parseInt(localStorage.date));

// 正则表达式
var regex = /^No\.[\d]*$/i;
localStorage.regex = regex;
var components = localStorage.regex.match("^/(.*)/([a-z]*)$");
regex = new RegExp(components[1], components[2]);

处理私有成员

使用JSON.stringify()无法序列化私有成员,可以通过重写.toString()方法解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
};

// 存储
var obj = new MyClass("invisible", "visible");
localStorage.object = obj;

// 读取
obj = MyClass.fromString(localStorage.object);

处理循环引用

JSON.stringify()无法处理循环引用,可使用其第二个参数来解决。

1
2
3
4
5
6
7
8
9
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;
}
});

核心代码

扩展Storage对象处理数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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);
};

处理循环引用的完整实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
LOCALSTORAGE = (function() {
"use strict";
var ignore = [Boolean, Date, Number, RegExp, String];
function primitive(item) {
if (typeof item === 'object') {
if (item === null) { return true; }
for (var i = 0; i < ignore.length; i++) {
if (item instanceof ignore[i]) { return true; }
}
return false;
} else {
return true;
}
}
function infant(value) {
return Array.isArray(value)? [] : {};
}
function decycleIntoForest(object, replacer) {
if (typeof replacer!== 'function') {
replacer = function(x) { return x; };
}
object = replacer(object);
if (primitive(object)) return object;
var objects = [object];
var forest = [infant(object)];
var bucket = new WeakMap();
bucket.set(object, 0);
function addToBucket(obj) {
var result = objects.length;
objects.push(obj);
bucket.set(obj, result);
return result;
}
function isInBucket(obj) { return bucket.has(obj); }
function processNode(source, target) {
Object.keys(source).forEach(function(key) {
var value = replacer(source[key]);
if (primitive(value)) {
target[key] = { value: value };
} else {
var ptr;
if (isInBucket(value)) {
ptr = bucket.get(value);
} else {
ptr = addToBucket(value);
var newTree = infant(value);
forest.push(newTree);
processNode(value, newTree);
}
target[key] = { pointer: ptr };
}
});
}
processNode(object, forest[0]);
return forest;
}
function deForestIntoCycle(forest) {
var objects = [];
var objectRequested = [];
var todo = [];
function processTree(idx) {
if (idx in objects) return objects[idx];
if (objectRequested[idx]) return null;
objectRequested[idx] = true;
var tree = forest[idx];
var node = Array.isArray(tree)? [] : {};
for (var key in tree) {
var o = tree[key];
if ('pointer' in o) {
var ptr = o.pointer;
var value = processTree(ptr);
if (value === null) {
todo.push({
node: node,
key: key,
idx: ptr
});
} else {
node[key] = value;
}
} else {
if ('value' in o) {
node[key] = o.value;
} else {
throw new Error('unexpected');
}
}
}
objects[idx] = node;
return node;
}
var result = processTree(0);
for (var i = 0; i < todo.length; i++) {
var item = todo[i];
item.node[item.key] = objects[item.idx];
}
return result;
}
var console = {
log: function(x) {
var the = document.getElementById('the');
the.textContent = the.textContent + '\n' + x;
},
delimiter: function() {
var the = document.getElementById('the');
the.textContent = the.textContent +
'\n*******************************************';
}
}
function logCyclicObjectToConsole(root) {
var cycleFree = decycleIntoForest(root);
var shown = cycleFree.map(function(tree, idx) {
return false;
});
var indentIncrement = 4;
function showItem(nodeSlot, indent, label) {
var leadingSpaces =' '.repeat(indent);
var leadingSpacesPlus =' '.repeat(indent + indentIncrement);
if (shown[nodeSlot]) {
console.log(leadingSpaces + label + ' ... see above (object #' + nodeSlot + ')');
} else {
console.log(leadingSpaces + label + ' object#' + nodeSlot);
var tree = cycleFree[nodeSlot];
shown[nodeSlot] = true;
Object.keys(tree).forEach(function(key) {
var entry = tree[key];
if ('value' in entry) {
console.log(leadingSpacesPlus + key + ": " + entry.value);
} else {
if ('pointer' in entry) {
showItem(entry.pointer, indent + indentIncrement, key);
}
}
});
}
}
console.delimiter();
showItem(0, 0, 'root');
}
function stringify(obj) {
return JSON.stringify(decycleIntoForest(obj));
}
function parse(str) {
return deForestIntoCycle(JSON.parse(str));
}
var CYCLICJSON = {
decycleIntoForest: decycleIntoForest,
deForestIntoCycle: deForestIntoCycle,
logCyclicObjectToConsole: logCyclicObjectToConsole,
stringify: stringify,
parse: parse
}
function setObject(name, object) {
var str = stringify(object);
localStorage.setItem(name, str);
}
function getObject(name) {
var str = localStorage.getItem(name);
if (str === null) return null;
return parse(str);
}
return {
CYCLICJSON: CYCLICJSON,
setObject: setObject,
getObject: getObject
}
})();

最佳实践

  • 使用抽象库:如jStoragesimpleStoragelocalForage等,这些库提供了更好的兼容性和更多的功能。
  • 对于TypeScript用户,可以使用类型化的包装器来确保类型安全。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export class TypedStorage<T> {
public removeItem(key: keyof T): void {
localStorage.removeItem(key);
}

public getItem<K extends keyof T>(key: K): T[K] | null {
const data: string | null = localStorage.getItem(key);
return JSON.parse(data);
}

public setItem<K extends keyof T>(key: K, value: T[K]): void {
const data: string = JSON.stringify(value);
localStorage.setItem(key, data);
}
}

// 使用示例
interface MyStore {
age: number;
name: string;
address: { city: string };
}

const storage: TypedStorage<MyStore> = new TypedStorage<MyStore>();
storage.setItem("address", { city: "Here" });
const address: { city: string } = storage.getItem("address");

常见问题

函数存储问题

不建议存储函数,因为eval()存在安全、优化和调试方面的问题,且函数序列化/反序列化依赖于具体实现。

循环引用问题

JSON.stringify()无法处理循环引用,需要使用额外的方法来解决。

私有成员问题

JSON.stringify()无法序列化私有成员,可通过重写.toString()方法解决。


如何在HTML5的localStorage和sessionStorage中存储对象
https://119291.xyz/posts/2025-05-12.how-to-store-objects-in-html5-localstorage-sessionstorage/
作者
ww
发布于
2025年5月12日
许可协议