Variable amount of nested for loops

One solution that works without getting complicated programatically would be to take the integers and multiply them all. Since you're only nesting the ifs, and only the innermost one has functionality, this should work:

var product = 0;
for(var i = 0; i < array.length; i++){
    product *= array[i];
}

for(var i = 0; i < product; i++){
    doSomething();
}

Alternatively:

for(var i = 0; i < array.length; i++){
    for(var j = 0; j < array[i]; j++){
        doSomething();
    }
}

Set up an array of counters with the same length as the limit array. Use a single loop, and increment the last item in each iteration. When it reaches it's limit you restart it and increment the next item.

function loop(limits) {
  var cnt = new Array(limits.length);
  for (var i = 0; i < cnt.length; i++) cnt[i] = 0;
  var pos;
  do {
    doSomething(cnt);
    pos = cnt.length - 1;
    cnt[pos]++;
    while (pos >= 0 && cnt[pos] >= limits[pos]) {
      cnt[pos] = 0;
      pos--;
      if (pos >= 0) cnt[pos]++;
    }
  } while (pos >= 0);
}

Recursion can solve this problem neatly:

function callManyTimes(maxIndices, func) {
    doCallManyTimes(maxIndices, func, [], 0);
}

function doCallManyTimes(maxIndices, func, args, index) {
    if (maxIndices.length == 0) {
        func(args);
    } else {
        var rest = maxIndices.slice(1);
        for (args[index] = 0; args[index] < maxIndices[0]; ++args[index]) {
            doCallManyTimes(rest, func, args, index + 1);
        }
    }
}

Call it like this:

callManyTimes([2,3,5], doSomething);

Recursion is overkill here. You can use generators:

function* allPossibleCombinations(lengths) {
  const n = lengths.length;

  let indices = [];
  for (let i = n; --i >= 0;) {
    if (lengths[i] === 0) { return; }
    if (lengths[i] !== (lengths[i] & 0x7fffffff)) { throw new Error(); }
    indices[i] = 0;
  }

  while (true) {
    yield indices;
    // Increment indices.
    ++indices[n - 1];
    for (let j = n; --j >= 0 && indices[j] === lengths[j];) {
      if (j === 0) { return; }
      indices[j] = 0;
      ++indices[j - 1];
    }
  }
}

for ([a, b, c] of allPossibleCombinations([3, 2, 2])) {
  console.log(`${a}, ${b}, ${c}`);
}

The intuition here is that we keep a list of indices that are always less than the corresponding length.

The second loop handles carry. As when incrementing a decimal number 199, we go to (1, 9, 10), and then carry to get (1, 10, 0) and finally (2, 0, 0). If we don't have enough digits to carry into, we're done.