使用 setPrototypeOf 进行数组子类化

Array subclassing with setPrototypeOf

所以我阅读了一些博客文章、SO 线程和其他关于 subclassing Array in JavaScript 的讲座。对该主题的普遍看法是,没有办法创建具有某种缺点的子类。

在尝试一些事情时,我为自己想出了这个解决方案:

// This is the constructor of the new class.
function CustomArray() {

    // The "use strict" statement changes the way the "magic" variable
    // "arguments" works and makes code generally safer.
    "use strict";

    // Create an actual array. This way the object will treat numeric
    // properties in the special way it is supposed to.
    var arr = [],
        i;

    // Overwrite the object's prototype, so that CustomArray.prototype is
    // in the prototype chain and the object inherits its methods. This does
    // not break the special behaviour of arrays.
    Object.setPrototypeOf(arr, CustomArray.prototype);

    // Take all arguments and push them to the array.
    for (i = 0; i < arguments.length; i++) {
        arr.push(arguments[i]);
    }

    // Return the array with the modified prototype chain. This overwrites
    // the return value of the constructor so that CustomArray() really
    // returns the modified array and not "this".
    return arr;
}

// Make CustomArray inherit from Array.
CustomArray.prototype = Object.create(Array.prototype);

// Define a method for the CustomArray class.
CustomArray.prototype.last = function () {
    return this[this.length - 1];
};

var myArray = new CustomArray("A", "B", 3);
// [ "A", "B", 3 ]

myArray.length;
// 3

myArray.push("C");
// [ "A", "B", 3, "C" ]

myArray.length;
// 4

myArray.last();
// "C"

我的问题是:这段代码有什么问题吗?难以置信"the one solution"之前有这么多人搜索过

文章讨论了如何创建数组"subclass"。也就是说,我们想要创建一个在其原型链中具有 Array.prototype 的对象,但其直接原型父级不是 Array.prototype (即,以便原型父级可以提供数组之外的其他方法原型)。

那篇文章说创建数组 "subclass" 的一个基本困难是数组的行为来自 both

  1. 他们的原型,
  2. 只是数组实例。

如果数组从 Array.prototype 继承了它们的所有行为,我们的工作会非常快。我们将简单地创建一个对象,其原型链包括 Array.prototype。该对象将为我们的数组子类实例创建一个理想的原型。

但是,数组具有特殊的自动行为,这些行为数组实例独有,并且不是从原型继承的。 (特别是,我的意思是 length 属性 周围的行为会在数组更改时自动更改,反之亦然。)这些是由 [=15= 创建的每个数组实例提供的行为] 构造函数,在 ECMAScript 5 中无法忠实地模仿它们。因此,数组子类 的实例最初必须由 Array 构造函数 创建。如果我们想要适当的 length 行为,这是没有商量余地的。

这个要求与我们的另一个要求冲突,即实例必须有一个不是 Array.prototype 的原型。 (我们不想向 Array.prototype 添加方法;我们想向使用 Array.prototype 作为其自身原型的对象添加方法。)在 ECMAScript 5 中,使用 [=15= 创建的任何对象] 构造函数必须具有 Array.prototype 的原型父级。 ECMAScript 5 规范不提供在对象创建后更改其原型的机制。

相比之下,ECMAScript 6 确实提供了这样的机制。您的方法与文章“Wrappers. Prototype chain injection.”部分中描述的基于 __proto__ 的方法非常相似,只是您使用 ECMAScript 6 的 Object.setPrototypeOf 而不是 __proto__

您的解决方案正确地满足了以下所有要求:

  1. 每个实例实际上是一个数组(即,已由 Array 构造函数构造)。这确保 [[Class]] 内部 属性 正确,并且 length 行为正确。
  2. 每个实例都有一个不是 Array.prototype 的直接原型,但其原型链中仍包含 Array.prototype

这些要求以前在 ES5 中是不可能满足的,但是 ES6 使它变得相当容易。在 ES5 中,您可能有一个不满足要求 #2 的数组实例,或者一个不满足要求 #1 的普通对象。

实际上,通过使用神秘的 Array.of() 方法,甚至无需触及 Object.setPrototypeOf()__proto__ 即可实现数组子类化。 Array.of() 可以切换用于构造数组的构造函数。因为通常它绑定到 Array 对象,所以它会生成普通数组,但是一旦它绑定到另一个可以用作构造函数的对象(a.k.a 函数),它就会将该对象用作构造函数。让我们用 Array.of()

进行一些数组子类化

function SubArr(){}
SubArr.prototype = Object.create(Array.prototype);
SubArr.prototype.last = function(){return this[this.length-1]};

var what = Array.of.call(SubArr, 1, 2, 3, 4, "this is last");
console.log(JSON.stringify(what,null,2));
console.log(what.last());
console.log(what.map(e => e));
console.log(what instanceof Array);
console.log(Array.isArray(what));
what.unshift("this is first");
console.log(JSON.stringify(what,null,2));

因此,如您所见,使用 Array.of() 完成数组子类化是一项非常简单的任务。您可以找到它的规格 here。有趣的部分是;

NOTE 2 The of function is an intentionally generic factory method; it does not require that its this value be the Array constructor. Therefore it can be transferred to or inherited by other constructors that may be called with a single numeric argument.