5

I am trying to write a fairly simple Javascript function, and experiencing behavior I don't understand when I iterate the function.

I have distilled the problem down to the following situation. I want to write a function that will take as input an array consisting of arrays of arrays, e.g. A = [[[1]]]. I don't know the standard terminology for this, so I am going to refer to the main array as "level 0", which has elements that are arrays of "level 1". I'll say the level 1 arrays consist of arrays of "level 2". The level 2 arrays consist of integers.

The function does the following, on input A (a level 0 array):

  1. create an empty array L;
  2. for each level 1 array M in A
    • add one to each integer entry in each level 2 array in M;
    • add two copies of M to L
  3. return L.

Here is my code:

function myFunc(A){
  var L = [];
  for(var a=0; a<A.length; a++){
    var M = A[a].slice(0);
    for(var i=0; i<M.length; i++){
      for(var j=0; j<M[i].length; j++){
        M[i][j]++;
      }
    }
    for(var s=0; s<2; s++){
      var N = M.slice(0);
      L.push(N);
    }
  }
  return(L);
}

Now I test it out:

var A = [[[1]]];

A = myFunc(A)

After this, I get A = [[[2]],[[2]]], which is what I expect. However, suppose I iterate it:

var A = [[[1]]];

A = myFunc(A);

A = myFunc(A);

Then I expect to obtain A = [[[3]],[[3]],[[3]],[[3]]], but instead I have A = [[[4]],[[4]],[[4]],[[4]]].

On the other hand if I run myFunc([[[2]],[[2]]]), I do get the expected [[[3]],[[3]],[[3]],[[3]]].

I don't understand where this discrepancy is coming from.

8
  • 1
    That's not the issue. The function when called twice does not have the same results, read his question again. I'm investigating and I got a lead I think Commented Apr 12, 2015 at 20:50
  • 1
    Its probably a referenced value vs a copied one. Try A = JSON.parse(JSON.stringify(A)) between your A = myFunct(A) calls. I cannot test I am on my iPhone Commented Apr 12, 2015 at 20:50
  • 1
    @FlavienVolken yes that does solve it but I do not understand why it's failing here. :o Commented Apr 12, 2015 at 20:53
  • 1
    Still digging, weird behavior found, do x = [[[1]]] and then call myFunc(x) multiple times. Commented Apr 12, 2015 at 21:01
  • I think the issue has to do with var M = A[a].slice(0). Since it's a multidimensional array, it gets a reference to the original A array. Then when you increment M[i][j]++ it's altering A. Same as what @TristanFoureur found in his answer :) Commented Apr 12, 2015 at 21:11

3 Answers 3

5

The problem is the line:

M[i][j]++;

Node keeps this as a reference to your A's slice, and you see it clearly when you do this :

x = [[[1]]];
myFunc(x);
myFunc(x);
console.log(x); // ---> [[[3]]]

For a shallow copy you'd have to use the JSON.parse(JSON.stringify()) trick, and proof that M is the problem; adding this line just after M = A[a].slice(0); solves the issue.

M = JSON.parse(JSON.stringify(M))

Mozilla's documentation about Array.prototype.slice() :

For object references (and not the actual object), slice copies object references into the new array. Both the original and new array refer to the same object. If a referenced object changes, the changes are visible to both the new and original arrays.

Source

That is why, because when you do M[i][j], the array a level deeper is still referenced outside.

Sign up to request clarification or add additional context in comments.

Comments

1

Since Tristan Foureur has already pointed out what caused the discrepancy, I just wanted to add in why

var A = [[[1]]];

A = myFunc(A);
A = myFunc(A);

gives a different result versus

myFunc([[[2]],[[2]]])

When doing myFunc([[[2]],[[2]]]), what you are basically doing is myFunc(new Array(new Array(new Array(2))),(new Array(new Array(2))))).

So when the V8 engine elevates this line

var M = A[a].slice(0);

it would interpret A[a].slice(0) as

new Array(new Array(new Array(2))),(new Array(new Array(2))))[a].slice(0)

and hence you get 2 (from the new Array) every time it is called.

This can be seen if you examine the function with logs:

function myFunc(A){
  var L = [];
  console.log("A is"+ JSON.stringify(A));
  for(var a=0; a<A.length; a++){
    var M = A[a].slice(0);
    console.log("M is"+ JSON.stringify(M) + " while A is" +JSON.stringify(A),a); 
    for(var i=0; i<M.length; i++){
      for(var j=0; j<M[i].length; j++){
        M[i][j]++;
      }
    }
    for(var s=0; s<2; s++){
      var N = M.slice(0);
      L.push(N);
      console.log("L.push:"+ JSON.stringify(N));
    }
  }
  console.log("the end" + JSON.stringify(L), JSON.stringify(A));
  return(L);
}

var A = [[[1]]];
A = myFunc(A);
A = myFunc(A);
var t = myFunc([[[2]],[[2]]]);

If you replace var M = A[a].slice(0); with

var M = new Array(new Array(new Array(2))),(new Array(new Array(2))))[a].slice(0)

and run through the function without A, you would see [[[3]],[[3]],[[3]],[[3]]].

Comments

1

As others have already mentioned the issue is that the M.slice call only did a shallow copy. They have also given good solutions for how to do a deepcopy. I'd propose that you really don't need to do the copy at all though:

var L = [];
for (var iLevel1 = 0; iLevel1 < A.length; iLevel1++)
{
    for (var iLevel2 = 0; iLevel2 < A[iLevel1].length; iLevel2++)
    {
        for (var iLevel3 = 0; iLevel3 < a[iLevel1][iLevel2].length; iLevel3++)
           L.push(a[iLevel1][iLevel2][iLevel3] + 1); 
    }
}

return L.concat(L);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.