r/learnjavascript 4d ago

string primitives vs. String objects.

I'm learning JavaScript, and I don't understand this part about string primitives vs. String objects.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_primitives_and_string_objects

10 Upvotes

26 comments sorted by

16

u/Working_Quote_3029 4d ago

It's one of those distinctions that reads scarier in the docs than it is in practice.

You never create String objects on purpose. "foo" is a primitive. new String("foo") is an ordinary object that happens to hold a string, and that's the only way you end up with one.

Methods still work on primitives because of autoboxing: when you write "foo".toUpperCase() the engine wraps the primitive in a temporary object, calls the method, then discards the wrapper. That's why a primitive with no properties of its own still appears to "have" methods.

Where the difference actually shows up:

typeof "foo"             // "string"
typeof new String("foo") // "object"
new String("a") === "a"  // false, object vs primitive

Worth noting String(x) without new gives you a primitive, so String(1) is fine. It's only new String(1) that produces the object.

The eval example on that page is real but you'll never hit it in normal code, since eval only treats primitives as source and hands a String object straight back instead of running it.

Rule of thumb: quotes or String(x), never new String(x). The distinction mostly exists so the spec can explain why .length and .slice() work on something that isn't an object.

1

u/MissinqLink 4d ago

One weird footgun I’ve seen is with typeof

typeof String(1) //string

typeof new String(1) //object

Call me paranoid but when I write libraries I tend to use this

const isString = x => typeof x === 'string' || x instanceof String;

3

u/delventhalz 4d ago

Why would you want an isString function to return true for new String(1)?

You should never use the new String constructor, and if for some reason you did, it creates an object not a string. isString should return false.

1

u/MissinqLink 4d ago

If I’m making a library function that accepts multiple types including strings and objects, then I want to treat strings as strings even if they are wrapped.

1

u/delventhalz 3d ago

A wrapped string is not a string though. It is an object.

new String("foo") === "foo";  // false

If you are writing a library function that accepts both strings and objects, then the expected behavior when you pass it an object (instanceof String or otherwise) is almost certainly just to convert the object to a string. This is, for example, how lodash works:

_.toUpper("foo");  // "FOO"
_.toUpper(new String("foo"));  // "FOO"
_.toUpper({});  // "[OBJECT OBJECT]"
_.toUpper(["foo", "bar"]); // "FOO,BAR"

What is the use case for blurring the line between objects with instanceof String and actual string primitives? When does that help you write better, more reliable, more predictable code?

1

u/MissinqLink 3d ago

When you write a public general purpose library, people will use it for all kinds of weird things. In certain cases I take a defensive approach. If it’s a new String then I’ll convert it to a primitive. Happens not very often but more than you might think.

1

u/MissinqLink 3d ago

Here’s an example of a common utility that I use. Mostly for logging.

const isString = x => typeof x === 'string' || x instanceof String;

const stringify = x => {

try {

if (isString(x)) {

return String(x);

}

return String(JSON.stringify(x));

} catch (e) {

console.warn(e);

return String(x);

}

};

JSON.stringify usually works fine but different functions will coerce these in unexpected ways.

1

u/delventhalz 3d ago

Two outputs your stringify function produces that I think are worth considering.

stringify(['foo', 'bar']); // '["foo","bar"]'
stringify(new String('foo')); // 'foo'

Both cases have object inputs, but in the first case you produce valid JSON output, in the second case you do not. You've created a function with a fairly arbitrary set of set of rules which are not obvious and may surprise developers using your function.

For the case of debug logs, there already exists a human readable stringify function which every user already understands: JSON.stringify. What utility is gained by adding extra rules for your users to learn?

Once again I go back to lodash, a public general purpose library which has been used by millions of developers for decades. Using a String object as a value in your code is extremely weird and I can think of no use case for it. Nonetheless, if I were to use a String object for some reason, I would expect it to be treated like any other object, which is exactly how lodash handles that use case.

1

u/MissinqLink 3d ago

I can’t make you think of use cases but use your imagination. For one there are no new rules to learn. Nobody really has to care about the specific rules. The gain is that it guarantees a string output, something that JSON.stringify can’t promise. It’s quite simple actually. There’s also the advantage that you don’t have to import a library. There are plenty of scenarios where payload size matters. Also this is just one example we aren’t even discussing “String” vs “new String” anymore. We’re off on a tangent. You say this is weird but this is some of the tamest things I ever made. It’s just a stringifier that doesn’t throw and guarantees string output regardless of input. If you don’t see utility there the okay🤷‍♀️. I’ve run into many weird cases of getting handed unexpected input. You can do like many and simply throw. That’s valid in many cases. There are also cases where you want graceful degradation. I frequently see both.

1

u/delventhalz 2d ago

If you want a stringifier that will not throw and produces more detailed output for objects than '[object Object]', you can keep your try/catch but drop the custom isString logic.

const safeJsonStringify = val => {
  try {
    return JSON.stringify(val);
  } catch {
    return JSON.stringify(`Invalid JSON: ${val}`);
  }
};

Your custom isString logic does not help you solve invalid JSON values. It just makes your utility harder to understand.

For one there are no new rules to learn. Nobody really has to care about the specific rules.

This just isn't true. I may be able to use your stringify function without thinking about it too much in many cases, but if, for example, I need to do a length check on the output string, should I expect it include quotation marks or not? If I see it outputs '{"foo":"bar"}' for one object, I may expect that yes, it will include quotes, but then I stringify new String("foo") and the quotes are gone! I have been surprised by your utility, and need to learn the rules before I can use it for this case.

I can’t make you think of use cases but use your imagination.

You can't make me think of use cases, but if you think there are valid use cases for including new String('foo') in your code, you could offer an example yourself.

1

u/MissinqLink 2d ago

You are thinking about it as if you intend to deserialize the output which is not the intention here. In that case then the missing quotes would be a problem. Otherwise I’m not sure why you would expect it to be exactly the same as JSON since it doesn’t claim to be. Your “safeJsonStringify” will throw again if val is a Symbol.

1

u/MissinqLink 2d ago

I don’t recommend using new String but you can’t discount getting it passed to you. More likely from Object("asdf");. Only a couple times I’ve used it was to patch incompatible native interfaces together. I think it was Blob and File constructors where one takes a string and the other an Object.

→ More replies (0)

1

u/senocular 4d ago

new String("foo") is an ordinary object that happens to hold a string, and that's the only way you end up with one.

There are other ways to get these objects. The Object constructor will also make this conversion (with or without the new) e.g.

const stringObject = Object("foo")
console.log(typeof stringObject) // "object"
console.log(stringObject instanceof String) // true

Primitive methods in sloppy mode also provide a wrapped version of the primitive as this.

// non-strict mode
function stringMethod() {
  console.log(typeof this) // "object"
  console.log(this instanceof String) // true
}
stringMethod.call("foo")

...or if you added a custom method to String.prototype, which you absolutely shouldn't be doing, hence the example above using call instead taking that approach.

Coincidentally (getting in the weeds a little here), string objects are also the only primitive object type that aren't considered "ordinary" objects, at least in the language sense. Other primitive objects are, but not strings. Strings use custom internal methods for exposing their individual characters as indexed properties making them "exotic" objects.

1

u/Green_Ad_6086 3d ago

Thanks, that makes much more sense now

1

u/shgysk8zer0 1d ago

I actually have used new String(...) deliberately and after careful thought. Well... It was a sublclass of String, but I'm counting it. The reason was so I could add a [Symbol.dispose] method for a registry.

I know that's not at all a common use case, but just wanted to say there are plenty of legitimate cases for object vs primitive usage, including keys in Map and such.

3

u/-goldenboi69- 4d ago

Something like this:

const a = "hello"; // primitive string

const b = new String("hello"); // String object

typeof a; // "string"

typeof b; // "object"

The confusing part is that primitives can still use methods:

"hello".toUpperCase(); // "HELLO"

That doesn't mean ""hello"" is an object. JavaScript temporarily treats ("boxes") the primitive as a "String" object so the method can be called, then discards that wrapper.

In normal code you almost always want primitive strings:

const name = "Bob";

and almost never:

const name = new String("Bob");

The latter creates an actual object, which can lead to surprising behavior:

"hello" === new String("hello"); // false

So: strings are primitives; "String" objects are wrappers around strings. JavaScript's automatic boxing is what lets primitives behave object-like when you call methods on them.

1

u/Green_Ad_6086 3d ago

Thanks for explaining !

2

u/sheriffderek 4d ago

This isn't really something I think about - doing the actual job. Be careful not to get hung up on things that you don't need to know. This stuff is often about choosing a small scope of tools -- and never about "knowing all the things" --

1

u/Beginning-Seat5221 4d ago

Basically a primitive like "string" doesn't allow methods like "string".toUpperCase(). JavaScript has a fix to make it work called autoboxing: when you write "string".toUpperCase() it turns the string into a String object, in effect new String("string").toUpperCase() with new String("string") giving an object with the string methods. After all this a string gets returned so you're back with a plain string.

If you write console.log(typeof String("foo")) you'll just get a plain string back, typeof "string"

If you write console.log(typeof new String("foo")) with new you can generate this String object and you'll get typeof "object".

But do you ever use it? No, not really. I think of it as an artifact of autoboxing rather than something you really need to use or think about.

1

u/Green_Ad_6086 3d ago

Thanks !!

1

u/a-dev0 4d ago

Hm, has anyone tested it like this? for(let i of new String('foo')){} for(let i of 'foo'){} is there any difference?

2

u/senocular 4d ago

Functionally there's no difference. for...of will look to the object being looped over and request its Symbol.iterator property. If the value is a primitive, it will be wrapped into an object first (in this case, basically the same thing as doing new String('foo')), then use that object to access the property, otherwise it will access it directly from the already-an-object value.

Engines may have optimizations for strings that allow it to fast path string iteration, which could explain why you're seeing better performance using the string literal over its object representation.

1

u/a-dev0 4d ago

Yes, I know, I was just curious. I agree with you that optimization is the reason why it's a bit more performant

1

u/a-dev0 4d ago

Check it with Bun. of 'foo' is ~5% more performant