为什么我需要在JavaScript中冻结一个对象
Why would I need to freeze an object in JavaScript?
我不清楚什么时候有人需要在JavaScript中使用Object.freeze
。MDN和MSDN没有给出实际生活中有用的例子。我知道在运行时尝试更改这样的对象意味着崩溃。问题是,我什么时候才能欣赏这次撞车事故?
对我来说,不变性是一个设计时间约束,应该由类型检查器来保证。
那么,在动态类型语言中发生运行时崩溃,除了检测到违规行为比从不检测要好之外,还有什么意义吗?
Object.freeze
函数执行以下操作:
- 使对象不可扩展,因此无法向其添加新属性
- 将对象的所有属性的可配置属性设置为false。如果-configurative为false,则不能更改属性属性,也不能删除属性
- 将对象的所有数据属性的可写属性设置为false。如果writible为false,则无法更改数据属性值
这是什么的部分,但为什么会有人这样做?
在面向对象的范例中,现有的API包含某些元素,这些元素不打算在当前上下文之外进行扩展、修改或重用。各种语言中的final
关键字是最合适的类比。即使在未编译且因此易于修改的语言中,它仍然存在,即PHP,在本例中为JavaScript。
当您有一个对象表示逻辑上不可变的数据结构时,可以使用它,尤其是在以下情况下:
- 更改对象的属性或更改其"鸭子类型"可能会导致应用程序中其他地方的不良行为
- 该对象类似于可变类型,或者在其他方面看起来是可变的,您希望程序员在尝试更改它时得到警告,而不是获得未定义的行为
作为一个API作者,这可能正是你想要的行为。例如,您可能有一个内部缓存的结构,该结构表示您通过引用提供给API用户的规范服务器响应,但仍在内部用于各种目的。您的用户可以引用此结构,但更改它可能会导致API具有未定义的行为。在这种情况下,如果您的用户试图修改它,您希望抛出一个异常。
在我的nodejs服务器环境中,我使用冻结的原因与我使用"use strict"的原因相同。如果我有一个对象不想被扩展或修改,我会冻结它。如果有人试图扩展或修改我冻结的对象,我希望我的应用程序抛出错误。
对我来说,这与一致、高质量、更安全的代码有关。
此外,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大大提高了冻结/密封阵列的性能。因此,您希望冻结对象的一个原因是当您的代码对性能至关重要时。
当您在JS中编写库/框架时,您不希望某些开发人员通过重新分配"内部"或公共属性来破坏您的动态语言创建。这是不变性最明显的用例。
当您可能想要冻结对象时,实际情况是什么?例如,在应用程序启动时,您创建了一个包含应用程序设置的对象。您可以将该配置对象传递给应用程序的各个模块。但一旦创建了设置对象,您就希望知道它不会被更改。
这是一个老问题,但我认为冻结可能会有所帮助。我今天遇到了这个问题。
问题
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
的children
中有newNode
,但newNode
的parent
中没有myNode
。所以你刚刚打破了它。
(OFF-TOPIC)你为什么要暴露孩子们
是的,我可以创建很多方法:countChildren()、getChild(index)、getChildrenIterator()(返回生成器)、findChildIndex(node)等等……但这真的比只返回一个数组更好吗?数组提供了一个所有javascript程序员都知道的接口?
- 你可以访问它的
length
,看看它有多少孩子 - 您可以通过孩子的索引(即
children[i]
)访问他们 - 您可以使用
for .. of
对其进行迭代 - 您还可以使用Array提供的其他一些不错的方法
注意:返回数组的副本是不可能的!它需要线性时间,并且对原始阵列的任何更新都不会传播到副本!
解决方案
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__
)。仅此一项就几乎解决了整个问题:- 它简单快捷(恒定时间)
- 您可以使用Array接口提供的任何内容
- 如果修改返回的对象,则不会更改原始对象
Object.freeze
:然而,您可以修改返回的对象,但这些更改不会影响原始数组,这对类的用户来说是非常令人困惑的!所以,我们只是冻结它。如果他试图修改它,就会抛出一个异常(假设为严格模式),他知道自己不能(以及为什么)。很遗憾,如果您不在严格模式中,myFrozenObject[x] = y
不会出现异常,但myFrozenObject
无论如何都没有被修改,所以它仍然没有那么奇怪
当然,程序员可以通过访问__proto__
绕过它,例如:
someNode.children.__proto__.push(new Node());
但我喜欢认为,在这种情况下,他们实际上知道自己在做什么,并且有充分的理由这样做
重要:请注意,这对对象不太适用:在for中使用hasOwnProperty。。in将始终返回false。
UPDATE:使用Proxy为对象解决相同的问题
只是为了完成:如果你有一个对象而不是数组,你仍然可以通过使用代理来解决这个问题。事实上,这是一个通用的解决方案,应该适用于任何类型的元素,但由于性能问题,我建议不要使用(如果可以避免的话):
get myObject() { return Object.freeze(new Proxy(this._myObject, {})); }
这仍然返回一个不能更改的对象,但保留了它的所有只读功能。如果你真的需要,你可以删除Object.freeze
并在Proxy中实现所需的陷阱(set、deleteProperty…),但这需要额外的努力,这就是为什么Object.freeze
在代理中派上了用场。
我能想到Object.freeze非常方便的几个地方。
第一个可以使用freeze
的现实世界实现是在开发一个应用程序时,该应用程序需要服务器上的"状态"来匹配浏览器中的内容。例如,假设您需要为函数调用添加一定级别的权限。如果您在应用程序中工作,开发人员可能会在没有意识到的情况下轻松更改或覆盖权限设置(尤其是在通过引用传递对象的情况下!)。但总的来说,权限永远不会改变,当它们改变时出错是首选。因此,在这种情况下,权限对象可能会被冻结,从而限制开发人员错误地"设置"权限。对于登录名或电子邮件地址等类似用户的数据,情况也是如此。这些东西可能会被错误的或恶意的坏代码破坏。
另一个典型的解决方案是在游戏循环代码中。有许多游戏状态设置,您希望冻结以保持游戏状态与服务器同步。
把Object.freeze看作是一种使对象成为常量的方法。任何时候,只要您想要一个变量常量,出于类似的原因,您就可以有一个冻结的对象常量。
有时,您希望通过函数和数据传递传递不可变的对象,并且只允许使用setter更新原始对象。这可以通过为"getters"克隆和冻结对象并仅使用"setters"更新原始对象来实现。
这些都是无效的东西吗?也可以说,由于缺乏动态变量,冻结对象可能更具性能,但我还没有看到任何证据。
Object.freeze
的唯一实际用途是在开发过程中。对于生产代码,冻结/密封对象绝对没有好处。
愚蠢的打字
它可以帮助您在开发过程中发现这个非常常见的问题:
if (myObject.someProp = 5) {
doSomething();
}
在严格模式下,如果myObject
被冻结,这将引发错误。
强制执行编码协议/限制
它还将有助于在团队中执行特定的协议,尤其是对于可能与其他人编码风格不同的新成员。
很多Java爱好者喜欢在对象中添加很多方法,让JS感觉更熟悉。冷冻物体会阻止他们这样做。
所有其他答案几乎都回答了这个问题。
我只是想总结一下这里的所有内容以及一个例子。
- 当您需要对其未来状态提供最大保证时,请使用Object.freeze。您需要确保代码的其他开发人员或用户不会更改内部/公共属性。Alexander Mills的回答
- Object.freeze自2019年6月19日V8 v7.6发布以来具有更好的性能。菲利普的回答。还要看一下V8文档
以下是Object.freeze的作用,它应该会为那些对Object.freeze.只有表面理解的人消除疑虑
const obj = {
name: "Fanoflix"
};
const mutateObject = (testObj) => {
testObj.name = 'Arthas' // NOT Allowed if parameter is frozen
}
obj.name = "Lich King" // Allowed
obj.age = 29; // Allowed
mutateObject(obj) // Allowed
Object.freeze(obj) // ========== Freezing obj ==========
mutateObject(obj) // passed by reference NOT Allowed
obj.name = "Illidan" // mutation NOT Allowed
obj.age = 25; // addition NOT Allowed
delete obj.name // deletion NOT Allowed
当您使用交互式工具时,我可以看到这一点非常有用。而不是:
if ( ! obj.isFrozen() ) {
obj.x = mouse[0];
obj.y = mouse[1];
}
你可以简单地做:
obj.x = mouse[0];
obj.y = mouse[1];
只有当对象未冻结时,属性才会更新。
不知道这是否有帮助,但我用它来创建简单的枚举。它让我希望不会在数据库中获得糟糕的数据,因为我知道数据的来源是不可更改的,而没有故意破坏代码。从静态类型的角度来看,它允许对代码构造进行推理。
- 如果使用 lodash 将属性存在于另一个对象中,则向对象添加属性
- Javascript(Angular)从一个对象数组到第二个数组查找值
- 对一个对象使用reduce可以返回一个没有't在数组中包含目标字母
- AngularJS&JSON-从Previous&下一个对象
- jQuery$.inArray()总是返回-1和一个对象数组
- javascript处理一个对象数组以获得一个新的对象数组
- javascript函数,它接受两个输入:一个对象和一个键,并返回对象中该键的相应值
- 你能用来自数组的属性名称生成一个对象吗
- 预期响应包含一个对象,但在angular js中得到一个数组错误
- Protractor:element.getText()返回一个对象,而不是String
- 如何使用jQuery添加另一个对象的高度作为边距
- 计算从一个对象到另一个对象的路径并沿其移动
- 如何将一个对象添加到每个对象数组中
- 为了避免创建全局变量,可以将所有变量分配给一个对象吗
- 将javascript对象(属性+值)合并到一个对象中
- 尝试简化检查对象键是否为true并将其推送到另一个对象
- 从 javascript 中的对象方法返回一个对象
- 响应应包含一个对象,但得到的却是GET操作的数组
- js:如何制作一个循环,将20个不同的东西添加到一个对象中
- 为什么我需要在JavaScript中冻结一个对象