75

I inherited some JavaScript code another developer wrote. He didn't like the grid component we used throughout the project, so he decided to write his own. The grid he wrote can't sort dates, because it can only bind to strings / numbers. He converts all dates to strings before using them. I looked at the string formatting of date function he wrote, and figured I could just add a date property to the string with the original value, and then when sorting see if the string has a date property and sort based on that. However, it seems like you can't add properties to strings in JavaScript. I wasn't aware there were certain types you can't add properties to. For example:

var test = "test";
test.test = "test inner";
console.log(test);
console.log(test.test);

test.test will be undefined. Weird.

My question is why this code doesn't work? And also, if you can think of any workarounds for sorting dates on that grid (besides actually binding to date objects instead of strings, which would be a pain to fix,) that would be really helpful.

VLAZ
  • 26,331
  • 9
  • 49
  • 67
Shawn
  • 19,465
  • 20
  • 98
  • 152

3 Answers3

99

There are 8 language types in JavaScript:

  • 7 primitive types: Undefined, Null, Boolean, Number, BigInt, String, and Symbol
  • 1 non-primitive type: Object

Values of the primitive types are called primitive values and they cannot have properties.
Values of the Object non-primitive type are called objects an they can have properties.

When you try to assign a property named 'bar' to a variable foo, like so:

foo.bar = 'abc';

then the result will depend on the type of the value of foo:

(a) if the value of foo is of the type Undefined or Null, then an error will be thrown,

(b) if the value of foo is of the type Object, then a named property 'bar' will be defined on the object foo (if necessary), and its value will be set to 'abc',

(c) if the value of foo is of any other type, then a TypeError will be thrown in strict mode: “can't assign to property "bar" on foo: not an object”. In loose mode, the above assignment operation will be a no op. In either case, the variable foo will not be changed in any way.

So, as you can see, assigning properties to variables only makes sense if those variables are objects. If that is not the case, then the assignment will either do nothing at all, or even throw an error.


In your case, the variable test contains a value of the type String, so this:

test.test = "test inner";

does nothing at all.


However, since ES5 introduced accessor properties, there is an exception to what I've said above. Accessor properties allow us to define functions which are invoked whenever the property is either retrieved or set.

For instance:

var str = '';
str.prop;

Here str is a variable holding a String value. Therefore, accessing a property of that variable should be a no-op (str.prop merely returns undefined). This is true with one exception: if String.prototype contains a accessor property 'prop' with a defined getter, then that getter will be invoked.

So, if this is defined:

Object.defineProperty( String.prototype, 'prop', {
    get: function () {
        // this function is the getter
    }
}); 

then this

str.prop;

will invoke that getter function. This also works in strict mode.

Live demo: http://jsfiddle.net/fmNgu/

However, I don't think that adding accessor properties to the built-in prototypes would be a good practice.

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
Šime Vidas
  • 182,163
  • 62
  • 281
  • 385
  • 16
    'you cannot assign properties to the values' is a bit misleading: because of autoboxing (more specifically, the algorithm described in section 8.7.2 of ECMA-262, 5th edition), it's perfectly valid to assign properties to primitives; however, the properties will be added to the purely temporary wrapper-object and not the primitive, so there's no way to get at the property (the wrapper-object does not replace the primitive); thus, assigning a property to a primitive is a noop, except when the assignment has side-effects (eg if the property is implemented via accessor functions) – Christoph Mar 05 '11 at 13:14
  • @Christoph Yes, you're right. I've updated my answer. Could you please fact-check it? – Šime Vidas Mar 05 '11 at 18:10
  • 2
    @Šime: your answer accurately describes the behaviour of ECMAScript3: according to section 11.2.1, property access invokes `ToObject()`, which will throw for undefined and null, create wrapper objects for values and returns the argument for objects; in ES5, the situation is slightly more complex, as you could add a setter function to the prototype object of a primitive, ie setting properties on primitives can have side-effects... – Christoph Mar 05 '11 at 21:50
  • It might be worth pointing out that as Christoph says toObject is called and thus one can call "String" methods on "string primitives". I think that's where alot of the confusion starts. –  Aug 09 '13 at 03:19
  • @nus What matters is that, in JavaScript, it is not an error to invoke methods on primitive *String* values. Instead, it is a no-op as I've stated in **(c)**. The internals, i.e. the internal `ToObject` call, is not important to understand this behavior. Let's keep things simple for the regular users. – Šime Vidas Aug 09 '13 at 20:35
  • You say: "Values of the primitive types are called primitive values and they cannot have properties." but this is not exactly true. How else could I do str.length? length is obviously a built-in property. – Mike May 19 '15 at 23:32
  • 2
    @Mike When properties are accessed (or then methods are invoked) on primitive String values, the JavaScript engine wraps the value in a `String` (wrapper) object, specifically it performs `new String(str)`. The `length` property is then retrieved from that object, after which the object is discarded. – Šime Vidas May 20 '15 at 03:09
36

If you use a String object you can add properties:

var test = new String("test");
test.test = "test inner";
console.log(test.toString()); // prints out "test"
console.log(test.test); // prints out "test inner"
treaint
  • 699
  • 1
  • 8
  • 13
  • 14
    It does have one drawback. The following returns false: `test === 'test'`. Here is [why it does that](http://stackoverflow.com/questions/10951906/why-does-foo-new-stringfoo-evaluate-to-false-in-javascript). – dgo.a Sep 14 '12 at 14:56
  • 2
    To add to dgo.a's comment, the "typeof" will also be "object" rather than "string". – iPzard Jun 10 '19 at 15:36
1

The reason it doesn't work is because string is a primitive. It looks like an object (which you can add random properties to) because you can call methods on it:

test.includes(searchString)

However, what's happening is that JavaScript autoboxes the primitive string and effectively calls

new String(test).includes(searchString)

So when you call

test.test = 5;

That is the same as

new String(test).test = 5

Which means you're actually adding a property to the autoboxed value which is thrown away. As noted by Šime Vidas, this is not allowed in strict mode.

If you really had to do this, you could always create the object yourself but it's usually avoided because it behaves differently from primitive strings.

const testObj = new String("test");
const testOb2 = new String("test");
const testPrimitive = "test";
const testPrimitive2 = "test";

console.log(testPrimitive == testPrimitive2); // true
console.log(testPrimitive === testPrimitive2); // true
console.log(typeof testPrimitive); // string

console.log(testObj == testPrimitive); // true
console.log(testObj === testPrimitive); // false
console.log(testObj == testOb2); // false
console.log(testObj === testOb2); // false
console.log(typeof testObj); // object, not string
Ruan Mendes
  • 90,375
  • 31
  • 153
  • 217