Problem
The problem:
I have a function that writes an array it takes as argument to a div, in a single line.
Example:
writeToOutput([1,2,3])
outputs
1,2,3
The aim is to improve this function, to be able to take an array of arguments, and display them on several lines
Example: writeToOutput([[1,2,3],[4,5,6]])
outputs
1,2,3
4,5,6
An obvious solution is to use writeToOutput([[1,2,3]])
to replicate the previous results, however, the function call with a 1-D array is already used by several scripts, and I would like them to keep working.
The code:
function writeToOutput(values, outputName) {
var outputDiv = document.getElementById(outputName)
var outputText = "";
if (!Array.isArray(values[0])) {
values = [values]
}
var valuesSubArray;
for (var i = 0; i < values.length; i++) {
valuesSubArray = values[i];
outputText = "";
for (var j = 0; j < valuesSubArray.length; j++) {
outputText += valuesSubArray[j]
}
if (i > 0) {
outputDiv.innerHTML += "</br>";
}
outputDiv.innerHTML += outputText;
}
}
What I am interested in:
As you may have guessed, I don’t really care about writing numbers to a div, but I have a legacy function that takes a 1-D array, and I would like it to deal with 2-D arrays without breaking existing implementation.
I am interesting in figuring out if the trick with Array.isArray(values[0])
is the best way to do this, or if there is more performant, more widely used, etc. ways to do this. (The project is using jQuery).
Altought this stub has been made to sugarcoat the 1-D -> 2-D array, I am eager to learn, and if there is an obvious improvement to be made, please let me know!
Solution
Regarding your main question
From my point of view, yes, your current solution to allow legacy 1-D arrays to work with your new version is the best one I can think.
Rather funny: I just proposed a close solution in a PHP context question, less than 24 hours ago!
Another obvious method might be to keep the legacy function as is, and to write an almost identical one (simply expecting only 2-D arrays) with an alternative name.
But it doesn’t seem better at all, notably because in case of changes regarding the core job they do, these two functions should be both updated!
Now since you asked for other possible improvements
I know that your example is not a real one, but since you implemented it “seriously” I propose you some points you might consider.
Performance
for (var i = 0; i < values.length; i++) {
// ...
}
With the above code (your outer loop), values.length
is evaluated for each step while looping, that may become significative when length
is high.
A well known best practice is to avoid it, using this instead:
for (var i = 0, n = values.length; i < n; i++) {
// ...
}
This way, values.length
is evaluated only once.
Simplification
Anyway, instead of the above code you can use this alternative construction, resulting in a simpler code:
values.forEach(function(valuesSubArray, i) {
// ...
});
NOTE: with this construction, you can also drop the previous declaration var valuesSubArray;
.
Again simplification, and probably also performance
outputText = "";
for (var j = 0; j < valuesSubArray.length; j++) {
outputText += valuesSubArray[j]
}
The above code (your inner loop) can be replaced by this simple statement:
outputText = valuesSubArray.join('');
NOTE: this results in 123
for a sub-array like [1, 2, 3]
, like does your outputText += valuesSubArray[j]
. If you rather want to get 1,2,3
as stated by the initial explanation in your OP, you must use outputText = valuesSubArray.join(',');
instead.
More simplification
At a higher level we can observe that the whole desired result outputDiv.innerHTML
structure is the same as the outputText
‘s one`:
- each element is the current
outputText
- the separator is
<br />
instead of a comma
So we can use join('<br />')
at this level, applying it to an array of the generated outputText
s. This array can be obtained using reduce()
:
values.reduce(
function(result, valuesSubArray) {
result.push(valuesSubArray.join(','));
return result;
},
[]
);
NOTE: we don’t need using i
any longer, nor previously declaring outputText
.
Finally
We can completely drop any variable declaration, and achieve the whole job with a “one-liner”:
function writeToOutput(values, outputName) {
if (!Array.isArray(values[0])) {
values = [values]
}
document.getElementById(outputName).innerHTML =
values.reduce(
function(result, valuesSubArray) {
result.push(valuesSubArray.join(','));
return result;
},
[]
)
.join('<br />');
}
writeToOutput([[1, 2, 3], [4, 5, 6]], 'outputs');
<div id="outputs"></div>
Since you are looking for performance, using a functional method like array.forEach
or array.reduce
is not advised. for
will always be the fastest way as these functions accept a callback to perform processing, and that adds to performance.
That said, you can still use functional approach in your code.
Suggestions:
- Try to create more generic functions. This will ensure you use your code more. It will also help you to create a function that only focuses on the business logic.
- Try to avoid
innerHTML
. It rewrites the HTML so any handlers attached will get lost. - Also, DOM manipulations are very expensive. You should try to keep them at minimum. So updating
innerHTML
in loop is a bad idea. Instead, append it to a local string and set it once and perform a bulk operation. - Try to avoid
<br/>
. Its not a good practice. Instead create a block element likediv
orp
or setdisplay: block
to the element. - Try to use inbuilt methods:
for (var j = 0; j < valuesSubArray.length; j++) {
outputText += valuesSubArray[j]
}
This loop is unnecessary. If all you want is to get a concatinated string, use functions like array.join(delimiter)
. By default, array.toString
will join the elements by comma.
Reference:
Use the inbuilt toString()
coercion.
Javascript is a loosely typed language. When you use two objects that are not the same type javascript will convert one of them to match the type of the other, if possible. This is called type coercion.
console.log(1 * "3"); // outputs 3 the string "3" is coerced to a Number
console.log("A number " + 3); //the 3 is coerced to a String
console.log("A array " + [1,2,3]); // the array coerced to a String "1,2,3"
When an object (an array is an object) needs to be converted to a String
Javascript calls its toString()
method. For the array the toString
method does the same as the Array.join(",")
function. Each item in the array is also converted to a string, if needed, and using its toString
.
So an array of arrays will be converted to a string by just assigning the array to a string. const str = ""; str += [[1,2,3],[4,5,6]; console.log(str);
outputs "1,2,3,4,5,6"
This means all the work your function needs to do is mostly done for you. You just need to make sure that you add the line break tag "<br>"
at the end of each array. For that you can use the Array.join
function directly.
Delimiting the output.
There remains one problem. If you call [1,2,3].join("<br>")
it will put a break between each item. So you need to still have the test.
Creating a second array is overkill as all you need is what delimiter to use. This can be done with a simple expression.
var delimiter = Array.isArray(array[0]) ? "<br>" : ",";
And then join the array using the delimiter
const str = array.join(delimiter);
Note: I assume that the test you use is accepted as the way to determine if the array contains arrays.
Test the input
You can not be sure that the id of the element is correct. What you do in that case is upto the design specs for the function. I will assume that your function is not meant to generate any type of error. So you need to check that the id gets an element.
As we are now calling an array method directly Array.join(",");
the function will throw an error is the passed argument does not contain an array. So you need to check that as well.
A rewrite.
function arrayToString(arr, outputId) {
const div = document.getElementById(outputId);
if (div !== null) {
if (Array.isArray(arr)) {
const delimiter = Array.isArray(arr[0]) ? "<br>" : ",";
div.innerHTML += arr.join(delimiter) + "<br>";
} else {
div.innerHTML += arr + "<br>";
}
}
}
arrayToString([1,2,3,4,5], "test");
arrayToString(["A","B","C","D","E"], "test");
arrayToString([["A","B"],["C","D","E"]], "test");
div { border : 1px solid black }
<div id="test"></div>
And just to clarify Javascript arrays.
Javascript only has 1D arrays.
In Javascript there are really only 1D arrays. We can simulate 2D to infinite D arrays by adding arrays to each item. But you can not guarantee that each item will be another array, or if it is an array, what and how many items it contains.
const a = [1,2,3,4];
const b = [[1,2,3],[4,5,6]];
const c = [[1,2,3],4,5,6];
a
is a flat array. b
is an array of arrays and is not a 2D array. c
is a mixed content array.
const d = [a,2,3,4];
a[0] = d;
d
is an array holding the array a
and by adding to a[0]
the array d
I have created an cyclic reference. If you were to use the dimensional description it would be a infinite dimensioned array, which is clearly impossible (no memory store is big enough) but still d[[[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]]] === 2
works. I have never tested how far you can nest the [] but there is a limit somewhere.