超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

对象“冻结”,防止被修改

在有些应用场合下,我们希望我们的对象是不可以被修改的,比如我们提供给外一个服务,但是不想这个服务被修改,这就需要对象能够防止被修改。

在另外一些应用场合,比如 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); // true
Reflect.isExtensible(empty); // true

Object.preventExtensions(empty);

Object.isExtensible(empty); // false
Reflect.isExtensible(empty); // false

这两种方法的不同点在于,如果参数不是对象,Object.isExtensible 会进行强制的类型转换,而 Reflect.isExtensible 会报错:

Reflect.isExtensible(1);
// TypeError: 1 is not an object

Object.isExtensible(1);
// false

Object.preventExtensions/Reflect.preventExtensions

这两个函数用于将对象变的不可扩展,并返回原对象。

但是需要注意的是,不可扩展对象的属性可以被修改和删除,只是不能添加新的属性

var obj = {
a: 'a'
}

Object.preventExtensions(obj);
Object.isExtensible(obj);
// false

obj.a = 'b';
// Object {a: "b"}
delete obj.a
// Object {}

还有,preventExtensions 只能阻止对象被添加自身属性,但可以为其原型添加属性,不过不允许将其原型重新指向另一个对象:

var fixed = Object.preventExtensions({});
fixed.__proto__.a = 'a';
console.log(fixed.a);
// a

fixed.__proto__ = {}
// TypeError: #<Object> is not extensible

同样的,如果参数不是对象,Reflect.preventExtensions 会报错,Object.preventExtensions 会进行强制的类型转换:

Reflect.preventExtensions(1);
// TypeError: 1 is not an object

Object.preventExtensions(1);
// false

在非严格模式下,为一个不可扩展对象的新属性赋值会静默失败,而严格模式下抛出 TypeError 异常:

var nonExtensible = Object.preventExtensions({});
(function fail() {
"use strict";
nonExtensible.newProperty = "FAIL";
}());
// TypeError: Cannot add property newProperty, object is not extensible

使用Object.defineProperty方法为一个不可扩展的对象添加新属性会抛出异常:

Object.defineProperty(nonExtensible, "new", { value: 1 });
// TypeError: Cannot define property new, object is not extensible

密封对象

密封对象就是让对象不能添加新的属性,并且已有的属性不能被配置,也就是说不能删除已有属性,不能修改已有属性的可枚举性、可配置性、可写性,但 可能 可以修改已有属性的值的对象。密封对象主要涉及两个函数:Object.seal()Object.isSealed()

Object.isSealed

本方法判断一个对象是否被密封:

// 新建的对象默认不是密封的.
var empty = {};
Object.isSealed(empty); // === false

// 如果你把一个空对象变的不可扩展,则它同时也会变成个密封对象.
Object.preventExtensions(empty);
Object.isSealed(empty); // === true

// 但如果这个对象不是空对象,则它不会变成密封对象,因为密封对象的所有自身属性必须是不可配置的.
var hasProp = { fee: "fie foe fum" };
Object.preventExtensions(hasProp);
Object.isSealed(hasProp); // === false

// 如果把这个属性变的不可配置,则这个对象也就成了密封对象.
Object.defineProperty(hasProp, "fee", { configurable: false });
Object.isSealed(hasProp); // === true

// 一个密封对象同时也必然是不可扩展的.
Object.isExtensible(sealed); // === false

注意,在 ES5 中,如果参数不是一个对象类型(原始类型),将抛出一个 TypeError 异常。在 ES6 中, 非对象参数将被视为一个密封的普通对象,因此会返回 true

// ES5 code
Object.isSealed(1);
// TypeError: 1 is not an object

// ES6 code
Object.isSealed(1);
// true

Object.seal

密封一个对象会让这个对象变的不能添加新属性,且所有已有属性会变的不可配置,即属性不可被删除,数据属性不能被重新定义为访问其属性,但是值可能允许被修改:

var obj = {
foo: "foo"
};
Object.seal(obj);

// 仍然可以修改密封对象上的属性的值.
obj.foo = "bar";
// Object {foo: "bar"}

// 但你不能把一个数据属性重定义成访问器属性.
Object.defineProperty(obj, "foo", { get: function() { return "g"; } });
// 抛出TypeError异常

为密封对象添加属性或者删除属性会静默失败,在严格模式下会抛出 TypeError 异常:

var obj = {
foo: "foo"
};
Object.seal(obj);

obj.bar = "bar"; // 静默失败,新属性没有成功添加
delete obj.foo; // 静默失败,属性没有删除成功
console.log(obj); // Object {foo: "bar"}

// ...在严格模式中,会抛出TypeError异常
(function fail() {
"use strict";
delete obj.foo; // TypeError: Cannot delete property 'foo' of #<Object>
}());
(function fail() {
"use strict";
obj.bar = "bar"; // TypeError: Cannot add property bar, object is not extensible
}());

密封对象不会影响从原型链上继承的属性,但原型属性的值也会不能修改。

冻结对象

冻结对象的所有属性都不能被修改,任何尝试修改的操作都会失败,也就是说被冻结的对象是不可扩展(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);

// 让这个对象变的不可扩展,并不意味着这个对象变成了冻结对象,
// 因为p属性仍然是可以配置的(而且可写的).
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);

// 使用Object.freeze是冻结一个对象最方便的方法.
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

// ES5 code
Object.isFrozen(1);
// TypeError: 1 is not an object

// ES6 code
Object.isFrozen(1);
// true

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; // "aValue"
Object.isFrozen(obj.internal);// false

如果想让一个对象变的完全冻结,也就是对象的属性为对象的时候也冻结它,称之为“深冻结”可以使用下面函数:

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; // undefined
Object.isFrozen(obj2.internal) // true

总结

这里综合比较一下三种冻结的特性:

  • 阻止扩展 (不可被扩展,属性可以被修改、删除,不能添加)
    • 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 的相关文档,不再一一列举。