为什么我需要在JavaScript中冻结一个对象

Why would I need to freeze an object in JavaScript?

本文关键字:冻结 一个对象 JavaScript 为什么      更新时间:2023-09-26

我不清楚什么时候有人需要在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);

现在,myNodechildren中有newNode,但newNodeparent中没有myNode。所以你刚刚打破了它。

(OFF-TOPIC)你为什么要暴露孩子们

是的,我可以创建很多方法:countChildren()、getChild(index)、getChildrenIterator()(返回生成器)、findChildIndex(node)等等……但这真的比只返回一个数组更好吗?数组提供了一个所有javascript程序员都知道的接口?

  1. 你可以访问它的length,看看它有多少孩子
  2. 您可以通过孩子的索引(即children[i])访问他们
  3. 您可以使用for .. of对其进行迭代
  4. 您还可以使用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;
    }

完成!

  1. Object.create:我们创建一个继承自this._children的对象(即this._children作为其__proto__)。仅此一项就几乎解决了整个问题:
    • 它简单快捷(恒定时间)
    • 您可以使用Array接口提供的任何内容
    • 如果修改返回的对象,则不会更改原始对象
  2. 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];

只有当对象未冻结时,属性才会更新。

不知道这是否有帮助,但我用它来创建简单的枚举。它让我希望不会在数据库中获得糟糕的数据,因为我知道数据的来源是不可更改的,而没有故意破坏代码。从静态类型的角度来看,它允许对代码构造进行推理。