0

How do I efficiently make changes to a string in Javascript without creating a new string every time I make a change? The attached code illustrates this as each "result +=" operation will generate a new string.

The idea of using a Unit8Array as a work area appears to fail because converting to a string is just as inefficient.

; Use mask to change characters in string to a
; '-' if mask dictates that. 
function maskOutput(input, mask) {
 let result = '';
 for (let i=0; i < input.length; i++){
    if (mask.charAt(i) === 'X') {
      result += '-';
    } else {
       result += input.charAt(i);
    }
 }
 return result;
}
6
  • 5
    You can't, strings are immutable. Commented Jan 19, 2022 at 16:54
  • 1
    Actually, repeated += is well-optimised in engines. Is this really a problem in your application, did you benchmark your current solution? Commented Jan 19, 2022 at 16:59
  • 1
    Maybe you should create the result using String.replace and a RegExp object, the RegExp would do the dirty work for you, and you would have created a single string only. Commented Jan 19, 2022 at 17:00
  • 1
    "converting to a string is just as inefficient." - what makes you say that? How did you write it? Btw I'd recommend a Uint16Array if you want to go for that, it matches the JS string representation more closely. Commented Jan 19, 2022 at 17:01
  • @Bergi It's good news that the engines are well optimized for this case. I was hoping to find an array implementation that provided a useful toString() method but the ones tried produced unusable output like "-,-,-,0,0,0,0". Commented Jan 20, 2022 at 17:54

1 Answer 1

3

The first thing I'd do is check whether the temporary strings were actually causing a problem for my page/app, because JavaScript engines are extremely good at optimizing common issues like lots of temporary strings, especially in cases like your maskOutput where the string is the result of string concatenation. (V8, the engine in Chrome and elsewhere, does it when necessary by combining partial strings into a linked list.) It seems unlikely that the string concatenation in your maskOutput function is a bottleneck for your page/app. It used to be fairly standard practice that you'd see code building up a large string by using an array and then .join("") at the end, because it was faster circa 2009, but JavaScript engines have moved on a lot since then and I remember it became faster to just use += on a string several years ago.

If there were some use case (memory pressure, whatever) where the temporary strings were a problem, you could use a plain array¹ of individual characters, converting once (from string to array) at the outset if needed (not in your case) and at the end (from array to string). But for a short string like the ones I imagine maskOutput handles, as you say, the overhead wouldn't be worth it.

But just for completeness:

function maskOutput(input, mask) {
    const len = input.length;
    let result = Array(len); // Some engines actually do optimize this
    for (let i = 0; i < len; i++) {
        if (mask[i] === 'X') { // No need for a method call
            result[i] = "-";
        } else {
            result[i] = input[i];
        }
    }
    return result.join("");
}

¹ Uint8Array would be a poor choice, because:

  • Strings are made up of 16-bit values (UTF-16 code units). If you want to use a typed array, Uint16Array would be more appropriate.
  • Putting a value into a Uint8Array/Uint16Array requires converting it from JavaScript's number type to an 8-bit/16-bit unsigned integer (and the converse on reading), which is really cheap but isn't free. And presumably you'd have to use charCodeAt to get the value to store.
Sign up to request clarification or add additional context in comments.

8 Comments

I just saw a SO post (maybe a week ago) where it was stated, that modern browsers have optimized string concatenations so that they're faster than building an array and then joining it to a string. I couldn't find the post, but there were some benchmarkings as evidence.
@Teemu - Yeah. I think that point was cleared several years ago in fact, I remember it happening ("it" being the point where most engines were faster with += than adding to an array and joining it). So it would need to be in response to an issue of memory pressure or some such, and I just can't see it. :-)
@Domino - Yeah, I was surprised by that too, but Mathias Bynens of the V8 team set me straight on it, and confirmed just recently that this article is still up-to-date. Which is why I use Array.from({length: 10_000_000}, () => { /*...something filling it in*/ }) instead of Array.from(Array(10_000_000), () => { /*...something filling it in*/ }) in the extremely rare case where I'm working with blocks that big (well, I do it by habit even with smaller ones now, but you understand).
@Domino - Yes, but we're not going to use the array so it's not important. The key bit in regard my example is that it creates backing storage for the array: twitter.com/mathias/status/1392132666544246784. So if I'm not going to use it (as in my example), I want to avoid that, so I use {length: x} instead. :-)
Thanks to everyone for the answers and information. I feel thoroughly answered and also have some homework to do with the references you guys provided. Thanks!!
|

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.