JavaScript提供了三种方法,精确控制一个对象的读写状态,防止对象被改变。最弱一层的保护是Object.preventExtensions,其次是Object.seal,最强的Object.freeze。
Object.preventExtensions()
Object.preventExtensions方法可以使得一个对象无法再添加新的属性。例如: var o = new Object(); Object.preventExtensions(o); Object.defineProperty(o, 'p', { value: 'hello' }); // TypeError: Cannot define property:p, object is not extensible. o.p = 1; o.p // undefined
如果是在严格模式下,则会抛出一个错误。例如:
(function () { 'use strict'; o.p = '1' }()); // TypeError: Can't add property bar, object is not extensible
对于使用了preventExtensions方法的对象,可以用delete命令删除它的现有属性。例如:
var o = new Object(); o.p = 1; Object.preventExtensions(o); delete o.p; o.p // undefined
Object.isExtensible()
Object.isExtensible方法用于检查一个对象是否使用了Object.preventExtensions方法。也就是说,检查是否可以为一个对象添加属性。例如:
var o = new Object(); Object.isExtensible(o) // true Object.preventExtensions(o); Object.isExtensible(o) // false
上面代码新生成了一个o对象,对该对象使用Object.isExtensible方法,返回true,表示可以添加新属性。对该对象使用Object.preventExtensions方法以后,再使用Object.isExtensible方法,返回false,表示已经不能添加新属性了。
Object.seal()
Object.seal方法使得一个对象既无法添加新属性,也无法删除旧属性。例如:
var o = { p: 'hello' }; Object.seal(o); delete o.p; o.p // "hello" o.x = 'world'; o.x // undefined
在上面代码中,一个对象执行Object.seal方法以后,就无法添加新属性和删除旧属性了。
Object.seal实质是把属性描述对象的configurable属性设为false,因此属性描述对象不再能改变了。例如:
var o = { p: 'a' }; // seal方法之前 Object.getOwnPropertyDescriptor(o, 'p') // Object { // value: "a", // writable: true, // enumerable: true, // configurable: true // } Object.seal(o); // seal方法之后 Object.getOwnPropertyDescriptor(o, 'p') // Object { // value: "a", // writable: true, // enumerable: true, // configurable: false // } Object.defineProperty(o, 'p', { enumerable: false }) // TypeError: Cannot redefine property: p
在上面代码中,使用Object.seal方法之后,属性描述对象的configurable属性就变成了false,然后改变enumerable属性就会报错。
可写性(writable)有点特别。如果writable为false,使用Object.seal方法以后,将无法将其变成true;但是,如果writable为true,依然可以将其变成false。例如:
var o1 = Object.defineProperty({}, 'p', { writable: false }); Object.seal(o1); Object.defineProperty(o1, 'p', { writable:true }) // Uncaught TypeError: Cannot redefine property: p var o2 = Object.defineProperty({}, 'p', { writable: true }); Object.seal(o2); Object.defineProperty(o2, 'p', { writable:false }); Object.getOwnPropertyDescriptor(o2, 'p') // { // value: '', // writable: false, // enumerable: true, // configurable: false // }
在上面代码中,同样是使用了Object.seal方法,如果writable原为false,改变这个设置将报错;如果原为true,则不会有问题。
至于属性对象的value是否可改变,是由writable决定的。例如:
var o = { p: 'a' }; Object.seal(o); o.p = 'b'; o.p // 'b'
在上面代码中,Object.seal方法对p属性的value无效,是因为此时p属性的writable为true。
Object.isSealed()
Object.isSealed方法用于检查一个对象是否使用了Object.seal方法。例如:
var o = { p: 'a' }; Object.seal(o); Object.isSealed(o) // true
这时,Object.isExtensible方法也返回false。例如:
var o = { p: 'a' }; Object.seal(o); Object.isExtensible(o) // false
Object.freeze()
Object.freeze方法可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量。例如:
var o = { p: 'hello' }; Object.freeze(o); o.p = 'world'; o.p // hello o.t = 'hello'; o.t // undefined
在上面代码中,对现有属性重新赋值(o.p = 'world')或者添加一个新属性,并不会报错,只是默默地失败。但是,如果是在严格模式下,就会报错。例如:
var o = { p: 'hello' }; Object.freeze(o); // 对现有属性重新赋值 (function () { 'use strict'; o.p = 'world'; }()) // TypeError: Cannot assign to read only property 'p' of #<Object> // 添加不存在的属性 (function () { 'use strict'; o.t = 123; }()) // TypeError: Can't add property t, object is not extensible
Object.isFrozen()
Object.isFrozen方法用于检查一个对象是否使用了Object.freeze()方法。例如:
var obj = { p: 'hello' }; Object.freeze(obj); Object.isFrozen(obj) // true
前面说过,如果一个对象被冻结,再对它的属性赋值,在严格模式下会报错。Object.isFrozen方法可以防止发生这样的错误。例如:
var obj = { p: 'hello' }; Object.freeze(obj); if (!Object.isFrozen(obj)) { obj.p = 'world'; }
在上面代码中,确认obj没有被冻结后,再对它的属性赋值,就不会报错了。
局限性
上面的方法锁定对象的可写性有一个漏洞,依然可以通过改变原型对象,来为对象增加属性。例如:var obj = new Object();
Object.preventExtensions(obj); var proto = Object.getPrototypeOf(obj); proto.t = 'hello'; obj.t // hello
一种解决方案是,把原型也冻结住。例如:
var obj = Object.seal( Object.create( Object.freeze({x: 1}), { y: { value: 2, writable: true } } ) ); Object.getPrototypeOf(obj).hello = "hello"; obj.hello // undefined
另外一个局限是,如果属性值是对象,上面这些方法只能冻结属性指向的对象,而不能冻结对象本身的内容。例如:
var obj = { foo: 1, bar: ['a', 'b'] }; Object.freeze(obj); obj.bar.push('c'); obj.bar // ["a", "b", "c"]
在上面代码中,obj.bar属性指向一个数组,obj对象被冻结以后,这个指向无法改变,即无法指向其他值,但是所指向的数组是可以改变的。