如何在HTML5的localStorage和sessionStorage中存储对象
技术背景
HTML5的localStorage和sessionStorage提供了在浏览器中存储数据的功能,但它们原生仅支持存储字符串类型的键值对。当需要存储对象、数组等复杂数据类型时,就需要对数据进行处理。
实现步骤
基本方法:使用JSON.stringify()和JSON.parse()
将对象转换为字符串进行存储,取出时再将字符串解析为对象。
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对象的原型来简化操作。
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');
处理不同数据类型
不同的数据类型在存储和读取时需要不同的处理方式。
// 对象和数组
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]*)#34;);
regex = new RegExp(components[1], components[2]);
处理私有成员
使用JSON.stringify()无法序列化私有成员,可以通过重写.toString()方法解决。
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()无法处理循环引用,可使用其第二个参数来解决。
var obj = { id: 1, sub: {} };
obj.sub["circular"] = obj;
localStorage.object = JSON.stringify(obj, function(key, value) {
if (key == 'circular') {
return "$ref" + value.id + "#34;;
} else {
return value;
}
});
核心代码
扩展Storage对象处理数组
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 = (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
}
})();
最佳实践
- 使用抽象库:如jStorage、simpleStorage、localForage等,这些库提供了更好的兼容性和更多的功能。
- 对于TypeScript用户,可以使用类型化的包装器来确保类型安全。
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()方法解决。