【问题标题】:Why does this array-like object behave like this?为什么这个类似数组的对象会有这样的行为?
【发布时间】:2017-06-29 03:34:16
【问题描述】:

通过将对象的原型方法设置为数组方法,对象的行为就像是对象和数组的混合体。下面是一个简单的例子:

function Foo() {}
Foo.prototype.push = Array.prototype.push;
Foo.prototype.forEach = Array.prototype.forEach;

var foo = new Foo();

foo.push('abc');

foo.length; // = 1 as expected. But wait, why isn't foo.length undefined? How/when did this property get attached to foo? 

foo[1] = 'def';

foo.length; // still = 1. But foo={0:'abc',1:'def'}, Why not =2?

foo.forEach(function(item) {
  console.log(item)
}); //shows only'abc' and not 'def'

foo.push('ghi');

foo.length; // = 2, and now foo = {0:'abc', 1:'ghi'}. So it overwrote the key=1, which means its accessing the same location, but the first approach did not change the length ( didn't become a part of the array ) why ?

foo.forEach(function(item) {
  console.log(item)
}); //now shows 'abc' and 'ghi'

为什么会发生所有这些奇怪的行为,为什么模仿这样的数组不好?

【问题讨论】:

  • 因为javascript中的数组仍然是它下面的对象,它模仿了我们从其他语言中知道的数组。就像您创建自己的复杂性并且难以维护一样。下一个查看您的代码的人会更加困惑。

标签: javascript arrays object ecmascript-6 prototype


【解决方案1】:

length 属性是如何设置的?

当您调用 Array#push 或在本例中为 Foo#push 方法时。根据ECMAScript 2015 Specification

22.1.3.17 Array.prototype.push ( ...items )

[...]

  1. [...]

     [...]

     d.设 lenlen+1

  1. setStatusSet(O, "length", len, true )。

因此,当您调用该函数时,如果它不存在,它会自动将长度属性设置为值len,每次推送时都会递增。

为什么长度不是2?

现在,当您直接在数组中设置索引时,不会更新长度属性。这是因为 JavaScript 中的数组是 exotic objects,当您直接设置属性时,它会在内部增加长度。再次按照规范:

9.4.2 数组奇异对象

[...]

每当创建或更改 Array 对象的自身属性时,都会根据需要调整其他属性以保持此不变性。具体来说,每当添加名称为数组索引的自有属性时,length 属性的值会在必要时更改为比该数组索引的数值大一;

根据定义,奇异对象是覆盖所有普通对象所具有的内部方法的任何对象。在这种情况下,数组外来对象会覆盖内部[[DefineOwnProperty]] 方法,因此当您定义数组的属性(例如设置索引)时,需要额外的步骤来确保更新属性length 等内容。您的Foo 构造函数不会创建奇异对象,因此它不会像数组那样覆盖[[DefineOwnProperty]] 内部方法——因此当您直接在索引处定义值时不会更新length

为什么push 会推送到上一个索引?

由于Foos 不是外来对象,因此在直接使用foo[1] = 'def' 添加元素时不会自动递增length,因此当您第二次尝试向其推送时length 仍为1 .如果我们再看Array#push

22.1.3.17 Array.prototype.push ( ...items )

[...]

  1. lenToLength(Get(O, "length"))。

[...]

  1. itemsList,其元素按从左到右的顺序是传递给此函数调用的参数。

[...]

  1. 重复,而 items 不为空
    一种。从 items 中移除第一个元素,并让 E 成为该元素的值。
    湾。让 setStatusSet(O, ToString(len), E, true )。

因此,由于foo[1] = 'def' 没有修改length 属性,因此您的数组长度仍为1,它会将新的待推送元素设置为索引1,因为长度为1。

同样的原则也适用于Array#forEachforEach 依赖于 length 来遍历数组。由于当您执行 foo[1] = 'def' 并保持 1 时您的长度没有被修改,forEach 仅从索引 [0, 1) 进行迭代,导致它仅记录第一个元素。推送更新长度并使其从 [0, 2) 迭代并记录两个元素。

为什么我不应该这样做?

这是因为数组是奇异的对象。它们无法与常规对象相处,因为常规对象从根本上无法实现与外来对象相同的行为。根据定义,外来对象会覆盖内部方法的默认行为,以实现功能所需的某些行为。在这种情况下,数组必须特别处理设置索引和管理长度——使用内部方法[[DefineOwnProperty]] 完成。对于常规对象,它不会覆盖[[DefineOwnProperty]],因此许多基本操作都无法正常工作——所以你不应该这样做。

不过,您可以使用诸如 Proxy 对象之类的外来对象来实现您自己的代码,用于 [[DefineOwnProperty]] 之类的数组来模拟行为。另一种方式,正如loganfsmyth 所提到的,您可以正确使用 ES2015 类到 extend and subclass builtin exotic object constructors 从而模仿数组行为。

【讨论】:

  • @mutenka 再次编辑地址forEach 以及为什么不应该这样做。
  • 请注意,您可以使用真正的 ES6 class Foo extends Array 扩展 Array,因为 ES6 类语法正确返回了一个奇特的数组,它只是不适用于标准ES5 原型扩展。
  • @loganfsmyth 很有趣,感谢您提供的信息!我会补充的。
【解决方案2】:
foo.push('abc');

foo.length; // = 1 as expected. But wait, why isn't foo.length undefined? 
            //How/when did this property get attached to foo? 

.push() 方法正在设置.length,就像使用真实数组一样。 .length 不存在并不担心,它会将其视为 0 - 因此在键 0.length 处添加的新元素设置为 1

foo[1] = 'def';

foo.length; // still = 1. But foo={0:'abc',1:'def'}, Why not =2?

你的对象不是一个数组,所以虽然添加带有数字索引的元素是可行的(尽管它们被转换为字符串,就像真正的数组对象索引一样)它并没有神奇地为你设置 .length 喜欢它会用一个真正的数组。 1 的现有 .length 因而没有改变。

foo.push('ghi');
foo.length; // = 2, and now foo = {0:'abc', 1:'ghi'}. So it overwrote the key=1,
// which means its accessing the same location, but the first approach
// did not change the length ( didn't become a part of the array ) why ?

.push() 方法使用.length 的现有值来决定在哪里添加新元素。根据.length 你只有一个元素,所以新的元素被推入索引1.length 被更改为2

【讨论】:

    【解决方案3】:

    push() 函数更新数组的长度,所以在foo[1] = 'def' 之后,长度仍然是 1。foo 是一个对象,因此将 'def' 分配给键 1 有效,但不会增加长度,在 foo.push('ghi') 长度之后增加。此外,foo.push('ghi') 使用 foo 的当前长度(当时为 1)作为“ghi”的索引。 同样在您第一次调用push() 函数之前,foo.length 实际上是未定义的。 push() 函数第一次定义它,之后每次调用都会增加它。

    【讨论】:

      猜你喜欢
      • 2022-01-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多