利用 javascript 原型系统创建共享结构的不可变对象是否有意义
Does it make sense to create immutable objects that share structure by utilizing the javascript prototype system
到目前为止,在 Javascript 中似乎有两种对立的不变性解决方案:
- immutable.js
- 无缝不可变
immutable.js 引入了它们自己的(浅层)不可变对象,这些对象与对象和数组的默认 javascript 协议不兼容。
seamless-immutable 使用 POJO,这些 POJO 在没有任何魔法的情况下是完全不可变的,但没有结构共享。
将两全其美结合起来会很棒。不可变原型 chains/trees 是一个合适的解决方案吗?
底层原型机制给了希望:
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 引擎似乎会自动将这些值直接复制到实例中。我不知道,但这完全有道理。
但是,使用不可变 (keyed/indexed) 对象很快就会遇到问题:
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);
这个很简单:长度属性继承自不可变原型,因此不可变。解决问题并不难:
var b = Object.create(a, {length: {value: a.length, writable: true}});
但可能还会有其他问题,尤其是在更复杂的现实世界场景中。
也许有人已经处理过这个想法并且可以告诉我它是否值得推理。
Aadit 对相关问题的 answer 和 Bergi 对此的评论触及了我的问题,但没有给出答案。
the js engine seems to copy these values straight into the instance automatically
那是因为所有数组方法(shift
、push
等)都只使用对索引的赋值(幸运的是,.length
,它不会在非数组)。如您所知,赋值只是在继承对象上创建一个新的 属性,即使原型具有 属性(除非它具有奇怪的属性,如您的冻结长度示例)。
无论如何,你的实际问题是
Could immutable prototype chains/trees be a proper solution?
否。问题是原型链永远不会被垃圾回收。一旦所有继承的属性都被新的 "mutated" 实例覆盖,引擎将不再知道您 "don't need" 原型 - 并永远保留它。
您需要手动对其进行垃圾收集(取消引用),这正是 immutable.js 及其结构共享所做的。仅此 mutating the [[prototype]] is a bad idea,因此您可以通过其他方式更好地管理您的结构并手动执行 属性 查找。
除了内存泄漏之外,当您使用原型系统进行结构共享时还会遇到其他问题:
- 长原型链中的不利查找时间
- 在 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]
到目前为止,在 Javascript 中似乎有两种对立的不变性解决方案:
- immutable.js
- 无缝不可变
immutable.js 引入了它们自己的(浅层)不可变对象,这些对象与对象和数组的默认 javascript 协议不兼容。
seamless-immutable 使用 POJO,这些 POJO 在没有任何魔法的情况下是完全不可变的,但没有结构共享。
将两全其美结合起来会很棒。不可变原型 chains/trees 是一个合适的解决方案吗?
底层原型机制给了希望:
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 引擎似乎会自动将这些值直接复制到实例中。我不知道,但这完全有道理。
但是,使用不可变 (keyed/indexed) 对象很快就会遇到问题:
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);
这个很简单:长度属性继承自不可变原型,因此不可变。解决问题并不难:
var b = Object.create(a, {length: {value: a.length, writable: true}});
但可能还会有其他问题,尤其是在更复杂的现实世界场景中。
也许有人已经处理过这个想法并且可以告诉我它是否值得推理。
Aadit 对相关问题的 answer 和 Bergi 对此的评论触及了我的问题,但没有给出答案。
the js engine seems to copy these values straight into the instance automatically
那是因为所有数组方法(shift
、push
等)都只使用对索引的赋值(幸运的是,.length
,它不会在非数组)。如您所知,赋值只是在继承对象上创建一个新的 属性,即使原型具有 属性(除非它具有奇怪的属性,如您的冻结长度示例)。
无论如何,你的实际问题是
Could immutable prototype chains/trees be a proper solution?
否。问题是原型链永远不会被垃圾回收。一旦所有继承的属性都被新的 "mutated" 实例覆盖,引擎将不再知道您 "don't need" 原型 - 并永远保留它。
您需要手动对其进行垃圾收集(取消引用),这正是 immutable.js 及其结构共享所做的。仅此 mutating the [[prototype]] is a bad idea,因此您可以通过其他方式更好地管理您的结构并手动执行 属性 查找。
除了内存泄漏之外,当您使用原型系统进行结构共享时还会遇到其他问题:
- 长原型链中的不利查找时间
- 在 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]