6

Note: I'm only a novice coder, so there might be a glaring error or misconception at the heart of this question.

Essentially, I need to deep copy multidimensional arrays 'by value' in JavaScript to an unknown depth. I thought this would require some complex recursion, but it seems that in JavaScript you only need to copy one level deep in order to copy the whole array by value.

As an example, here is my test code, using a deliberately convoluted array.

function test() {
  var arr = [ ['ok1'],[],[ [],[],[ [], [ [ ['ok2'], [] ] ] ] ] ];
  var cloned = cloneArray(arr);
  arr = '';   // Delete the original
  alert ( cloned );
}


function cloneArray(arr) {  
  // Deep copy arrays. Going one level deep seems to be enough.
  var clone = [];
  for (i=0; i<arr.length; i++) {
    clone.push( arr[i].slice(0) )
  }
  return clone;
}

In my running of this test (latest stable Chrome and Firefox on Ubuntu), even the deepest parts of the array seem to be successfully copied by value in the clone, even after the original is deleted, despite the fact that the slice() "copying" only went one layer deep. Is this the standard behaviour in JavaScript? Can I depend on this to work for older browsers?

user2625911
  • 97
  • 1
  • 7
  • 1
    On stackoverflow, you should not edit your question to provide the eventual answer. That's what the best answer designation is for. Stackoverflow wants the question to appear as you originally asked it (plus any clarifying edits) so that is the context that people see when they come back later. – jfriend00 Aug 03 '14 at 02:30
  • OK, I will edit the question back to its original state. – user2625911 Aug 03 '14 at 02:38

4 Answers4

2

Your code does not work:

  • If your array contains other variables such as numbers or strings (not only arrays), it will fail, because it will call arr[i].slice(), but since that arr[i] is a number, it hasn't got any .slice property and this will throw an error.
  • Your function will, in any case, leave all the references to objects and other stuff inside the array alive. So you will not actually obtain a copy of your array.

Example:

var a = [1,2, [11,13], ['ok']];
var b = cloneArray(a);

> TypeError: undefined is not a function // because numbers have no .slice method

Solution:

To copy an array you will need to make a deep copy of it. Since that creating a deep copy will need a function that uses recursion to deep copy any object or array inside of the main one, the easiest way to do it is by using jQuery and its .extend method, which performs a deep copy of the array, see here for more info.

var a =[[1], [2], [3]];
var b = $.extend(true, [], a);

b[0][0] = 99;
a[0][0] // still 1
Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Marco Bonelli
  • 63,369
  • 21
  • 118
  • 128
  • This isn't an answer to the question in any way. – jfriend00 Aug 03 '14 at 01:10
  • 1
    Except that jQuery isn't part of the question at all. A solution should generally only require jQuery if the OP mentioned jQuery or tagged jQuery. As it stands now, this is a plain javascript question. Also why does your first bullet point refer to `.split()` when that has nothing to do with the question or answer? – jfriend00 Aug 03 '14 at 02:28
2

Your test is flawed for whether a true copy is being made which makes your conclusion incorrect that you are getting a full copy of all the data in the nested arrays. You are only doing a two level copy, not an N level copy.

Javascript is a garbage collected language so you don't actually delete variables or objects and, even if you tried that doesn't affect the same variable if it's being referenced somewhere else in your code. To see if you truly have a completely independent copy, try nesting an object two levels deep and then change a property on the object in the original array. You will find that the same object changes in the cloned array because you aren't doing a deep clone. Both arrays have a reference to the exact same object.

Here's an example.

function cloneArray(arr) {  
  // Deep copy arrays. Going one level deep seems to be enough.
  var clone = [];
  for (i=0; i<arr.length; i++) {
    clone.push( arr[i].slice(0) )
  }
  return clone;
}

var x = [[{foo: 1}]];

var y = cloneArray(x);
x[0][0].foo = 2;

// now see what the value is in `y`
// if it's 2, then it's been changed and is not a true copy
// both arrays have a reference to the same object
console.log(y[0][0].foo);    // logs 2

The same result would happen if the third level was another array too. You will have to recursively traverse every element that is an object type and then clone that object itself to get a complete clone of everything in the nested arrays.

If you want code that will do a deep copy (to an arbitrary level) and work for all data types, see here.

FYI, your cloneArray() function assumes that all first level members of your array are arrays themselves and thus doesn't work if it contains any other type of value.

Community
  • 1
  • 1
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Indeed as you say, it seems I was running into garbage collection behaviour. I changed my test as follows: [link](http://jsfiddle.net/CRWfs/1/), and changes to the original array did affect my supposedly "cloned" array. As it happens, I can rely that all first (and second) order members of the array will themselves be arrays, and none of the members will be objects. I want to take a DIY approach even if it means reinventing the wheel (just for a learning experience), so I'll try to come up with my own system for cloning the array reliably. Thanks for the help. – user2625911 Aug 03 '14 at 02:26
2

Array.prototype.slice is not suitable for cloning arrays

This should work well for you

function deepClone(arr) {
  var len = arr.length;
  var newArr = new Array(len);
  for (var i=0; i<len; i++) {
    if (Array.isArray(arr[i])) {
      newArr[i] = deepClone(arr[i]);
    }
    else {
      newArr[i] = arr[i];
    }
  }
  return newArr;
}

If you need to support older browser, make sure to use this polyfill (via MDN)

if(!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}
maček
  • 76,434
  • 37
  • 167
  • 198
  • It looks like you're only doing a deep copy of arrays, not objects. – jfriend00 Aug 03 '14 at 01:25
  • 1
    Well yeah, that meets the requirements of the question. The asker makes no mention of requirement for copying (non-array) objects. – maček Aug 03 '14 at 03:14
0

Here's my recursive approach to "cloning" a multidimensional array. It runs all the way down to the deepest levels:

if ( !Array.clone )
{
        Array.prototype.clone = function()
        {
                var _arr = ( arguments[0] == null ) ? [] : arguments[0] ; 
                for( var _p = 0 ; _p < this.length ; _p++ )
                {
                         if ( this[_p] instanceof Array )
                         {
                                 var _sub = [] ;
                                 this[_p].clone( _sub ) ;
                                 _arr.push( _sub.slice() );
                         }
                         else _arr.push( this[_p] );
                }

                return _arr ;
        }
}

Now try this code:

var _a = [ "a", "b", [ "c", "d", [ "e", "f" ] ] ];
var _b = _a.clone();
console.log( _b );

Vars _a and _b are two distinct objects: if you remove an element from var _b, then var _a would not be affected.

Sandro Rosa
  • 507
  • 4
  • 12