在 JavaScript 中,有时候需要锁定一个对象,有三个方法可供选择:
-
Object.freeze
- 不能新增属性
- 不能修改属性
- 不能修改已有属性的可枚举性、可配置性、可写性
- 不能删除属性
-
Object.seal
- 不能新增属性
- 不能修改已有属性的可枚举性、可配置性、可写性
- 可以修改属性
- 不能删除属性
-
Object.preventExtensions
- 不能新增属性
可以用下面的方法判断是否被冻结、是否密封和是否可拓展:
Object.isFrozen(myObj)
Object.isSealed(myObj)
Object.isExtensible(myObj)
如果想要深度冻结一个对象,可以用 deep-freeze 包,提供了下面的函数:
function deepFreeze (o) {
Object.freeze(o)
Object.getOwnPropertyNames(o).forEach(function (prop) {
if (o.hasOwnProperty(prop)
&& o[prop] !== null
&& (typeof o[prop] === "object" || typeof o[prop] === "function")
&& !Object.isFrozen(o[prop])) {
deepFreeze(o[prop])
}
})
return o
}
深度剖析锁定后的对象
我们可以利用 Object.defineProperty
方法来分析被锁定的对象,该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象,例如:
var obj = {}
Object.defineProperty(obj, 'name', {
value: 'keliq'
})
console.log(obj)
可以看到,该方法有三个参数,分别是:
obj
:要定义属性的对象propName
:属性名称descriptor
:属性描述符
前两个参数非常好理解,关键是第三个参数,是一个包含如下两个属性的对象:
enumerable
:可枚举性,默认 false,表示不可以被 for-in 或 Object.keys() 遍历到。configurable
:可配置性,默认 false,表示属性不可枚举、不可写、不可删除和不可配置。
除此之外,还有四个可选属性:
value
:属性对应的值,默认 undefinedwritable
:能否被赋值运算符改变,默认 falseget
:属性的 getter 函数,默认 undefinedset
:属性的 setter 函数,默认 undefined
但是,上面四个属性前两个和后两个是互斥的,即 value
与 writable
不能和 get
与 set
同时存在,要么设置 value
与 writable
,要么设置 get
与 set
。如果设置了前两个,我们称之为数据描述符,如果设置了后两个,我们称之为存取描述符,可以用下面这个表格来描述:
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
数据描述符 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
存取描述符 | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
讲了这么多,你可能还是云里雾里,我们从创建一个普通的对象开始看:
var obj = {name: 'keliq'}
console.log(Object.getOwnPropertyDescriptors(obj))
得到的结果为:
{
name: {
value: 'keliq',
writable: true,
enumerable: true,
configurable: true
}
}
也就是说 var obj = {name: 'keliq'}
等价于下面的代码:
var obj = {}
Object.defineProperty(obj, 'name', {
value: 'keliq',
writable: true,
enumerable: true,
configurable: true
})
如果我们只设置 value 会是什么效果呢?
var obj = {}
Object.defineProperty(obj, 'name', {
value: 'keliq',
})
console.log(Object.getOwnPropertyDescriptors(obj))
结果是:
{
name: {
value: 'keliq',
writable: false,
enumerable: false,
configurable: false
}
}
嗯,可以看出来区别了。接下来,我们看 Object.seal 作用在对象上会怎样:
var obj = {name: 'keliq'}
Object.seal(obj)
console.log(Object.getOwnPropertyDescriptors(obj))
结果是:
{
name: {
value: 'keliq',
writable: true,
enumerable: true,
configurable: false
}
}
这下就比较清晰了,也可以顺理成章地解释为何 sealed 对象有以下特点:
- 不能新增属性
- 不能修改已有属性的可枚举性、可配置性、可写性
- 可以修改属性
- 不能删除属性
再看 Object.freeze 的效果:
var obj = {name: 'keliq'}
Object.seal(obj)
console.log(Object.getOwnPropertyDescriptors(obj))
结果是:
{
name: {
value: 'keliq',
writable: false,
enumerable: true,
configurable: false
}
}
可以看到,连 writable 都设置为 false 了,基本上这个对象被真的「冻住了」:
- 不能新增属性
- 不能修改属性
- 不能修改已有属性的可枚举性、可配置性、可写性
- 不能删除属性
最后看下 Object.preventExtensions 方法的效果:
var obj = {name: 'keliq'}
Object.preventExtensions(obj)
console.log(Object.getOwnPropertyDescriptors(obj))
结果是:
{
name: {
value: 'keliq',
writable: true,
enumerable: true,
configurable: true
}
}
这个结果和直接定义一个普通对象 var obj = {}
得到的属性描述符是一样的,并不能看出来其特殊之处,只有当添加新属性的时候,才能发现并不奏效:
var obj = {name: 'keliq'}
Object.preventExtensions(obj)
obj.name = 'david'
obj.age = 12
console.log(obj) // { name: 'david' }