Usefulness of primitive constructors in JS

Viewed 189

Note: I am not looking for opinions. A single, objective useful example of a primitive constructor would satisfy the question.


Are there any good reasons to use new Number(n)? Why does this constructor exist?

I can think of several reasons to avoid it, eg.,

  • literals are more memory efficient since they don't construct anything new.
  • literals are equal to each other (5 === 5 but new Number(5) !== new Number(5)).
  • the typeof operator indicates that it's a number/str/etc rather than an object.
  • new Number(0) is never falsey. (!!new Number(false) == true).
  • You can use it for type conversion, but there are more concise methods. (+false === 0).

To be clear, I understand the Number object (or String or Object or Array objects) has useful methods and properties, my question is specifically about the constructor.

I am also aware that the constructors are called implicitly when using the methods of these primitive types (eg. 123.45.toFixed(n)), but that doesn't explain why the constructors are exposed to the browser, or how (or if) the constructors themselves are useful.

I'm using Number as an example, but I don't understand why Javascript exposes other constructors where literals are available either (String, Object, Array, etc.).

Why do these exist and are there ever any good reason to use them?

3 Answers

In the end, such a question is answered by "because it is in the specification", but still there is some intuitive logic to have a Number constructor:

As you already mentioned in the question, there is a (useful) prototype object for numbers, so that you can call methods like .toFixed et al. Every JavaScript object with a non-null prototype has a constructor. Constructor and prototype go hand in hand; unless code overwrites the involved properties, there is this relationship for every object that has a non-null prototype:

instance.constructor.prototype === Object.getPrototypeOf(instance)

As this is like a general rule, it makes sense to not deviate from that for (object-wrapped) numbers, and have a meaningful value for that constructor property, i.e. a ... constructor. And by definition of "constructor", it can be called with new.

Useful?

Here we really get into the grey zone of opinion.

Just one observation: Number instances can have custom properties, but even for that purpose you can rely on the automatic wrapping:

let n = Object.assign(2, { isPrime: true });
console.log(n instanceof Number); // true
console.log(n.isPrime); // true
console.log(n*2); // 4

These constructors create a "Wrapper object" around the primitive type, hence the returned object is fundamentally different than the primitive itself.

var example = new String('abc')

would return a wrapper object that contains the value "abs" and to access it you need to call the function .valueOf()

example.valueOf() // returns abc 

The primitives also have access to the prototype of the wrapper, but still they're fundamentally different. Primitives merely "borrow" the methods on it's prototype from it's wrapper.

The reason of the existence of the primitive's wrapper is merely historical as this behavior exists in Java which Javascript inherited the difference between objects and primitive from.

The only reason I can think of to use it is to add a property on a primitive like the following:

if you do this:

var example = "Foo" ; 
example.bar = "Baz"; // The property "bar" only exist on this line and does not persist; 

Using the wrapper :

var example = new String("foo") ; 
example.bar = "baz" ; // Now you can access the property "baz"
example.charAt(1); //You still can access the methods on the string prototype  

It's very awkward syntax but it works.

Thanks for the great question and I'm looking forward to see all the answers and different discussions to try to understand the existence behind some of the very weird behaviors in Javascript.

Are there any good reasons to use new Number(n)?

I can think of one reason but it might not be valid today. It's a stretch, but I believe it could be used for performance reasons depending on the implementation.

The spec says this about a case where you'd do something like:

1.2.toString()

The object that may be created in step 4.a is not accessible outside of the above abstract operation and the ordinary object [[Get]] internal method. An implementation might choose to avoid the actual creation of the object.

So technically, someone that's doing something like that a lot could benefit a little bit if they preemptively created the number object and called the method there. That's if the implementation does not avoid the object wrapper.

However, given current implementations today you would probably have the opposite results and get worse performance.

Anyway, it's a big stretch.

Why does this constructor exist?

Well, a couple of reasons I can think of, mostly because of consistency. First of all, String, Number and Boolean each double down as constructors and as regular functions. They each can convert their input into their respective types when called without new. There were actually very few functions back then that weren't constructors (such as eval or Function.prototype). You also had to think where to put these object prototypes and a pretty obvious spot to put them would have been on these functions, just like the other builtins.

So all in all, you probably wouldn't want these functions to not be constructors because you wouldn't want to put an unnecessary burden of remembering all these edge cases on your users and those who will implement these. We would have questions today like: "Why is Number a function and not a constructor when it even has the Number prototype in its .prototype property?"

I'm using Number as an example, but I don't understand why Javascript exposes other constructors where literals are available either (String, Object, Array, etc.).

Well, each of these can also be extended through class. Extending Array for example was somewhat sought out before ES6. E.g.

class myArrayClass extends Array {
    myCustomMethod(){

    }
}

You also can extend String and Number in a somewhat safe manner.

Related