可变对象
我们知道,JavaScript中对象是弱类型的。一般情况下,可以不受限制的为对象添加属性,修改属性,删除属性。大部分情况下,我们使用的都是可变对象。
不可变对象
对应的,我们不希望代码中某些对象被任意修改,比如添加、修改、删除等。这就是我们的不可变对象。JavaScript为我们提供了一些原生方法,借助它们可以讲一些可变对象转变成不可变对象。一共有三种:不可扩展,密封,冻结。
不可扩展
如果一个对象可以添加新的属性,则这个对象是可扩展的。相反,不可以添加新属性时就是不可扩展对象。我们可以使用 preventExtensions 让这个对象变的不可扩展。当对象不可扩展时,添加属性将静默失败或者 TypeError(严格模式下)。
function Person(name) {
this.name = name;
}
var kaka = new Person('kaka')
Object.preventExtensions(kaka);
kaka.secondName = 'zhang';
// 非严格模式下,静默失败
// 'use strict' 下 TypeError: Can't add property secondName, object is not extensible
检测
JS 提供了 Object.isExtensible 方法帮我们检测对象是否可扩展。
Object.isExtensible(kaka) // false, kaka为不可扩展对象
浅层
我们知道,JavaScript中对象的赋值有浅复制和深复制。同样,这里也有。
// 浅层 prevent
var kaka = {
name : 'qiqi',
info: {
age: 22,
height: 170
}
}
var preventedkaka = Object.preventExtensions(kaka);
preventedkaka.info.sex = 'male'; // 可以扩展深层的属性
可以发现,JavaScript只阻止浅层的扩展,引用属性的扩展不受影响。因此,可以想象。不可扩展对象 __proto__ 是不能重新指向对象的,但是可以为原型添加属性。而且,将对象变成不可扩展的对象是不可逆的。
密封
Object.seal(obj) 方法可以让一个对象密封,并返回被密封后的对象。密封对象将会阻止向对象添加新的属性。另外也会 改变属性的 configurable 描述。
看代码
function Person(name) {
this.name = name;
}
var kaka = new Person('kaka');
kaka.secondName = 'sb'
Object.getOwnPropertyDescriptor(kaka, 'secondName')
var secondJo = Object.seal(kaka)
delete secondJo.secondName // false, 删除失败,严格模式下,TypeError
Object.defineProperty(kaka, "age", { value: 22 })// TypeError: Can't add property secondName, object is not extensible
也就是说,当对象被密封后,也不能对其进行添加属性。
从上面可看,两个属性的 configurable 描述都已经改变。configurable 决定属性能否被 delete
检测
对应的,我们可以通过 Object.isSealed(jo) 来判断对象是否密封了。如果这个对象是密封的,则返回 true,否则返回 false。
Object.isSealed(jo) // true
另外,最好不要对原始类型进行检测,ES6对着有了变化。
// 对比
// ES5
Object.isSealed(1); // TypeError: 1 is not an object
// ES6
Object.isSealed(1); // true
浅密封
值得一说的是,密封也是浅层属性的密封。
var kaka = {
name : 'qiqi',
info: {
age: 22,
height: 170
}
}
var sealedKaka = Object.seal(kaka);
Object.isSealed(sealedKaka.info) // false, 只密封了当前对象,引用属性的对象没有被密封。
冻结
Object.freeze() 方法可以冻结一个对象。冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。也就是说,这个对象永远是不可变的。该方法返回被冻结的对象。
function Person(name) {
this.name = name;
}
var jo = new Person('jo');
Object.getOwnPropertyDescriptor(jo, 'name')
var freezedJo = Object.freeze(jo)
Object.getOwnPropertyDescriptor(jo, 'name')
Object.getOwnPropertyDescriptor(freezedJo, 'name')
freezedJo === jo // 其实是同一个对象
对比下,冻结前后的描述符。
从上面可以看出,冻结会改变 所有属性的 configurable 和 writable 两个描述符。
检测
Object.isFrozen(freezedJo)// true
浅冻结
跟上面一样,对于有引用属性的对象,只能冻结当前对象的原始属性。var kaka = {
name : 'qiqi',
info: {
age: 22,
height: 170
}
}
var freezedkaka = Object.freeze(kaka)
Object.isFrozen(freezedkaka.info) // false
总结
总的来说构建一个不可变对象,有上面三种方式。具体使用哪种可以根据需要。相同点:
1. 操作并返回操作对象,且操作不可逆的
2. 均有对应的检查函数
3. 均不能添加新属性,严格模式下 TypeError
4. 均是浅层操作,不影响引用类型
不同点
三种方式还是有程度上的区别的。下面用一个属性的描述符上来对比下。
var obj = {
name: 'kaka'
}
下面是操作后 name 属性描述符的影响
属性描述符 | default | preventExtensions | seal | freeze |
writable | true | true | false | false |
configurable | true | true | true | false |
enumerable | true | true | true | true |
从上面看,三个操作对对象及其属性的限制是逐渐变强的。实际使用需要哪个可以根据需要选择。