黄子涵

5.12 不可变对象

5.12.1 不可变对象的定义

所谓不可变对象,指的是在被生成之后状态不能再被改变的对象。由于对象的状态是由其各个属性的值所决定的,因此从形式上来说也是指无法改变属性的值的对象。也有观点认为,在对象引用了另一个对象的情况下,只有当那个被引用的对象也是不可变的时候,引用了它的对象才能被称为不可变对象。

从广义上来说,不可变对象指的是不去改变状态的对象。而从狭义上来说,只有既没有改变,也无法改变状态的对象,即为了禁止改变而专门设计的对象,才被称为不可变对象。JavaScript 中的一种典型的不可变对象就是字符串对象。

5.12.2 不可变对象的作用

灵活运用不可变对象有助于提高程序的健壮性。这是因为,程序中的很多错误都是由于非法改变了对象的状态而造成的。例如,将对象传递给方法的参数时,存在方法会改写对象内容的隐患。如果那是一个不可变对象,则不用担心这一问题。不清楚对象的内部构造就改写很容易引起错误,在排除了这种情况之后,就可以减少花在这个问题上的精力。

虽然不可变对象是一种便利的程序设计技巧,但其实在 JavaScript 开发中并没有被大量使用。其中最主要的一个原因就是花销的取舍。为了确保对象的不可变,不得不增加一些和主要功能无关的代码。对于一直使用小规模代码的 JavaScript 来说,需要权衡花销。

5.12.3 实现不可变对象的方式

在 JavaScript 中可以通过以下方式实现对象的不可变。

  • 将属性(状态)隐藏,不提供变更操作。
  • 灵活运用 ECMAScript 第 5 版中提供的函数。
  • 灵活运用 writable 属性、configurable 属性以及 setter 和 getter。

JavaScript 中的对象没有像 private 属性这样的显式访问控制功能。为了将属性隐藏,可以使用一种被称为闭包的方法。

在 ECMAScript 第 5 版中有一些用于支持对象的不可变化的函数(表 5.2)。seal 可以向下兼容 preventExtensions,freeze 可以向下兼容 seal。这里的向下兼容,指的是比后者有更为严格的限制。

表 5.2 ECMAScript 第 5 版中用于支持对象的不可变化的函数
方法名 属性新增 属性删除 属性变更 确认方法
preventExtension × Object.isExtensible
seal × × Object.isSealed
freeze × × × Object.isFrozen

代码清单 5.6 ~ 代码清单 5.8 是各个方法的具体示例。Object.keys方法用于对属性枚举。

代码清单5.6 Object.preventExtensions的例子
var hzh = { x:2, y:3 };
console.log("调用preventExtensions方法:");
console.log(Object.preventExtensions(hzh));
console.log("");
// 无法新增属性
hzh.z = 4;
console.log("看是否新增z属性:");
console.log(Object.keys(hzh));
console.log("");
// 可以删除属性
delete hzh.y;
console.log("是否已经删除y属性:");
console.log(Object.keys(hzh));
console.log("");
// 可以更改属性值
hzh.x = 20;
console.log("x属性是否已经更改:");
console.log("hzh.x = " + hzh.x);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用preventExtensions方法:
{ x: 2, y: 3 }

看是否新增z属性:
[ 'x', 'y' ]

是否已经删除y属性:
[ 'x' ]

x属性是否已经更改:
hzh.x = 20

[Done] exited with code=0 in 0.281 seconds
代码清单5.7 Object.seal的例子
var hzh = { x:2, y:3 };
console.log("调用seal方法:");
console.log(Object.seal(hzh));
console.log("");
// 无法删除属性
delete hzh.y;
console.log("是否已经删除y属性:");
console.log(Object.keys(hzh));
console.log("");
// 可以更改属性值
hzh.x = 20;
console.log("x属性是否已经更改:");
console.log("hzh.x = " + hzh.x);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用seal方法:
{ x: 2, y: 3 }

是否已经删除y属性:
[ 'x', 'y' ]

x属性是否已经更改:
hzh.x = 20

[Done] exited with code=0 in 0.203 seconds
代码清单5.8 Object.freeze的例子
var hzh = { x:2, y:3 };
console.log("调用freeze方法:");
console.log(Object.freeze(hzh));
console.log("");
// 无法更改属性值
hzh.x = 20;
console.log("x属性是否已经更改:");
console.log("hzh.x = " + hzh.x);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用freeze方法:
{ x: 2, y: 3 }

x属性是否已经更改:
hzh.x = 2

[Done] exited with code=0 in 0.182 seconds

对于表 5.2 中的方法,有以下几点需要注意。

  • 一旦更改就无法还原。
  • 如果想让原型继承中的被继承方也不可变化,需要对其进行显式的操作。

从内部实现来看,seal 的作用是将属性的 configurable 属性置为假,而 freeze 是将 writable 属性置为假。如果在生成对象时,对这些属性进行显式地设置,也能够取得相同的效果。灵活运用属性的属性,还能够实现只有 getter 方法而没有setter 方法的不可变对象。

尽可能不使用不可变对象。应该为程序的健壮性与其开销选择一个折中方案。为了安全性而增加开销,产品可能就会无法按时完成。此外,客户端 JavaScript 对代码的体积有着严格的要求,因此过分注重安全性的代码可能反而会降低用户体验。这不是一个简单的是非问题,而是一个需要做出判断的问题。尽管不可变对象是提升代码健壮性的一个有效方法,但如果过分拘泥于此而降低了用户的使用体验,反而本末
倒置了。实际的程序开发与理论研究有所不同,请时刻谨记考虑健壮性与开销之间的平衡。

posted @ 2022-05-28 10:42  黄子涵  阅读(47)  评论(0编辑  收藏  举报