我能否创建一个对象,使得调用Array.isArray()返回true,而不使用Array构造函数或数组字面量?

9

通过将对象的原型设置为 Array.prototype,我可以轻松地使一个普通对象看起来像一个数组:

const obj = {};
Reflect.setPrototypeOf(obj, Array.prototype);

(我知道魔法长度属性和稀疏数组也存在一些问题,但这不是本问题的重点。)
我想让Array.isArray(obj)返回true(当然不能修改Array.isArray()方法)。MDN关于Array.isArray()polyfill如下:
if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}
通过使用 Symbol.toStringTag 属性,我可以使 Object.prototype.toString.call(obj) 返回 '[object Array]'
obj[Symbol.toStringTag] = 'Array';
console.log(Object.prototype.toString.call(obj) === '[object Array]'); // true

现在,经过填充的Array.isArray()对于obj返回true(请忽略不支持Array.isArray()的浏览器都不支持Symbol.toStringTag的事实)。然而,本地Array.isArray()函数仍然对obj返回false。我查看了ECMAScript 2017规范,它说Array.isArray()使用抽象操作IsArray,如果参数是一个数组奇异对象,则返回true。如果参数是代理,则直接在目标对象上调用IsArray,因此似乎在这里使用代理并没有帮助。

有没有办法使Array.isArray(obj)返回true?为了明确起见,我不想修改Array.isArray()或任何其他内置对象。

这基本上与Can you fake out Array.isArray() with a user-defined object?相同,但该问题是5年前提出的,答案基于ECMAScript 5规范。我正在寻找基于ECMAScript 2017规范的答案。


2
你为什么想要这样做? - Explosion Pills
@ExplosionPills 我只是好奇这是否可能。 - Michał Perłakowski
4个回答

6
不,正如你所说的,真实的数组(也就是Array.isArray检测到的)是一个“数组外部对象”,这意味着它的.length表现出一种特殊的方式。
构造这个对象的唯一方法是使用数组构造函数或Array的子类(而子类中又调用了数组构造函数),或来自其他领域的相同构造函数。此外,无数其他方法返回新的数组(例如String::splitString::matchArray.fromArray.ofArray原型方法、Object.keysObject.getOwnPropertyNames)。此外,用于标记模板或作为代理应用/构建陷阱的函数将接收全新的数组,并且数组也会作为Promise.all.entries()迭代器的结果的一部分被构造出来。
如果你正在寻找创建数组的语法方式,那么数组字面量将是你的首选方式,但是解构表达式(在数组文字或函数中)也可以从迭代器创建数组。
如果你实际上的问题是“我能把任意对象转化为数组外部对象吗?”,答案是坚决的“不行”。

2

一个对象要么作为异类数组创建,要么没有创建,并且你不能更改它。

但是,正如您所提到的,您可以创建一个目标为数组的代理对象。

console.log(Array.isArray(new Proxy([], {}))) // true

代理对象不是一个数组,但是通过Array.isArray函数可以将其视为数组。它将是一个新的、不同的对象,但是您可以将所有内部操作重定向到所需的对象,从而实现成为一个实时克隆体。

function redirect(trap) {
  return (target, ...args) => Reflect[trap](obj, ...args);
}
var obj = {0:0, 1:1, length:2};
var arrayified = new Proxy([], {
  apply: redirect('apply'),
  construct: redirect('construct'),
  defineProperty: redirect('defineProperty'),
  deleteProperty: redirect('deleteProperty'),
  enumerate: redirect('enumerate'),
  get: redirect('get'),
  getOwnPropertyDescriptor: redirect('getOwnPropertyDescriptor'),
  getPrototypeOf: redirect('getPrototypeOf'),
  has: redirect('has'),
  isExtensible: redirect('isExtensible'),
  ownKeys: redirect('ownKeys'),
  preventExtensions: redirect('preventExtensions'),
  set: redirect('set'),
  setPrototypeOf: redirect('setPrototypeOf')
});
console.log(arrayified); // [0, 1]


我也可以使用常规数组,而不使用代理。整个重点是使用普通对象来完成这个任务。 - Michał Perłakowski
1
通过代理,您可以将内部操作重定向到所需的对象。 - Oriol

2
Babel可以将class MyObject extends Array转换成ES5代码,使得Array.isArray(new MyObject)返回true: 实时演示

ES2015

class MyObject extends Array{};

console.log(Array.isArray(new MyObject))

Babel生成的ES5

"use strict";

function _possibleConstructorReturn(self, call) {
    if (!self) {
        throw new ReferenceError(
            "this hasn't been initialised - super() hasn't been called"
        );
    }
    return call && (typeof call === "object" || typeof call === "function")
        ? call
        : self;
}

function _inherits(subClass, superClass) {
    if (typeof superClass !== "function" && superClass !== null) {
        throw new TypeError(
            "Super expression must either be null or a function, not " +
                typeof superClass
        );
    }
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    if (superClass)
        Object.setPrototypeOf
            ? Object.setPrototypeOf(subClass, superClass)
            : (subClass.__proto__ = superClass);
}

var MyObject = (function(_Array) {
    _inherits(MyObject, _Array);

    function MyObject() {
        return _possibleConstructorReturn(
            this,
            (MyObject.__proto__ || Object.getPrototypeOf(MyObject))
                .apply(this, arguments)
        );
    }

    return MyObject;
})(Array);

console.log(Array.isArray(new MyObject()));

这不是我的问题所在。我想找出,正如Bergi所说,我是否可以将任意对象转换为数组异类对象。 - Michał Perłakowski
1
我回答了标题中的问题,即使它对你没有帮助,人们仍然可能寻找答案。 - fregante

0

不可以。你可以很容易地创建类似数组的对象,但与.push()或其他修改length属性的数组方法不同,当你在像a[a.length] = "test"这样的异类数组中执行时,你永远无法使length属性自动增加,例如nodeListHTMLCollection

这里是我能够创建的最接近真正的数组的Frankenstarray,但它并没有使用真正的数组来扩展,而是使用了一个适当的对象。


你永远无法让length属性自行增加,但是使用代理(Proxy)可以实现。 - Michał Perłakowski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接