-1

Why/when would one use .concat() in place of assignment operators?

i.e. if I am trying to combine the following:

var p1 = "My name is ";
var p2 = "Joe";
var sen = p1+p2;
//Or you could use concat to do the same
var sen2 = p1.concat(p2);
//My question is, why would you ever use the latter?
Michael Sacks
  • 917
  • 1
  • 11
  • 17

2 Answers2

4

Sometimes its best to consult the documentation: Array.concat, and String.concat.

Simply, Array.concat() is used to create a new array equivalent to the flat merging of all passed in objects (arrays or otherwise). String.concat() is used to create a new string, which is equivalent to the merging of all passed in strings.

However, as MDN hints at, String.concat() should not be used as the assignment +, += operators are much faster. Why then would you use String.concat()? You wouldn't. Why have it then? It's part of the spec: See Page 111 - 112 (Section: 15.5.4.6).

So on to the question of Why is String.Concat so slow?. I did some digging through Chrome's V8 Engine. To start with, behind the scenes, this is what a call to String.prototype.concat is doing:

// ECMA-262, section 15.5.4.6
// https://github.com/v8/v8/blob/master/src/string.js#L64
function StringConcat(other /* and more */) {  // length == 1
  CHECK_OBJECT_COERCIBLE(this, "String.prototype.concat");

  var len = %_ArgumentsLength();
  var this_as_string = TO_STRING_INLINE(this);
  if (len === 1) {
    return this_as_string + other;
  }
  var parts = new InternalArray(len + 1);
  parts[0] = this_as_string;
  for (var i = 0; i < len; i++) {
    var part = %_Arguments(i);
    parts[i + 1] = TO_STRING_INLINE(part);
  }
  return %StringBuilderConcat(parts, len + 1, "");
}

As you can see all of the real work happens in StringBuilderConcat, which then calls a StringBuilderConcatHelper which then finally calls String::WriteToFlat to build a string. These are each extremely long functions and I've cut most of it out for brevity. But if you'd like to look for your self have a look in github:

StringBuilderConcat

// https://github.com/v8/v8/blob/master/src/runtime.cc#L7163
RUNTIME_FUNCTION(Runtime_StringBuilderConcat) {
  // ...
  StringBuilderConcatHelper(*special,
                              answer->GetChars(),
                              FixedArray::cast(array->elements()),
                              array_length);
  // ...
}

StringBuilderConcatHelper

template <typename sinkchar>
static inline void StringBuilderConcatHelper(String* special,
                                             sinkchar* sink,
                                             FixedArray* fixed_array,
                                             int array_length) {

  // ...
  String::WriteToFlat(string, sink + position, 0, element_length);
  // ...
}

String::WriteToFlat

// https://github.com/v8/v8/blob/master/src/objects.cc#L8373
template <typename sinkchar>
void String::WriteToFlat(String* src,
                         sinkchar* sink,
                         int f,
                         int t) {
  String* source = src;
  int from = f;
  int to = t;
  while (true) {
      // ...
      // Do a whole bunch of work to flatten the string
      // ...
    }
  }
}

Now what's different about the assignment pathway? Lets start with the JavaScript addition function:

// ECMA-262, section 11.6.1, page 50.
// https://github.com/v8/v8/blob/master/src/runtime.js#L146
function ADD(x) {
  // Fast case: Check for number operands and do the addition.
  if (IS_NUMBER(this) && IS_NUMBER(x)) return %NumberAdd(this, x);
  if (IS_STRING(this) && IS_STRING(x)) return %_StringAdd(this, x);

  // Default implementation.
  var a = %ToPrimitive(this, NO_HINT);
  var b = %ToPrimitive(x, NO_HINT);

  if (IS_STRING(a)) {
    return %_StringAdd(a, %ToString(b));
  } else if (IS_STRING(b)) {
    return %_StringAdd(%NonStringToString(a), b);
  } else {
    return %NumberAdd(%ToNumber(a), %ToNumber(b));
  }
}

First thing to note, there's no loops and its quite a bit shorter compared to StringConcat up above. But most of the work we're interested in happens in the %_StringAdd function:

// https://github.com/v8/v8/blob/master/src/runtime.cc#L7056
RUNTIME_FUNCTION(Runtime_StringAdd) {
  HandleScope scope(isolate);
  DCHECK(args.length() == 2);
  CONVERT_ARG_HANDLE_CHECKED(String, str1, 0);
  CONVERT_ARG_HANDLE_CHECKED(String, str2, 1);
  isolate->counters()->string_add_runtime()->Increment();
  Handle<String> result;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, result, isolate->factory()->NewConsString(str1, str2));
  return *result;
}

This is pretty simple actually, some counters and a call to something called NewConsString with the left and right operands. NewConsString is also pretty simple:

// https://github.com/v8/v8/blob/master/src/ast-value-factory.cc#L260
const AstConsString* AstValueFactory::NewConsString(
    const AstString* left, const AstString* right) {
  // This Vector will be valid as long as the Collector is alive (meaning that
  // the AstRawString will not be moved).
  AstConsString* new_string = new (zone_) AstConsString(left, right);
  strings_.Add(new_string);
  if (isolate_) {
    new_string->Internalize(isolate_);
  }
  return new_string;
}

So this just returns a new AstConsString, what's that:

// https://github.com/v8/v8/blob/master/src/ast-value-factory.h#L117
class AstConsString : public AstString {
 public:
  AstConsString(const AstString* left, const AstString* right)
      : left_(left),
        right_(right) {}

  virtual int length() const OVERRIDE {
    return left_->length() + right_->length();
  }

  virtual void Internalize(Isolate* isolate) OVERRIDE;

 private:
  friend class AstValueFactory;

  const AstString* left_;
  const AstString* right_;
};

Well this doesn't look like a string at all. Its actually an 'Abstract Syntax Tree', this structure forms a 'Rope' which is efficient for modifying strings. It turns out most of the other browsers now use this type or rope structure when doing string addition.

The take away from this, is that the addition pathway uses a more efficient data structure, where as StringConcat does significantly more work with a different data structure.

klyd
  • 3,939
  • 3
  • 24
  • 34
  • String.concat() and Array.concat() are two different methods. – James G. Sep 03 '14 at 02:40
  • Actually, to be precise, it doesn't merge arrays. It merges a list of objects onto an array, and if the object happens to be an array, then it merges its elements. –  Sep 03 '14 at 02:45
  • 1
    Excellent analysis. I'm thoroughly illuminated. But, although I'm as interested in performance as the next guy, this performance difference (around a factor of 2) is perilously close to falling into the "who cares" category. I'd say the main reason for preferring `+` is that it's shorter. –  Sep 03 '14 at 16:28
  • Thank you! I'd vote your answer up, but I don't have enough rep. – Michael Sacks Sep 03 '14 at 17:01
-1

According to Javascript: The Good Parts by Douglas Crockford:

The concat method makes a new string by concatenating other strings together. It is rarely used because the + operator is more convenient

Concat is not only less convenient, it is also slower: Benchmark

On the documentation page from MDN:

It is strongly recommended that assignment operators (+, +=) are used instead of the concat method.

Javascript has some less than ideal parts. Every language has at least some bad parts. Don't think you have to use every part of any language.

James G.
  • 2,852
  • 3
  • 28
  • 52
  • 1
    I'm confused. Saying "`+` is more convenient" is wrong because `concat` is slower? Convenience is different from performance (although in this case, both convenience *and* performance point to using `+`). –  Sep 03 '14 at 16:31
  • No, I was just saying it's wrong because that's not the sole reason for it not being used. I'll update that to be more clear. – James G. Sep 03 '14 at 17:58