3

I've been trying to implement a method which takes an array and an index as input, copies the last element of the given array into the entry at the given index, and then truncates the last element (i.e., sets the array shorter by one).

Here is the function which I was given as a suggestion for this task:

function swapWithLastAndRemove(array, index) {
    array[index] = array.pop();
}

I then tested this function to make sure that it works on any given index:

for (let index = 0; index < 5; index++) {
    var array = [0, 1, 2, 3, 4];
    swapWithLastAndRemove(array, index);
    console.log(array);
}

And I found it that it works correctly for all but the last index:

[ 4, 1, 2, 3 ]
[ 0, 4, 2, 3 ]
[ 0, 1, 4, 3 ]
[ 0, 1, 2, 4 ]
[ 0, 1, 2, 3, 4 ]

In other words, for the last element, it pops it out of the array, but then rewrites it into the array.

What strikes me, is that this line:

array[index] = array.pop();

Which is equivalent to this line when executed on the last element:

array[array.length - 1] = array.pop();

Could not have possibly resulted with the contents of array being equal to [ 0, 1, 2, 3, 4 ].

The way I see it, there are two options here:

  1. The statement array.pop() is executed before the expression array.length - 1 is evaluated
  2. The statement array.pop() is executed after the expression array.length - 1 is evaluated

In the first case, the contents of array would change as follows:

  • [ 0, 1, 2, 3, 4 ] // initial state
  • [ 0, 1, 2, 3 ] // after popping 4
  • [ 0, 1, 2, 4 ] // after assignment

In the second case, it should have triggered some sort of memory-access violation, because there would be an attempt to write into the 5th entry in the array (array[4]), when the length of the array is only 4 entries.

I know that NodeJS could be applying some memory-management scheme which would somehow allow this to complete without an "array index out of bound" exception, but I still don't get why it would let this operation "get away with it", and moreover, why the result is what it is.

Is the line array[array.length - 1] = array.pop() possibly undefined behavior?

Is there even such thing as undefined behavior in JavaScript?

halfer
  • 19,824
  • 17
  • 99
  • 186
goodvibration
  • 5,980
  • 4
  • 28
  • 61
  • what result do you want by taking the last index? – Nina Scholz Nov 27 '19 at 10:55
  • "*In the second case, it should have triggered some sort of memory-access violation,*" why? `arr = []; arr[42] = "hello"` is not a problem. Arrays in JS are dynamic and don't really correspond to arrays in most other languages but are more like an ArrayList in Java – VLAZ Nov 27 '19 at 10:59
  • 1
    No, there's no undefined behaviour in JS wrt evaluation order - it's always left-to-right. The `arrray.length-1` expression is evaluated before the `array.pop()` call – Bergi Nov 27 '19 at 11:14

2 Answers2

2

The last index results in [0, 1, 2, 3, <empty>, 4], not [ 0, 1, 2, 3, 4 ]:

function swapWithLastAndRemove(array, index) {
  array[index] = array.pop();
}

var array = [0, 1, 2, 3, 4];
swapWithLastAndRemove(array, 5);
console.log(array);

Execution order isn't really something to worry about here, because the index is constant - the parameter index is never reassigned, so the number passed into swapWithLastAndRemove will always be the index, which the popped item is assigned to. Considering it introduces more confusion than necessary - easier to just keep in mind that the passed index is never reassigned.

What's going on is:

(1) The last item of the array is popped off (4), resulting in the array becoming [0, 1, 2, 3]

(2) The index passed in was 5, so array[5] is set to 4. The operation is equivalent to:

const arr = [0, 1, 2, 3];
arr[5] = 4;
console.log(arr);

This is resulting in a sparse array - there's no item at index 4 anymore. It's not undefined behavior, it's just weird, because sparse arrays are weird.

Is there even such thing as undefined behavior in Javascript?

Yes, see here.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • 1
    I think @goodvibration is trying to assign value at `arr[4], not arr[5]` – Harun Or Rashid Nov 27 '19 at 12:10
  • Sorry, but your call to `swapWithLastAndRemove(array, 5)` is not what I do in my code and not what I meant to do either. Indexes in Javascript (as well as any other language as far as I'm aware of) are 0-based. So the index of the last element in my case is 4, not 5. And this is also the index which I am using in my coding example above. As far as I can tell, this renders your entire answer irrelevant (though parts of it may be true, but I have t admit that I stopped reading once I saw that "5"). – goodvibration Nov 27 '19 at 14:08
  • @HarunurRashid: Yes, just so your comment, you are correct. – goodvibration Nov 27 '19 at 14:09
2

The second option: The statement array.pop() is executed after the expression array.length - 1 is evaluated

This is indeed what happens. JS evaluation is always left-to-right. It evaluates the array.length - 1 to index 4 of the array, then pops the last element from the array, then assigns that element to index 4.

In the second case, it should have triggered some sort of memory-access violation, because there would be an attempt to write into the 5th entry in the array (array[4]), when the length of the array is only 4 entries.

No, there are no memory access violations in JS. It just creates a new property again, and changes the length of the array accordingly. It's just the same as what happens with the code

const array = [0, 1, 2, 3, 4];
const element = array.pop();
console.log(JSON.stringify(array)); // [0,1,2,3]
array[4] = element;
console.log(JSON.stringify(array)); // [0,1,2,3,4]

Similarly, filling an array uses the same feature:

const array = [];
for (let i=0; i<5; i++)
    array[i] = i; // no memory access violation
console.log(array.length); // 5
Bergi
  • 630,263
  • 148
  • 957
  • 1,375