Standard constructors and the new operator

Why do various standard constructors behave differently when invoked with and without the new operator. I've identified four categories:

  1. Different. These constructors return an object when invoked with new and a primitive value otherwise. They are Boolean, Date, Number, RegExp, and String.

  2. Same. These constructors return an object when invoked with or without the new operator. Examples include Array, Error, Function, Object, and the aggregate and native errors.

  3. Only new. These constructors return an object when invoked with new and throw TypeError otherwise. Examples include ArrayBuffer, the TypedArray constructors, DataView, Map, Promise, etc.

  4. No new. These constructors throw a TypeError when invoked with new and return a primitive value otherwise. They are BigInt and Symbol.

I am writing a Medium article about this, and I would be glad for perspectives from the community.
I am guessing there are both functional and historical reasons. Thank you for your thoughts!

The "primitive value otherwise" doesn't apply to RegExp here. RegExp returns RegExp objects in both cases (the difference between being called as a constructor and a function is that the function version may return the argument its given rather than a new RegExp if that argument is itself already a RegExp).

1 Like

Category 1 is for pre-ES6 non-nullish primitives. Category 2 is pre-ES6 builtin objects. Category 3 is post-ES6 builtin objects. Category 4 is post-ES6 primitives.

(Date is a weird legacy constructor from pre-ES6 which, when invoked without new, returns a string)

The "why" is just because ideas driving the language, along with the people involved, shift over time.

3 Likes

Thanks, senocular and ljharb. Regarding Date, I'm looking forward to Temporal. Regarding Category 1, I'm still curious why new Boolean seemed like a good idea at the time, but that's just with the advantage of hindsight. Appreciate the input, and I'll leave this topic open in case anyone else is willing to provide some historical context.

If you're not already familiar with JavaScript the first 20 years, that could be a source for some information as it covers a lot around some of the early development of the language. The old https://esdiscuss.org/ list could also be useful as well. And of course there's always the proposals covering everything post-ES6 (e.g. looking into BigInt issues could possibly shed some extra light there).

2 Likes

Thank you, senocular. I didn't know about JavaScript the first 20 years. I'll read it. Looks like the source I'm looking for.

Maybe worth considering the Object(thing) case which makes both Symbol and BigInt, together with other primitives, an actual object, instance of their primitive constructor.

The most notable shenanigan there is Object(null) which doesn't return the equivalent of Object.create(null), it returns an object literal.

Why would you expect Object to behave the same as Object.create?

I wouldn't expect that for anything but Object('string') return a new String('string') and Object(false) return a new Boolean(false) but Object(null) return just {} instead of an object which primitive/prototype is actually null ... all others can be reverse-engineered to their primitive form/shape/type, Object(null) cannot.

It is indeed a legacy inconsistency that Object doesn’t throw on nullish values; it should.

1 Like

I wouldn't say nullish ... Object(undefined) throwing? I am OK with it, but Object(null) as (faster?) shortcut for Object.create(null) ... I'd sign for it instead of having just a literal ... I guess it's too late for that though, I wasn't suggesting a change, rather underlying that legacy quirk that couldn't be fixed in ES 5.1 time.

to expand:

Object(false).valueOf() === false; // true
Object('').valueOf() === ''; // true
Object(Symbol.for('symbol')).valueOf() === Symbol.for('symbol'); // true

Object(null).valueOf() === null; // false

That's it ... anything can be used as primitive even behind an Object(...) call, but not null ... this is again not a request, just yet another quirk in legacy JS.

edit ironically enough Object(null).valueOf() should also throw as null prototype has no such method :sweat_smile:

That simply wouldn’t make any sense; Object doesn’t take a [[Prototype]] value.

The thing that it does with all non-nullish values is make an object form of it; since null and undefined aren’t object-coercible, it can’t make an object form of it, so it should throw.

I guess it's PoV and DX ... Object('') generates an instanceof String out of the type of the argument so it infers the prototype and return new String('') ... the same could be done for Object(null) where the the inferred prototye is the same behind Object.prototype itself and return what Object.create(null) does.

To me that would've worked wonderfully, instead I need to know Object(null) doesn't do what I expect and fallback to Object.create(null) when Object(null) is actually what I meant :person_shrugging:

I'm not sure why you're throwing these into the same box. The internal [[Prototype]] of an object and the internal [[PrimitiveValue]] of an object are two completely separate concepts. An object which is not a Boolean/String/Number/Symbol/BigInt instance simply doesn't have a [[PrimitiveValue]], no matter whether it is Obect.create(null) or Object.create(Object.prototype)/{}.

It seems like you want Object(x).valueOf() === x to hold for all x, but that wouldn't even work if Object(null) returned Object.create(null)/{__proto__: null}.

I don’t think this conversation is worth anyone time and I don’t personally care about it neither … simply put: the DX on Object(null) is both misleading and considered a legacy mistake while I would love for it to have been a faster shortcut to Object.create(null).

That’s it, that’s my comment and if anyone is going to write a post about constructors and object primitive wrappers via Object(type) I think it’s worth mentioning null case too, as that means nothing and it’s a pity (to me).

The rest is history, I was never here to change that, I was here to suggest the Object(bigint) and others where new BigInt or Symbol would fail, yet there are wrappers for those too.