Is there a better way to implement this string builder?
I really have difficulty understanding your explanation or your code, but the wanted output is clear. So going only by that I propose this function:

function duplicates(testArray, id = 1)
{
    return testArray.filter(testId =&gt; testId !== id);
}


the filter() method creates a new array with all elements that pass the test implemented by the provided function. In this case I test that the array items are unequal to the given id. The output of your tests is:


  This item is the same as item 2
  This item is the same as item 2,3
  This item is the same as item 2,3,4


So, there's not the fancy 'and' in there yet, but that can be added. Is it really needed? Anyway, adding a bit of extra code like this:

function formatArray(idArray)
{
    var ending = (idArray.length &gt; 1) ? ' and ' + idArray.pop() : '';
    return idArray.join(', ') + ending;
}

function duplicates(testArray, id = 1)
{
    return formatArray(testArray.filter(test =&gt; test !== id));
}


where we join the array to a string separated by commas, except for the ending if there are multiple items, when we insert an 'and'. The thing that might not be obvious is the conditional (ternary) operator. If you don't like it you can replace it with a normal if (...) {...} else {...} statement. This returns:


  This item is the same as item 2
  This item is the same as item 2 and 3
  This item is the same as item 2, 3 and 4


Now this is "Code Review" so I should review your code. That is difficult. I can almost see what you were trying to do, but it is such a mess (sorry). It seems you started with the for loop and then just created this Frankenstein monster by adding bits. Inside the loop you have four ifs, two elses and even a return. Some of the actions are the same, like adding a space. I don't believe even you know exactly what's going on there. It might work, but that's it. This is not how you should be programming.

Some advice:

Try to analyse the problem before you start writing any code. There are two clear distinct jobs to do: 


Get rid of the duplicates.
Generate the output.


Keep these two jobs apart. That makes each problem easier to solve and leads to easier to understand code, because the jobs themselves are easier. Ideally these two jobs should be done in two seperate functions. A function should only do one thing, and not two.

Now I have to agree that, what I did, simply requires a lot of experience. You need to know the language you're using. That will come with time and practice.

Another thing you need to pay attention to is the names you use. One of the major reason I cannot understand your code is that it is unclear what it all means. What is in duplicatesArray? And why is the function called duplicates? That just doesn't make much sense. Something like removeDuplicates would be better as a function name, because it implies a clear action.

In my personal opinion each function should always, and only, have one return in it. I'll admit that I don't always stick to this rule, but as a general idea it is sound. Having multiple returns makes a function harder to understand, and having no return just seems lazy. There's always something to return.