在有些应用场合下,我们希望我们的对象是不可以被修改的,比如我们提供给外一个服务,但是不想这个服务被修改,这就需要对象能够防止被修改。
在另外一些应用场合,比如 React
通常搭配使用的 immutable.js
采用不可变的数据结构,可以保证最大限度的降低副作用。
对象防止被修改有三个级别:阻止扩展、密封对象、冻结对象,本章内容,针对这些,总结如何使对象数据不可变。
阻止扩展
如果一个对象可以添加新的属性,那么这个对象是可以扩展的,阻止扩展就是让这个对象不能被扩展,也就是说不能添加新的属性。
阻止扩展主要涉及这两组函数:Object.isExtensible(obj)/Reflect.isExtensible(obj)
、Object.preventExtensions(obj)/Reflect.preventExtensions(obj)
,前一组用于判断对象是否可以扩展,后一组用于阻止对象扩展。
Object.isExtensible(obj)/Reflect.isExtensible(obj)
这两个函数都可以检查对象是否可以扩展:
var empty = {}; |
这两种方法的不同点在于,如果参数不是对象,Object.isExtensible
会进行强制的类型转换,而 Reflect.isExtensible
会报错:
Reflect.isExtensible(1); |
Object.preventExtensions/Reflect.preventExtensions
这两个函数用于将对象变的不可扩展,并返回原对象。
但是需要注意的是,不可扩展对象的属性可以被修改和删除,只是不能添加新的属性:
var obj = { |
还有,preventExtensions
只能阻止对象被添加自身属性,但可以为其原型添加属性,不过不允许将其原型重新指向另一个对象:
var fixed = Object.preventExtensions({}); |
同样的,如果参数不是对象,Reflect.preventExtensions
会报错,Object.preventExtensions
会进行强制的类型转换:
Reflect.preventExtensions(1); |
在非严格模式下,为一个不可扩展对象的新属性赋值会静默失败,而严格模式下抛出 TypeError
异常:
var nonExtensible = Object.preventExtensions({}); |
使用Object.defineProperty方法为一个不可扩展的对象添加新属性会抛出异常:
Object.defineProperty(nonExtensible, "new", { value: 1 }); |
密封对象
密封对象就是让对象不能添加新的属性,并且已有的属性不能被配置,也就是说不能删除已有属性,不能修改已有属性的可枚举性、可配置性、可写性,但 可能 可以修改已有属性的值的对象。密封对象主要涉及两个函数:Object.seal()
、Object.isSealed()
。
Object.isSealed
本方法判断一个对象是否被密封:
// 新建的对象默认不是密封的. |
注意,在 ES5
中,如果参数不是一个对象类型(原始类型),将抛出一个 TypeError
异常。在 ES6
中, 非对象参数将被视为一个密封的普通对象,因此会返回 true
。
// ES5 code |
Object.seal
密封一个对象会让这个对象变的不能添加新属性,且所有已有属性会变的不可配置,即属性不可被删除,数据属性不能被重新定义为访问其属性,但是值可能允许被修改:
var obj = { |
为密封对象添加属性或者删除属性会静默失败,在严格模式下会抛出 TypeError
异常:
var obj = { |
密封对象不会影响从原型链上继承的属性,但原型属性的值也会不能修改。
冻结对象
冻结对象的所有属性都不能被修改,任何尝试修改的操作都会失败,也就是说被冻结的对象是不可扩展(not extensible
),不可配置(not configurable
),并且所有的数据属性都不可写(not writable
),换言之,这个对象是不可变的。
同样的,冻结操作也包含两个函数: Object.isFrozen()
、Object.freeze()
。
Object.isFrozen
看几个例子:
// 一个对象默认是可扩展的,所以它也是非冻结的. |
上述示例看起来有点绕,总结起来也就是说如果一个对象不可扩展,并且其所有属性都是不可配置的,那么这个对象是被密封的,如果同时,所有属性都是不可写的,那么这个对象同时是被冻结的。
一个冻结对象也是一个密封对象,当然,也是一个不可扩展的对象:
// 一个冻结对象也是一个密封对象. |
同样的,在 ES5
中,如果参数不是一个对象类型,将抛出一个 TypeError
异常。在 ES6
中,非对象参数将被视为一个冻结的普通对象,因此会返回 true
:
// ES5 code |
Object.freeze
Object.freeze() 方法可以冻结一个对象,
var obj = { |
同样的,对象被冻结后,任何尝试修改该对象的操作都会失败,非严格模式下静默失败,严格模式下会抛出异常,不再举例。
深冻结
如果对象的一个属性的值是个对象,则这个对象中的属性是可以修改的,除非它也是个冻结对象,Object.freeze
只能“浅冻结”一个对象:
obj = { |
如果想让一个对象变的完全冻结,也就是对象的属性为对象的时候也冻结它,称之为“深冻结”可以使用下面函数:
function deepFreeze (o) { |
总结
这里综合比较一下三种冻结的特性:
- 阻止扩展 (不可被扩展,属性可以被修改、删除,不能添加)
Object.isExtensible/Object.isExtensible
Object.preventExtensions/Object.preventExtensions
- 密封 (不可被扩展,属性不可被配置,属性可能允许修改,不能添加、删除)
Object.isSealed
Object.seal
- 冻结 (不可被扩展,属性不可被配置,不可被修改、添加、删除)
Object.isFrozen
Object.freeze
这三种阻止扩展的方法是层层加强的:
seal = configurable: false + preventExtensions
freeze = writable: false + configurable: false + preventExtensions
freeze = writable: false + seal
总而言之,如果一个对象不可扩展,并且其所有属性都是不可配置的,那么这个对象是被密封的,如果同时,所有属性都是不可写的,那么这个对象同时是被冻结的。
还有,需要注意的是,冻结操作一点被使用,则无法撤销。
参考
本文示例和内容大量参考了各个接口的 MDN 的相关文档,不再一一列举。