前言
在工作中维护项目时,遇到一个setItem重写问题,如下代码所示(已经简化):
/**
* 主要实现对setItem的重写,实现对localStorage数据变动的监听
*/
var signSetItem = localStorage.setItem;
localStorage.setItem = function (key, newValue) {
var setItemEvent = new Event("setItemEvent");
setItemEvent.key = key;
var oldValue = localStorage.getItem(key);
if (!isEqual(oldValue, newValue)) { // 新旧值深度判断,派发监听事件
setItemEvent.newValue = newValue;
setItemEvent.oldValue = oldValue;
window.dispatchEvent(setItemEvent);
signSetItem.apply(this, arguments);
}
};
// 全局监听localStorage的setItemEvent事件
window.addEventListener('setItemEvent', function (e) {
console.log(e.oldValue, e.newValue, e.key )
})
但是上面的方法在火狐浏览器上遇到了问题,即监听失效了!
为何出现监听失效?
对比下火狐浏览器和谷歌浏览器的Storage对象,我们就明白了。实践如下:
- 先决条件:统一重写
localStorage.setItem = 2(在命令行输入) - 观察差别:
| 差异 | chrome浏览器 | 火狐浏览器 |
|---|---|---|
| Storage对象 | ||
| 本地存储 |
以上不难看出,其实谷歌浏览器重写setItem的本质就是给Storage增加实例属性,当我们在调用setItem时会优先搜索到实例中的setItem属性,而其原型属性setItem没有发生变化。
但是火狐浏览器不这么认为,他会筛掉setItem,不把它保存在实例属性中,仅仅是会把它做为本地值存储起来。
作者认为是由于不同浏览器,其Storage对象的get和set机制不一样所导致的。
用Object.defineProperty改写Storage的方法
上文提到寻常的赋值方法,是不能改写setItem的,所以这里用了Object.defineProperty进行改写。
注意Storage的方法都重写一遍,不然调用其对象原型方法会报错。
// javascript,ES5
var localStorageMock = (function(win) {
var storage = win.localStorage; // 用闭包实现局部对象storage
return {
setItem: function(key, value) {
var setItemEvent = new Event("setItemEvent");
var oldValue = storage[key];
setItemEvent.key = key;
if (!isEqual(oldValue, value)) { // 新旧值深度判断,派发监听事件
setItemEvent.newValue = value;
setItemEvent.oldValue = oldValue;
win.dispatchEvent(setItemEvent);
storage[key] = value;
return true;
}
return false;
},
getItem: function(key) {
return storage[key];
},
removeItem: function(key) {
storage[key] = null;
return true;
},
clear: function() {
storage.clear();
return true;
},
key: function (index) {
return storage.key(index);
}
};
}(window));
Object.defineProperty(window, 'localStorage', { value: localStorageMock, writable: true });
进阶!用Object.defineProperty和Proxy改写Storage的方法
为什么不同浏览器,其Storage对象的get和set机制不一样?
这也是我通过Proxy代理改写Storage的set和get方法所了解的,感兴趣的同志也可以改写下面代码,一起留言讨论和进步。
在这里,作者也是比较推荐代理这个方法。
因为代理更像是中间件,且你不需要改变它原有的方法,所做的只是加个“哨兵”,去观察它的操作。
// typescript
interface SEventType {
newValue: any;
oldValue: any;
}
const setItem = function (key: string, value: any) {
return Reflect.set(storageProxy, key, value);
};
const getItem = function (key: string) {
return Reflect.get(storageProxy, key);
};
const removeItem = function (key: string) {
return storageProxy.removeItem(key);
};
const key = function (key: number) {
return storageProxy.key(key);
};
const clear = function () {
return storageProxy.clear();
};
const storageProxy = new Proxy(window.localStorage, {
set: function (ls, key, newValue) {
if (typeof key === "string") {
var oldValue = storage[key];
if (!isEqual(oldValue, newValue)) {
// 判断新旧值,新值更新
var setItemEvent: CustomEvent<SEventType> = new CustomEvent(
"setItemEvent",
{
detail: {
newValue,
oldValue,
},
}
);
window.dispatchEvent(setItemEvent);
return Reflect.set(ls, key, newValue);
}
}
return false;
},
get: function (ls, prop) {
// 在执行localstorage.setItem方法时,会出现报错
// 发现仅仅一步Reflect.get(ls, prop);是不行的
// 所以单独每个方法拿出来重写了一下
if (typeof prop === "string" && prop === "setItem") {
return setItem;
} else if (prop === "getItem") {
return getItem;
} else if (prop === "key") {
return key;
} else if (prop === "clear") {
return clear;
} else if (prop === "removeItem") {
return removeItem;
}
return Reflect.get(ls, prop);
},
});
Object.defineProperty(window, "localStorage", {
configurable: true,
enumerable: true,
value: storageProxy,
});