我不清楚何时有人需要Object.freeze
在 JavaScript 中使用。MDN 和 MSDN 在有用时不会给出现实生活中的示例。我知道在运行时更改此类对象的尝试意味着崩溃。问题是,我什么时候会欣赏这次崩溃?
对我来说,不变性是一种设计时间约束,应该由类型检查器来保证。
那么在动态类型语言中出现运行时崩溃还有什么意义呢?
我不清楚何时有人需要Object.freeze
在 JavaScript 中使用。MDN 和 MSDN 在有用时不会给出现实生活中的示例。我知道在运行时更改此类对象的尝试意味着崩溃。问题是,我什么时候会欣赏这次崩溃?
对我来说,不变性是一种设计时间约束,应该由类型检查器来保证。
那么在动态类型语言中出现运行时崩溃还有什么意义呢?
该Object.freeze
函数执行以下操作:
那是什么部分,但为什么有人会这样做呢?
好吧,在面向对象的范式中,存在这样一种观念,即现有 API 包含某些不打算在当前上下文之外扩展、修改或重用的元素。各种语言中的final
关键字是最合适的类比。即使在未编译并因此容易修改的语言中,它仍然存在,即 PHP,在这种情况下是 JavaScript。
当您有一个表示逻辑上不可变数据结构的对象时,您可以使用它,尤其是在以下情况下:
作为 API 作者,这可能正是您想要的行为。例如,您可能有一个内部缓存的结构,它表示您通过引用提供给 API 用户的规范服务器响应,但仍在内部用于各种目的。您的用户可以引用此结构,但更改它可能会导致您的 API 具有未定义的行为。在这种情况下,您希望在您的用户尝试修改它时引发异常。
在我的 nodejs 服务器环境中,我使用冻结的原因与使用“使用严格”的原因相同。如果我有一个我不想被扩展或修改的对象,我会冻结它。如果有东西试图扩展或修改我的冻结对象,我希望我的应用程序抛出错误。
对我来说,这与一致、高质量、更安全的代码有关。
此外, Chrome 在处理冻结对象时表现出显着的性能提升。
编辑:在我最近的项目中,我在政府实体之间发送/接收加密数据。有很多配置值。我正在为这些值使用冻结的对象。修改这些值可能会产生严重的不良副作用。此外,正如我之前链接的那样,Chrome 显示了冻结对象的性能优势,我认为 nodejs 也是如此。
为简单起见,示例如下:
var US_COIN_VALUE = {
QUARTER: 25,
DIME: 10,
NICKEL: 5,
PENNY: 1
};
return Object.freeze( US_COIN_VALUE );
没有理由修改此示例中的值。并享受速度优化带来的好处。
Object.freeze()
主要用于函数式编程(不变性)
不变性是函数式编程的核心概念,因为没有它,程序中的数据流是有损的。状态历史被遗弃,奇怪的错误可能会潜入您的软件。
在 JavaScript 中,重要的是不要将const
, 与不变性混淆。const
创建一个变量名绑定,创建后不能重新分配。const
不创建不可变对象。您无法更改绑定引用的对象,但您仍然可以更改对象的属性,这意味着使用创建的绑定const
是可变的,而不是不可变的。
不可变对象根本无法更改。您可以通过深度冻结对象来使值真正不可变。JavaScript 有一种方法可以将对象冻结到一级深度。
const a = Object.freeze({
foo: 'Hello',
bar: 'world',
baz: '!'
});
在V8 版本 v7.6中,冻结/密封阵列的性能得到了极大的改进。因此,您想要冻结对象的一个原因是您的代码对性能至关重要。
这是一个老问题,但我认为我有一个很好的例子,冻结可能会有所帮助。我今天遇到了这个问题。
问题
class Node {
constructor() {
this._children = [];
this._parent = undefined;
}
get children() { return this._children; }
get parent() { return this._parent; }
set parent(newParent) {
// 1. if _parent is not undefined, remove this node from _parent's children
// 2. set _parent to newParent
// 3. if newParent is not undefined, add this node to newParent's children
}
addChild(node) { node.parent = this; }
removeChild(node) { node.parent === this && (node.parent = undefined); }
...
}
如您所见,当您更改父节点时,它会自动处理这些节点之间的连接,使子节点和父节点保持同步。但是,这里有一个问题:
let newNode = new Node();
myNode.children.push(newNode);
现在,myNode
有newNode
它的children
,但newNode
没有myNode
它的parent
。所以你刚刚打破了它。
(题外话)你为什么要暴露孩子呢?
是的,我可以创建很多方法:countChildren()、getChild(index)、getChildrenIterator()(它返回一个生成器)、findChildIndex(node) 等等......但这真的是比仅仅返回更好的方法吗?一个数组,它提供了一个所有 javascript 程序员都知道的接口?
length
以查看它有多少个孩子;children[i]
;for .. of
您可以使用;对其进行迭代。注意:返回数组的副本是没有问题的!它花费线性时间,并且对原始数组的任何更新都不会传播到副本!
解决方案
get children() { return Object.freeze(Object.create(this._children)); }
// OR, if you deeply care about performance:
get children() {
return this._PUBLIC_children === undefined
? (this._PUBLIC_children = Object.freeze(Object.create(this._children)))
: this._PUBLIC_children;
}
完毕!
Object.create
:我们创建一个继承自this._children
(即this._children
作为它的__proto__
)的对象。仅此一项就几乎解决了整个问题:
Object.freeze
:但是,您可以修改返回的对象但更改不影响原始数组的事实对于该类的用户来说非常混乱!所以,我们只是冻结它。如果他尝试修改它,则会引发异常(假设为严格模式),并且他知道他不能(以及为什么)。遗憾的是,myFrozenObject[x] = y
如果您不在严格模式下,不会抛出异常,但myFrozenObject
无论如何都没有修改,所以它仍然不那么奇怪。当然,程序员可以通过访问绕过它__proto__
,例如:
someNode.children.__proto__.push(new Node());
但我喜欢认为,在这种情况下,他们实际上知道自己在做什么,并且有充分的理由这样做。
重要提示:请注意,这对于对象来说效果不佳:在 for .. in 中使用 hasOwnProperty 将始终返回 false。
更新:使用代理解决对象的相同问题
只是为了完成:如果你有一个对象而不是一个数组,你仍然可以通过使用代理来解决这个问题。实际上,这是一个通用的解决方案,应该适用于任何类型的元素,但由于性能问题,我建议不要(如果可以避免的话):
get myObject() { return Object.freeze(new Proxy(this._myObject, {})); }
这仍然返回一个无法更改的对象,但保留它的所有只读功能。如果您真的需要,您可以Object.freeze
在代理中删除并实现所需的陷阱(set、deleteProperty、...),但这需要额外的努力,这就是Object.freeze
代理可以派上用场的原因。
当您可能想要冻结对象时,实际情况是什么?例如,在应用程序启动时,您创建一个包含应用程序设置的对象。您可以将该配置对象传递给应用程序的各个模块。但是一旦创建了该设置对象,您就想知道它不会被更改。
当您在 JS 中编写库/框架并且您不希望某些开发人员通过重新分配“内部”或公共属性来破坏您的动态语言创建时。这是不变性最明显的用例。
唯一的实际用途Object.freeze
是在开发过程中。对于生产代码,冻结/密封对象绝对没有任何好处。
愚蠢的错别字
它可以帮助您在开发过程中发现这个非常常见的问题:
if (myObject.someProp = 5) {
doSomething();
}
myObject
在严格模式下,如果被冻结,这将引发错误。
执行编码协议/限制
它还有助于在团队中执行某种协议,尤其是对于可能与其他人没有相同编码风格的新成员。
很多 Java 人喜欢给对象添加很多方法,让 JS 感觉更熟悉。冻结物体会阻止他们这样做。
当您使用交互式工具时,我可以看到这很有用。而不是:
if ( ! obj.isFrozen() ) {
obj.x = mouse[0];
obj.y = mouse[1];
}
你可以简单地做:
obj.x = mouse[0];
obj.y = mouse[1];
仅当对象未冻结时,属性才会更新。
我可以想到 Object.freeze 会派上用场的几个地方。
第一个可以使用的现实世界实现freeze
是在开发需要服务器上的“状态”以匹配浏览器中的内容的应用程序时。例如,假设您需要为函数调用添加一定级别的权限。如果您在一个应用程序中工作,那么开发人员可能会在没有意识到的情况下轻松更改或覆盖权限设置(特别是如果对象是通过引用传递的!)。但总的来说,权限永远不会改变,并且在更改时出错是首选。所以在这种情况下,权限对象可能会被冻结,从而限制开发人员错误地“设置”权限。对于类似用户的数据,例如登录名或电子邮件地址,也可以这样说。这些东西可能会被错误的代码错误地或恶意地破坏。
另一个典型的解决方案是在游戏循环代码中。您需要冻结许多游戏状态设置以保持游戏状态与服务器保持同步。
将 Object.freeze 视为使对象成为常量的一种方式。任何时候你想要一个变量常量,你可以有一个冻结的对象常量,出于类似的原因。
有时您希望通过函数和数据传递传递不可变对象,并且只允许使用 setter 更新原始对象。这可以通过克隆和冻结“getters”对象并仅使用“setters”更新原始对象来完成。
这些都是无效的吗?也可以说,由于缺少动态变量,冻结对象可能会更高效,但我还没有看到任何证据。
不知道这是否有帮助,但我用它来创建简单的枚举。它让我希望不会在数据库中获取 duff 数据,因为我知道数据的来源已被尝试为不可更改,而没有故意尝试破坏代码。从静态类型的角度来看,它允许对代码构造进行推理。