在JS中如何如何定义一个常量对象
本文讨论如何定义一个常量对象。解释了const关键字的用法,用Object.freeze() 和 Proxy() 给出了一个定义常量对象的解决方案。
1. 需求
对于给定的常量如下:
const constSettings = {
appName:"fan",
info: {p1:200,p2:300 }
};
要求:
- 不能添加属性
constSettings.other = "abc"; // 报错
- 不能修改属性
constSetting.appName = "good"; // 报错
- 不能修改属性的属性
constSetting.info.p1 = 200; // 报错
报错
的意思是你的设置是不会生效的,并且抛出一个主动抛出一个Error让整个代码终止。
2.理解const
const 是用来定义常量的关键字。但它只是规定变量中保存的的地址值是不能修改的,而不是变量的值不能修改。
2.1 赋值运算符 =
const varName = initValue;
上面这句代码到底发生了什么事?
- 在内存中找个地方(房间)保存initValue的真实值。
- 把内存地址(房间号)保存在varName中。
const关键字不容许你修改varName中保存的房间号,但是,你可以修改房间中initValue的真实值。
下面举两个例子。
2.1.1 const对基本数据类型是生效
的
const a = 1;
a = 2; // 报错,达到了const的效果
分析:
第二句代码a = 2
可以这样理解:
- 在内存中找个地方(房间)保存2。
- 把内存地址(房间号)保存在varName中。
注意,这里的第二步是不被const允许的,所以报错了。
2.1.2 const对引用数据类型是无效
的
const obj = {name:"a"};
obj = "a" // 报错,达到了const的效果
obj.name = "b" // 不报错,成功地修改了obj的属性
分析:
第二句代码obj = "a"
可以这样理解:
- 在内存中找个地方(房间)保存a。
- 把内存地址(房间号)保存在varName中。
注意,这里的第二步是不被const允许的,所以报错了.
第三句代码obj.name = "b"
可以这样理解:
- 在内存中找个地方(房间)保存"b"。
- 把内存地址(房间号)保存在obj.name中。const只是规定了obj的地址不能修改,并没有规定它的属性的地址不能修改。
3. Object.freeze() 冻结整个对象
Object.freeze()方法可以冻结一个对象。具体来说冻结指的是:
- 不能向这个对象添加新的属性
- 不能修改其已有属性的值
- 不能删除已有属性
- 以及不能修改该对象已有属性的可枚举性、可配置性、可写性。
具体用法参考:这里
注意:
这个方法返回传递的对象,而不是创建一个被冻结的副本。或者说它直接修改了入参(参照理解 Array的reverse方法)。
3.1 Object.freeze()的用法示例
const constSettings = {
appName:"fan",
info: {p1:200,p2:300 }
};
Object.freeze(constSettings);
constSettings.appName = 1 ; // 悄悄地无效
constSettings.other = "abc"; // 悄悄地无效
constSettings.info.p1 = 100; // 生效了
console.info(constSettings)// {appName:"fan",info:{p1:100,p2:300}
注意:
- 不需要额外定义一个常量,写
const obj = Object.freeze(constSettings)
。 freeze()会直接修改入参。 - 添加other属性,修改appName 没有显示地报错误,也没有成功。
- 还是可以修改属性的属性:
constSettings.info.p1 = 100;
原因如上所述的const部分。
3.2 递归的Object.freeze()
const constSettings = {
appName:"fan",
info: {p1:200,p2:300 }
};
Object.freeze(constSettings);
上面的constSettings对象确实被冻住了,但它的属性info的值也是一个对象,而这个对象并没有被冻住
,所以你仍然可以通过constSettting.info找到这个对象,再对它的属性做进一步的修改操作。
下面,我们要通过递归,把属性的属性的属性.... 也冻起来(前提是它也是一个对象)。
通过一个函数来完成这个过程。下面的函数deepFreeze来自这里
function deepFreeze(obj) {
// 取回定义在obj上的属性名
var propNames = Object.getOwnPropertyNames(obj);
// 在冻结自身之前冻结属性
propNames.forEach(function(name) {
var prop = obj[name];
// 如果prop是个对象,冻结它
if (typeof prop == 'object' && prop !== null)
deepFreeze(prop);
});
// 冻结自身(no-op if already frozen)
return Object.freeze(obj);
}
const constSettings = {
appName:"fan",
info: {p1:200,p2:300 }
};
deepFreeze(constSettings);
constSettings.appName = 1// 悄悄地无效
constSettings.other = "abc" // 悄悄地无效
constSettings.info.p1 = 100 // 也悄悄地无效
console.info(constSettings)
下面,我们只剩一件事了: 不要 悄悄地无效
,要明确地抛出一个错误
! 这两种用户体验是完全的不同的。我更倾向于后者:如果这件事你允许我去做,但我做完了却没有没有得到正确的反馈,那么,还不如不让我做。
4. Proxy 去监听对象的操作
关于Proxy的用法可以参考这里, 也可以去看看我的另一篇文章 。 这里不做详细的介绍了。
4.1 修改属性就报错
回到我们前面的提的需求:对一个常量对象,修改属性的操作是不合法的,要报错。
我们可以代理属性的set操作,在具体的逻辑中,什么事都不做:直接报错!
const constSettings = {
appName:"fan",
info: {p1:200,p2:300 }
};
const con = new Proxy(constSettings, {
set(target,paraName){
alert(paraName+" no modify!!") // 直接报错
}
})
con.appName = "good"
当然,可以更进一步,如果试图访问一个不存在的属性,也报错。这只需要在get之前判断一下即可。
get(target,p){
if(!target.hasOwnProperty(p)){
throw new Error(p+ " no exist")
}
else{
return target[p]
}
},
set(target,p,value){
throw new Error(p+ " can not be modifiy")
}
5. 封装一个工具函数
最后,封装一个函数来生成真正的常量对象。
function createConst(obj) {
// 取回定义在obj上的属性名
var propNames = Object.getOwnPropertyNames(obj);
// 在冻结自身之前冻结属性
propNames.forEach(function(name) {
var prop = obj[name];
// 如果prop是个对象,冻结它
if (typeof prop == 'object' && prop !== null)
obj[name] = createConst(prop);
});
// 冻结自身(no-op if already frozen)
Object.freeze(obj);
return new Proxy(obj,{
get(target,p){
if(!target.hasOwnProperty(p)){
throw new Error(p+ "no exist")
}
else{
return target[p]
}
},
set(target,p,value){
throw new Error("no change"+p)
}
})
}
const settings = {
appName:"fan",
info: {p1:200,p2:300 }
};
const SETTINGS = createConst(settings)
// SETTINGS 就能够满足我们前面提的要求了。