'this' is not always what it seems to be

it's an article on a <body/>building blog

written by Bartek Szopka on 9th October 2009

Recently, while developing some simple stuff with jQuery, I came across strange problem with strings in JavaScript. After hours of debugging and investigating, it turned out to be a result of confusion between primitive strings and String objects in JavaScript. If you don’t want to repeat my mistakes and would like to understand a little bit more about strings in JavaScript, this post is for you.

I once needed to truncate some text in JavaScript before showing it on a page. I didn’t want to reinvent the wheel, so I took a truncate method from prototype.js and added it to String’s prototype.

So the simple use case may be to truncate given text to 20 characters and put it into a <div> element on a page, just like this:

jQuery("div").html( text.truncate(20) );

I was very surprised to find that it worked well when text had more than 20 characters (so it was truncated), but didn’t when it was shorter and didn’t need any truncating. It was just throwing some ugly exception from the deepest core of jQuery, but, as the problem was depending on the results of truncate method, I looked into it to find the reason.

That’s how the truncate’s code looks like:

String.prototype.truncate = function(length, truncation) {
  length = length || 30;
  truncation = truncation === undefined ? '...' : truncation;
  return this.length > length ?
    this.slice(0, length - truncation.length) + truncation : this;
};

The conditional behaviour that interests us is in the return statement: if the string’s length is greater than required then we return truncated string, otherwise unchanged value of the string is returned. And the problem shows up when we return unchanged string — the value of this. So what’s wrong with this?

I used Firebug to investigate it, and it gave me another clue. The trucnated and not truncated strings where displayed by in a different ways in Firebug’s console:

>>> "testing".truncate(6)
"tes..."
>>> "testing".truncate(7)
testing 0=t 1=e 2=s 3=t 4=i 5=n 6=g

So even Firebug sees the difference and treats the second case like an object, not a primitive string. Let’s check the type of a value returned by truncate method:

>>> typeof "testing".truncate(6)
"string"
>>> typeof "testing".truncate(7)
"object"

Gotcha! So when we truncate a string, we get a string, but when we return this we get an object… But it’s still a string, isn’t it?

>>> "testing".truncate(6).constructor
String()
>>> "testing".truncate(7).constructor
String()

They are both strings, but one is primitive string, and other one is a String object. The same difference we can see when using String constructor:

>>> typeof "primitive string"
"string"
>>> typeof new String("String object")
"object"

So we have 2 issues here. One is that when we return this from String’s prototype we get a String object rather than primitive string. Other problem is that jQuery does’t work well with String objects, but it’s not a big deal I guess, as using String objects in JavaScript is a very rare case.

The real problem lays in the truncate method. It’s results should be consistent, so it should always return primitive string (as all standard String methods do). The easiest way to turn a String object into a primitive string is to concatenate it with some other string, or to run it through global String method.

>>> new String("test")
test 0=t 1=e 2=s 3=t
>>> String(new String("test"))
"test"
>>> new String("test") + ""
"test"

So our fixed truncate method may look like this:

String.prototype.truncate = function(length, truncation) {
  length = length || 30;
  truncation = truncation === undefined ? '...' : truncation;
  return this.length > length ?
    this.slice(0, length - truncation.length) + truncation : String(this);
};

So what is the conclusion from this story?

If you want to extend prototype of some object (especially in case of String, Boolean, Number…) you should really know what you are doing and if you want to return the value of this directly, always check if it has required type, as this is not always what it seems to be.

Discuss

Did you like it? Do you have anything to add? Join the discussion and tell us about it, or just check what others have to say.

Subscribe to the articles feed