在有些应用场合下,我们希望我们的对象是不可以被修改的,比如我们提供给外一个服务,但是不想这个服务被修改,这就需要对象能够防止被修改。
在另外一些应用场合,比如 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(empty); Reflect.isExtensible(empty);
Object.preventExtensions(empty);
Object.isExtensible(empty); Reflect.isExtensible(empty);
|
这两种方法的不同点在于,如果参数不是对象,Object.isExtensible 会进行强制的类型转换,而 Reflect.isExtensible 会报错:
Reflect.isExtensible(1);
Object.isExtensible(1);
|
Object.preventExtensions/Reflect.preventExtensions
这两个函数用于将对象变的不可扩展,并返回原对象。
但是需要注意的是,不可扩展对象的属性可以被修改和删除,只是不能添加新的属性:
var obj = { a: 'a' }
Object.preventExtensions(obj); Object.isExtensible(obj);
obj.a = 'b';
delete obj.a
|
还有,preventExtensions 只能阻止对象被添加自身属性,但可以为其原型添加属性,不过不允许将其原型重新指向另一个对象:
var fixed = Object.preventExtensions({}); fixed.__proto__.a = 'a'; console.log(fixed.a);
fixed.__proto__ = {}
|
同样的,如果参数不是对象,Reflect.preventExtensions 会报错,Object.preventExtensions 会进行强制的类型转换:
Reflect.preventExtensions(1);
Object.preventExtensions(1);
|
在非严格模式下,为一个不可扩展对象的新属性赋值会静默失败,而严格模式下抛出 TypeError 异常:
var nonExtensible = Object.preventExtensions({}); (function fail() { "use strict"; nonExtensible.newProperty = "FAIL"; }());
|
使用Object.defineProperty方法为一个不可扩展的对象添加新属性会抛出异常:
Object.defineProperty(nonExtensible, "new", { value: 1 });
|
密封对象
密封对象就是让对象不能添加新的属性,并且已有的属性不能被配置,也就是说不能删除已有属性,不能修改已有属性的可枚举性、可配置性、可写性,但 可能 可以修改已有属性的值的对象。密封对象主要涉及两个函数:Object.seal()、Object.isSealed()。
Object.isSealed
本方法判断一个对象是否被密封:
var empty = {}; Object.isSealed(empty);
Object.preventExtensions(empty); Object.isSealed(empty);
var hasProp = { fee: "fie foe fum" }; Object.preventExtensions(hasProp); Object.isSealed(hasProp);
Object.defineProperty(hasProp, "fee", { configurable: false }); Object.isSealed(hasProp);
Object.isExtensible(sealed);
|
注意,在 ES5 中,如果参数不是一个对象类型(原始类型),将抛出一个 TypeError 异常。在 ES6 中, 非对象参数将被视为一个密封的普通对象,因此会返回 true。
Object.isSealed(1);
Object.isSealed(1);
|
Object.seal
密封一个对象会让这个对象变的不能添加新属性,且所有已有属性会变的不可配置,即属性不可被删除,数据属性不能被重新定义为访问其属性,但是值可能允许被修改:
var obj = { foo: "foo" }; Object.seal(obj);
obj.foo = "bar";
Object.defineProperty(obj, "foo", { get: function() { return "g"; } });
|
为密封对象添加属性或者删除属性会静默失败,在严格模式下会抛出 TypeError 异常:
var obj = { foo: "foo" }; Object.seal(obj);
obj.bar = "bar"; delete obj.foo; console.log(obj);
(function fail() { "use strict"; delete obj.foo; }()); (function fail() { "use strict"; obj.bar = "bar"; }());
|
密封对象不会影响从原型链上继承的属性,但原型属性的值也会不能修改。
冻结对象
冻结对象的所有属性都不能被修改,任何尝试修改的操作都会失败,也就是说被冻结的对象是不可扩展(not extensible),不可配置(not configurable),并且所有的数据属性都不可写(not writable),换言之,这个对象是不可变的。
同样的,冻结操作也包含两个函数: Object.isFrozen()、Object.freeze() 。
Object.isFrozen
看几个例子:
assert(Object.isFrozen({}) === false);
var vacuouslyFrozen = Object.preventExtensions({}); assert(Object.isFrozen(vacuouslyFrozen) === true);
var oneProp = { p: 42 }; assert(Object.isFrozen(oneProp) === false);
Object.preventExtensions(oneProp); assert(Object.isFrozen(oneProp) === false);
delete oneProp.p; assert(Object.isFrozen(oneProp) === true);
var nonWritable = { e: "plep" }; Object.preventExtensions(nonWritable); Object.defineProperty(nonWritable, "e", { writable: false }); assert(Object.isFrozen(nonWritable) === false);
Object.defineProperty(nonWritable, "e", { configurable: false }); assert(Object.isFrozen(nonWritable) === true);
var nonConfigurable = { release: "the kraken!" }; Object.preventExtensions(nonConfigurable); Object.defineProperty(nonConfigurable, "release", { configurable: false }); assert(Object.isFrozen(nonConfigurable) === false);
Object.defineProperty(nonConfigurable, "release", { writable: false }); assert(Object.isFrozen(nonConfigurable) === true);
var accessor = { get food() { return "yum"; } }; Object.preventExtensions(accessor); assert(Object.isFrozen(accessor) === false);
Object.defineProperty(accessor, "food", { configurable: false }); assert(Object.isFrozen(accessor) === true);
var frozen = { 1: 81 }; assert(Object.isFrozen(frozen) === false); Object.freeze(frozen); assert(Object.isFrozen(frozen) === true);
|
上述示例看起来有点绕,总结起来也就是说如果一个对象不可扩展,并且其所有属性都是不可配置的,那么这个对象是被密封的,如果同时,所有属性都是不可写的,那么这个对象同时是被冻结的。
一个冻结对象也是一个密封对象,当然,也是一个不可扩展的对象:
assert(Object.isSealed(frozen) === true);
assert(Object.isExtensible(frozen) === false);
|
同样的,在 ES5 中,如果参数不是一个对象类型,将抛出一个 TypeError 异常。在 ES6 中,非对象参数将被视为一个冻结的普通对象,因此会返回 true:
Object.isFrozen(1);
Object.isFrozen(1);
|
Object.freeze
Object.freeze() 方法可以冻结一个对象,
var obj = { prop: function (){}, foo: "bar" };
obj.foo = "baz"; obj.lumpy = "woof"; delete obj.prop;
var o = Object.freeze(obj);
assert(Object.isFrozen(obj) === true);
obj.foo = "quux"; obj.quaxxor = "the friendly duck";
|
同样的,对象被冻结后,任何尝试修改该对象的操作都会失败,非严格模式下静默失败,严格模式下会抛出异常,不再举例。
深冻结
如果对象的一个属性的值是个对象,则这个对象中的属性是可以修改的,除非它也是个冻结对象,Object.freeze 只能“浅冻结”一个对象:
obj = { internal : {} };
Object.freeze(obj); obj.internal.a = "aValue";
obj.internal.a; Object.isFrozen(obj.internal);
|
如果想让一个对象变的完全冻结,也就是对象的属性为对象的时候也冻结它,称之为“深冻结”可以使用下面函数:
function deepFreeze (o) { var prop, propKey; Object.freeze(o); for (propKey in o) { prop = o[propKey]; if (!o.hasOwnProperty(propKey) || !(typeof prop === "object") || Object.isFrozen(prop)) { continue; }
deepFreeze(prop); } }
obj2 = { internal : {} };
deepFreeze(obj2); obj2.internal.a = "anotherValue"; obj2.internal.a; Object.isFrozen(obj2.internal)
|
总结
这里综合比较一下三种冻结的特性:
- 阻止扩展 (不可被扩展,属性可以被修改、删除,不能添加)
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 的相关文档,不再一一列举。