为什么删除操作会保留数组元素?

12

今天我在Stack Overflow上偶然发现了一个问题 - 如何从JavaScript关联数组中删除对象?。让我感到困惑的是,被接受的答案既误导又得到了很多赞,因此我强调了可能存在的陷阱。

然而,在拼凑出一份纠正性答案的过程中,我意识到我不知道为什么使用delete会使元素成为assignundefined而不是被删除。

var elements = new Array()
elements.push(NaN)
elements.push(NaN)
elements.push(NaN)
delete elements[1]
console.log("number of elements: ", elements.length)   // returns 3

这背后有什么道理吗?


1
我很惊讶之前没有人问过这个问题,因为它触及了 JavaScript 核心和它处理数组的方式,非常“敏感”的地方。 - Shadow The Spring Wizard
1
请注意,链接问题的被接受答案是正确的。问题的措辞使用单词“数组”来表示“关联数组”,例如映射或字典。请注意,链接的问题根本不使用Array。(但如果它使用了,答案仍然是正确的;JavaScript数组实际上并不是真正的数组,请参阅我的答案以获取详细信息。) - T.J. Crowder
@Saul:delete 不按你的预期工作是完全有道理的。delete 在更低层次上直接作用于对象属性。它不“知道”你正在修改的对象是一个数组。如果 .length 的值是在访问时确定的,情况就不同了,但它并不是这样。 - Felix Kling
@T.J.Crowder - 我想这取决于你的观点。以+运算符为例。它在不同的构造函数(Number vs Array)中变异行为毫不犹豫。相比之下,delete则要简单得多。如果你问我,与根据操作数产生不同返回类型的+相比,更新数组的lengthdelete操作听起来相当合理。 - Saul
@T.J.Crowder - 从某种程度上来说,它们是有区别的... 比较 new Number() + new Number()new Array() + new Array()。 这两个操作都涉及到魔法,但归根结底,+ 对于不同的构造函数提供了不同的行为。这就是我想知道 delete 为什么会是现在这样的特殊原因。 - Saul
显示剩余12条评论
4个回答

12

我意识到我不知道为什么delete赋值undefined而不是删除它。

并不是这样的。 delete从对象中删除属性,不会将其设置为undefined。这里有一个简单的方法可以告诉你:

var a = ['a', 'b', 'c'];
console.log(1 in a); // logs "true"
delete a[1];
console.log(1 in a); // logs "false"
请确认下面翻译是否符合您的要求:

请注意,在执行完delete之后,a将不再具有名为1的属性。

与之相反的是:

var a = ['a', 'b', 'c'];
console.log(1 in a); // logs "true"
a[1] = undefined;
console.log(1 in a); // logs "true"

在这里,a仍然有一个叫做 1 的属性,只是这个属性的值是undefined

了解JavaScript中的一个有用的知识点是,数组实际上并不是真正的数组。它们只是对象,数组“索引”只是属性名称(字符串),我们将它们写成数字而已。数组有一些针对所有数字的属性名(索引)的特殊处理,如特殊的length属性和一些从Array.prototype继承来的函数。这在规范的第15.4节中非常清楚地阐述了。一旦你牢牢地掌握了JavaScript数组并不是真正的数组,它们就会变得更加容易理解。

从数组中删除一个“索引”属性并不会改变其length(即使您删除最高编号的属性),它只会在数组中创建一个空洞(JavaScript“数组”本质上是稀疏数组,即它们中间可能存在空隙)。因此,在上面的第一个示例中,我得到的正是如果我这样做会得到的完全相同的数组:

var a = [];
a[0] = 'a';
a[2] = 'c';

注意这个缺口,数组中没有 1 元素/属性。

如果你说:

var foo = a[3];

...foo 之所以可能获得值为 undefined,有两个完全不同的原因:

  1. a 拥有一个名为 3 的属性,其值为 undefined
  2. a 根本没有名为 3 的属性;在一个对象上执行属性访问操作时,如果该对象不存在该名称的属性,则返回 undefined。这种如果无属性则返回-undefined的情况,在规范中被很复杂地解释,但主要在第8.12.3节中描述。

这些是非常不同的事情。


@Saul: 因为delete在对象上是通用的,不会更新数组的长度,所以它始终是最高索引+1。但即使使用delete删除数组的最后一个元素也不会更新.length - Felix Kling
2
@Saul:length 属性只是数组中最高编号的属性加一。尝试 var a = new Array(); a[100] = 1; alert(a.length); - Tim Down
@jAndy: 我觉得这很难以置信。这与规格说明极为不符。 - T.J. Crowder
@FelixKling - 嗯...我会期望删除数组元素后其报告的长度也会相应减少。这就是我为什么会问的原因。不这样做有什么好处吗? - Saul
1
@jAndy: “无论如何,我将进行一些浏览器内存测试,因为我仍然不确定该规范是否以这种方式实现。” 只要符合规范的语义,实现可以自由地做任何它想做的事情。虽然我非常确定在任何成功的引擎中,new Array(500) 不会为 500 个属性(元素)分配内存,但这并不意味着引擎不可能在符合规范且有性能优势的情况下在某些地方使用连续内存数组。这对于编码人员来说必须是透明的;它必须按照规范工作。 - T.J. Crowder
显示剩余15条评论

6
它不会分配undefined,而是删除属性。(如果您尝试访问不存在的属性,则会得到undefined,并且length是基于数组中最高编号的项。)
这是有道理的,因为它在任何类型的对象上都是这样工作的。要使其以其他方式运作,它必须对对象进行特殊处理,如果它们是Arrayinstanceof,但仅当它是具有数字名称的属性时。
如果要从数组中删除项目,请使用splice
> var elements = [NaN, NaN, NaN];
> elements.splice(1,1);
> console.log(elements);
[ NaN, NaN ]

3
JavaScript数组可以是“稀疏”的。也就是说,某些槽位可能为空,从未分配过值或已删除值。如果您测试与该索引相关联的值,则会返回undefined,因为它不存在,而不是因为它被分配了undefined的值。
delete从数组中删除一个项目时,它不会自动滑动其余元素以填补空间:其他元素保留其现有索引,这反过来意味着.length属性不会更改,因为.length被定义为等于最高分配的索引加一。
如果要删除数组元素并使其他元素重新编号,请使用.splice()方法

1
这是因为 delete 运算符删除属性,被删除的属性的值为 undefined。要从数组中删除元素,可以使用数组的 splice 方法。 所有这些都是因为 JavaScript 中 delete 运算符的工作方式。
当您没有设置对象的属性并尝试检查其值时,它将为 undefined:
var obj = {};
alert(obj.foo); // undefined

这与以下内容相同:

alert(obj['foo']); // undefined

再看这个:

// create empty object
var obj = {};
// check its property named 1
alert(obj[1]); // undefined

// set property named 1 to value 'fooo'
obj[1] = 'fooo';
// and check it
alert(obj[1]); // 'fooo'

// now delete it 
delete obj[1];
// and check again
alert(obj[1]); // undefined 

delete已经删除了属性,其值为undefined - 而且以上所有代码都是关于对象的。

现在看看数组:

var arr = []; // it's better to use [] than new Array();
alert(arr[1]); // undefined  - same as above

// assign value
arr[1] = 'fooo' 
// check it
alert(arr[1]); // 'fooo' - same as above

// remove it 
delete arr[0];
// and check
alert(arr[1]); // undefined - same as above

行为是相同的,但数组的length属性呢?规范http://es5.github.com/#x15.4.5.2说:

“该Array对象的length属性是一个数据属性,其值始终数值上大于每个可删除属性的名称是数组索引的名称。”

所以当你看到这个:

var arr = ['foo', 'bar', 'foobar'];
alert(arr.length); // 3
// delete first
delete arr[1]; 
// and check length
alert(arr.length); // 3

上一次检查返回 3,因为此数组中最后一个可删除属性具有索引 2 - 第一个属性(索引 0)具有值 undefined(删除运算符设置了这个值),第二个项(索引 1)具有值 'bar',第三个(索引 2)具有值 'foobar'。因此,根据规范长度 = 2 + 1('始终数值大于最后可删除项');
这也可以在以下代码中看到:
var arr = [];

arr[10] = 'foo';
// element with index 10 is set to 'foo' but elements from 0 to 9 don't have value - they are undefined
// now check length
alert(arr.lenght); // 11 

最后可删除的属性索引是10,因此10 + 1等于11,尽管之前的元素未定义。

因此,删除运算符可以完成其工作,但它并不是设计用于从数组中删除项目的。


1
不,那完全是错误的,你之后的说法:“delete会将传入属性设置为undefined值。”才是正确的。 - T.J. Crowder

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