为什么Array.prototype.push返回新长度而不是更有用的东西?

30
自从在ECMA-262第3版中引入以来,Array.prototype.push方法的返回值是一个Number

  

15.4.4.7 Array.prototype.push([item1 [,item2 [,…]]])

     

参数按照它们出现的顺序附加到数组的末尾。 调用的结果是将新长度作为返回值。

返回数组新长度而不是返回一些更有用的内容(例如新追加项的引用或已变异的数组本身)的设计决策是什么? 为什么要这样做,是否有关于这些决策是如何形成的历史记录?


1
你为什么认为数组长度没有用处? - Bergi
相关链接:https://dev59.com/VGAg5IYBdhLWcg3wpMSz。 - user663031
4
对我而言,大部分情况下返回已变异的数组更加有用。 - Stanley Luo
7个回答

13

我在TC39的交流中心发表了这篇文章,并且能够了解到更多关于它背后历史的知识:

push, pop, shift, unshift were originally added to JS1.2 (Netscape 4) in 1997.

There were modeled after the similarly named functions in Perl.

JS1.2 push followed the Perl 4 convention of returning the last item pushed. In JS1.3 (Netscape 4.06 summer 1998) changed push to follow the Perl 5 conventions of returning the new length of the array.

see original jsarray.c source

/*
 * If JS1.2, follow Perl4 by returning the last thing pushed.  Otherwise,
 * return the new array length.
 */

13

我理解使用array.push()期望返回已改变的数组而不是其新长度,以及出于链式调用的原因希望使用这种语法。
然而,有一种内置的方法可以实现这一点:array.concat()。 请注意,concat 期望被给予一个数组而不是一个项。因此,如果它们不在一个数组中,请记得用 [] 包装您想要添加的项。

newArray = oldArray.concat([newItem]);

通过使用.concat()来完成数组链接,因为它返回一个数组,
但不能使用.push()来完成,因为它返回一个整数(数组的新长度)。

以下是在React中基于其先前值更改state变量的常见模式:

// the property value we are changing
selectedBook.shelf = newShelf;
       
this.setState((prevState) => (
  {books: prevState.books
           .filter((book) => (book.id !== selectedBook.id))
           .concat(selectedBook)
  }
));

state 对象有一个 books 属性,它保存着一个 book 数组。
book 是一个对象,拥有 idshelf 属性(以及其他属性)。
setState() 接受一个对象,该对象保存了要分配给 state 的新值。

selectedBook 已经在 books 数组中,但是需要更改它的 shelf 属性。
然而,我们只能给 setState 一个顶级对象,无法告诉它去寻找这本书,并且查找该书的某个属性,将其赋予新值。

因此,我们先取出原来的 books 数组。
使用 filter 方法删除旧版本的 selectedBook
然后使用 concat 方法将更新后的 selectedBook 添加回数组中。

尽管使用 push 链式操作也能很好地完成目标,但用 concat 是正确的方法。

总结:

  • array.push() 返回一个数字(数组新长度)。
  • array.concat([]) 返回一个新数组。

实际上,它返回一个新的数组,并将修改后的元素添加到末尾,而不改变初始数组。 返回新的数组实例,而不是重复使用现有数组实例,这是很重要的区别,它使其在 React 应用程序中的状态对象中非常有用,以获得更改的数据并重新渲染。


哦,我不太明白。在任何正常的JS环境中,oldArray.concat[newItem]; 对我来说都会返回未定义。在https://2ality.com/2016/11/concat-array-literal.html上有一个有趣的相关讨论,但没有解释你的语法应该如何工作。我猜这是React特有的东西?一些魔法干扰了数组原型吗? - Tao
1
我认为你打错字了 @Tao。应该是 oldArray.concat(newItem) - Nick Manning
3
感谢@NickManning,这确实是关键点;在我的评论大约5个月后,@SherylHohman编辑了答案,将明显错误的newArray = oldArray.concat[newItem];更改为正确的newArray = oldArray.concat([newItem]);,从而回答了我的问题。然而,他们在底部的摘要中仍然留下了错误的语法,但我现在也已经纠正了。我的困惑完全得到了解决。 - Tao
2
是的 - 就记录而言,我认为你也可以连接一个项目 [].concat(3) 和 [].concat([3]) 都是做同样的事情。 - Nick Manning

2

我无法解释为什么他们选择返回新长度,但是针对您的建议:

  • 返回新添加的项目:

鉴于JavaScript使用C风格赋值,它会发出分配的值(而不是Basic风格赋值不会),因此您仍然可以具有该行为:

var addedItem;
myArray.push( addedItem = someExpression() );

尽管我认识到这意味着您不能将其作为声明+分配组合中的r-value的一部分。
返回已经改变的数组本身:
这种方式是“流畅”API的风格,它在ECMAScript 3完成后显著流行起来,并且它不符合ECMAScript中其他库特性的风格。再次强调,通过创建自己的push方法来实现您想要的场景并不需要太多的额外工作。
Array.prototype.push2 = function(x) {
    this.push(x);
    return this;
};

myArray.push2( foo ).push2( bar ).push2( baz );

或者:

Array.prototype.push3 = function(x) {
    this.push(x);
    return x;
};

var foo = myArray.push3( computeFoo() );

2
关于流畅的API:对于那些改变数组的方法,它们并不会返回该数组。如果你主要是为了副作用而使用它们,那么它们并不真正适合链式调用。 - Bergi

2

既然你问了,我做了一个样本数组并在Chrome中进行了检查。

var arr = [];
arr.push(1);
arr.push(2);
arr.push(3);
console.log(arr);

enter image description here

由于我已经有了对数组的引用以及我推入其中的每个对象,只有另一个属性可能有用……长度。通过返回这个数组数据结构的一个附加值,我现在可以访问所有相关信息。这似乎是最好的设计选择。或者如果您想为了节省1个机器指令而争论的话,就什么都不返回。

为什么要这样做,是否有关于这些决策如何被做出的历史记录?

不清楚-我不确定是否存在这样一种基于这些理由的记录。这取决于实现者,并且很可能在实现ECMA脚本标准的任何给定代码库中进行了注释。


1
[1,2,3].map(x => x+=4).push(1) //=== 4,如果它返回 [5,6,7,1] 那就太好了,不是吗?但是... [1,2,3].map(x => x+=4).concat(1) //=== [5, 6, 7, 1] - MrHIDEn

1
如果您正在“链接”操作,并且需要获取对刚刚推入项的数组的引用,则可以使用逗号表达式:

const a = [1,2,3]
const b = (a.push(4), a).reverse()
console.log(a)
console.log(b)

如果您想创建一个新的数组并添加一个额外的元素,可以使用扩展语法:

const a = [1,2,3]
const b = [...a, 4].reverse()
console.log(a)
console.log(b)

请注意,reverse()会就地反转数组。这就是为什么在第一个示例中,ab变量都引用相同的反转数组。在第二个示例中,ab引用不同的数组,只有其中一个被反转。

0

你提到的文档(Ecma 262第3版)部分回答了这个问题,有一些方法会改变数组,而有一些则不会。那些改变数组的方法将返回改变后的数组长度。如果要添加元素,可以使用push、splice和unshift(取决于您想要新元素的位置)。

如果您想获取新的改变后的数组,可以使用concat。Concat将输入任意数量的数组,并将所有元素添加到一个新数组中。例如:

const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3=['g','h'];
const array4 = array1.concat(array2,array3);

新创建的数组将包含所有元素,其他三个元素不会被更改。有许多添加元素到数组的方法,既可以是可变的也可以是不可变的。所以这就是你的答案,它返回长度是因为它正在更改它,它不需要返回完整的数组。

0

我不知道为什么会这样做,是否有这些决定形成的历史记录呢?

但我也觉得,push() 返回 数组的长度 是不清晰和不直观的,就像下面这样:

let arr = ["a", "b"];

let test = arr.push("c");

console.log(test); // 3

如果你想使用清晰直观的方法而不是push(),那么你可以使用concat(),它会返回包含数组值的数组,如下所示:

let arr = ["a", "b"];

let test = arr.concat("c");

console.log(test); // ["a", "b", "c"]


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