控制对象状态

课后整理 2020-12-20

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对象被冻结以后,这个指向无法改变,即无法指向其他值,但是所指向的数组是可以改变的。