【问题标题】:Does it make sense to create immutable objects that share structure by utilizing the javascript prototype system通过使用 javascript 原型系统创建共享结构的不可变对象是否有意义
【发布时间】:2015-11-07 15:57:17
【问题描述】:

到目前为止,Javascript 中的不变性似乎有两种相反的解决方案:

  • immutable.js
  • 无缝不可变

immutable.js 引入了他们自己的(浅显的)不可变对象,这些对象与对象和数组的默认 javascript 协议不兼容。

seamless-immutable 使用完全不可变的 POJO,没有任何魔法,但没有结构共享。

将两全其美的优点结合起来会很棒。不可变的原型链/树会是一个合适的解决方案吗?

底层原型机制带来希望:

var a = [1, 2, 3];
var b = Object.create(a);

b[0]; // 1
b.map(function (x) { return ++x; }); // 2, 3, 4 

b.push(4, 5, 6); // initial assignment of b
a; // 1, 2, 3
b;  // 1, 2, 3, 4, 5, 6

for (var i = 0; i < b.length; i++) {
  console.log(b[i]);
} // 1, 2, 3, 4, 5, 6

a[1] = null; // prototype mutation
a; // 1, null, 3
b; // 1, null, 3, 4, 5, 6

b.unshift(0); // instance mutation
a; // 1, null, 3
b; // 0, 1, null, 3, 4, 5, 6 !!!

当当前实例 (b) 的突变(未移位)使其原型无法提供它们的值时,js 引擎似乎会自动将这些值直接复制到实例中。我不知道,但这完全有道理。

但是,使用不可变(键控/索引)对象很快就会遇到问题:

var a = [1, 2, 3];
Object.freeze(a);
var b = Object.create(a);
b.push(4, 5, 6); // Error: Cannot assign to read only property "length"
Object.freeze(b);

这个很简单:length 属性继承自不可变原型,因此不可变。解决问题并不难:

var b = Object.create(a, {length: {value: a.length, writable: true}});

但可能还会有其他问题,尤其是在更复杂的现实世界场景中。

也许有人已经处理过这个想法并且可以告诉我,是否值得对此进行推理。

Aadit 对一个相关问题的answer 和Bergi 对此的评论触及了我的问题而没有给出答案。

【问题讨论】:

  • 我觉得这将是一个真正有趣的讨论,但对于 Stack Overflow 来说,这种讨论并不完全正确(更适用于特定问题)。一旦你达到 20 次代表,你就应该来chat 与我们讨论。
  • 是的,我不想在那边与 Aadit 进行讨论,但我很高兴在这里提供答案。并希望@AaditMShah 可以给出一些对位,我想学习一些新的东西:-)

标签: javascript prototype immutability prototypal-inheritance immutable.js


【解决方案1】:

js 引擎似乎会自动将这些值直接复制到实例中

这是因为所有的数组方法(shiftpush 等)只使用对索引的赋值(幸运的是,对.length,它不会在非数组上自动更新)。如您所知,赋值只是在继承对象上创建一个新属性,即使原型具有该属性(除非它具有奇怪的属性,例如在您的冻结长度示例中)。

不管怎样,你的实际问题是

不可变的原型链/树是否是一个合适的解决方案?

。问题是原型链永远不会被垃圾收集。一旦所有继承的属性都被您的新“变异”实例覆盖,引擎就不再知道您“不再需要”原型 - 并永远保留它。
您需要手动对它进行垃圾收集(取消引用),这正是 immutable.js 的结构共享所做的。只有mutating the [[prototype]] is a bad idea,所以您可以通过其他方式更好地管理您的结构并手动进行属性查找。

【讨论】:

  • 在提到原型树时,我指的是共享原型的概念。这实际上在原型上下文中具有误导性和笨拙。当然,每个 javascript 对象只能有一个直接原型。感谢您的澄清!
  • @IvenMarquardt:啊,我明白了。只是还有其他语言确实允许继承多个原型,并且更容易将它们交换出来,所以我想向您保证不会被这些混淆。
  • 是的,[[prototype]] 不是一个选项。我猜你不能防止内存泄漏,至少不能在这个低级别。
  • 也许你可以确定实例 i 及其原型 p 在 i 创建时关于它们的属性的相对补集。由于 i 和 p 保证保持不变(就像链中的所有其他原型一样),这些计算应该只发生一次(在创建新实例时)。如果没有差异或达到某个阈值,则 i 被深度复制并标记为 GC。如果我没有更多的隐式或显式引用,它将被垃圾收集。这听起来很复杂,我不知道它是否实用。
  • 我刚刚意识到,当您对数组进行子类化并因此破坏Object.prototype.toString.call 时,您在[[Class]] 中丢失了数组。太糟糕了!
【解决方案2】:

在使用原型系统进行结构共享时,除了内存泄漏之外,您还会遇到更多问题:

  • 长原型链中的不利查找时间
  • 在 ES5 中丢失了与数组相关的 [[DefineOwnProperty]]

在哈希表中查找通常非常有效。然而,如果一个对象经常发生变异,可能会出现很长的原型链。在最坏的情况下,必须遍历整个链。如果此类行为经常发生,可能会对性能产生不利影响。

数组子类化导致[[DefineOwnProperty]]的丢失。此内部方法负责将length 属性与数组中的实际元素数同步:

function A() {}
A.prototype = Array.prototype;
var a = new A();

a[0] = 0, a[1] = 1;
console.log(a.length); // 0
a.length = 2;
console.log(a); // [0, 1]
a.length = 1;
console.log(a[1]); // 1

[观点]我相信所有这些问题都可以解决,而且只需极少的代码、极小的 API 和浅浅的学习曲线。这样的系统可能不会像基于向量或哈希映射尝试的不可变数据结构那样有效。但这比仅仅复制对象要高效得多。[/opinion]

【讨论】:

    猜你喜欢
    • 2019-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多