To me, the JavaScript language is as beautiful and unexpectedly wondrous as a mini donkey. If I could propose a new cover for the book Beautiful JavaScript, I would choose this one:
The reason I find JavaScript beautiful is that there’s something new to learn everyday. For example, recently I learned some surprising facts about the new
operator which I want to share with you in this post.
As JavaScript developers, we use the new
operator everyday to create fresh new instances, and using it seems easy if you’ve already tried it.
What I didn’t know about new
is how its classification as an operator could change the whole spectrum of my knowledge about the language. JavaScript is a powerful language, and it’s very well-designed. There’s a special meaning for operators in JavaScript, and having a keyword operator provides some interesting use cases, some of which I will explore here.
The Beginning
It started when I saw a code example that was written like this:
function Foo() {
this.bar = _ => ( { baz: 42 } );
// In ES5, this could be equally represented as:
// this.bar = function() { return { baz: 42 }; };
}
// what, no wrapping?
new Foo().bar().baz === 42; // true
My initial reaction was to tell the author that they needed to wrap their instantiation in parentheses. I was certain the correct code should be:
(new Foo()).bar().baz === 42; // true
To me, the added syntax made it obvious that new Foo()
is evaluated before it jumps into the property baz
of the instance I created.
That first example—the one without the parentheses wrapping it up? It is completely valid. Do you know what’s not valid? This:
new Foo.bar().baz === 42; // TypeError: object is not a constructor
In the words of the wise JavaScript philosophers, undefined is not a function. In this case, the new
operator is being evaluated on the nonexistent property bar
of Foo
.
Even though running only new Foo
is valid, this part must be wrapped:
(new Foo).bar().baz === 42; // true
I was desperate to understand this—how could the first example be valid? It was absurd! Thankfully, my colleague Mike Pennisi, one of the maintainers of test262, the test suite for ECMAScript itself, helped me understand. His answer was short. “new
is an operator,” he said, and gave this example:
3+4 + 5 // whitespace doesn't matter to operators.
It took me a while to accept this. So I tried to remove the irrelevant whitespace:
new(Foo)().bar().baz === 42; // true
It worked! new
is indeed an operator. You can check the syntax error on typeof new;
. The example looks weirder when we remove those extra ()
, because it repeats the new Foo.bar
case:
new(Foo).bar().baz === 42; // TypeError: object is not a constructor
But how does this really work?
It was around this time I started an investigation into the ECMAScript language specification to really dig into what was going on.
First, I searched for the new operator on the ES2015 specs, and then I found chapter 12.3.3. Within its logical syntax, it explains that the new
operator might be represented as such:
NewExpression : new NewExpression
MemberExpression : new MemberExpression Arguments
In chapter 12.3, you can find that NewExpression might be represented by a MemberExpression:
NewExpression [Yield]:
MemberExpression [?Yield]
new NewExpression [?Yield]
And a MemberExpression can be represented by another MemberExpression plus a .
IdentifierName.
MemberExpression [Yield]:
PrimaryExpression [?Yield]
MemberExpression [?Yield] [ Expression [In, ?Yield] ]
MemberExpression [?Yield] . IdentifierName
MemberExpression [?Yield] TemplateLiteral [?Yield]
SuperProperty [?Yield]
MetaProperty
new MemberExpression [?Yield] Arguments [?Yield]
This is the case in new Foo.bar
, where the .bar
represents the “.
IdentifierName” case.
When we wrap new Foo
in parentheses, we have the NewExpression represented by a new
MemberExpression where the MemberExpression is a PrimaryExpression.
The PrimaryExpression is representing Foo
as an IdentifierReference, which is represented as an Identifier. This is deep, right?
What about the new Foo().bar()
? That’s a bit tricky. new Foo()
is evaluated earlier in the following syntax:
new MemberExpression [?Yield] Arguments [?Yield]
Here, Arguments is perfectly represented by ()
. That’s what validates that line of code without needing to wrap it up. Precedence is guaranteed.
Can we make it more mind-boggling?
Do you remember that NewExpression can be represented by a MemberExpression or another “new NewExpression“?
Imagine we wanted a brand new instance from the result of instantiating something else. In order to do that, we need to make sure the first instance returns a valid constructor.
The following code should work:
function Foo() {
return function() {
this.bar = 42;
};
}
new new Foo(); // { bar: 42 }
What happens if you try to get the value of the bar
property of this
in the same line?
new new Foo().bar; // TypeError: object is not a constructor
That’s an IdentifierName for new Foo()
as a valid MemberExpression. This is the same as new (new Foo()).bar
, but you can get there with some ridiculous extra parentheses:
new new Foo()().bar; // 42
This example above represents two MemberExpressions terminated by Arguments. The first one is the contained new Foo()
, and the second one is the sum of new Foo()
plus the extra parentheses new Foo()()
.
Conclusion
It turns out we can learn something new
every day, and this special case was an amazing experience for me! There are even more puzzling cases on the new
operator, but I’ve decided to limit myself to just a few in this post to get this conversation started.
With a little effort to understand the logical syntax in the ECMAScript specification, we can achieve a better understanding of the code we write and understand where we really need to wrap our code in parentheses.
As it turns out, this is valid too!
function New() {
return New;
}
new new new new new new new New();
// learning a new trick everyday
Have you encountered any other puzzling cases for the new
operator? Please share your experiences with us!
Creative Commons imagery:
- Mini Donkey under CC BY-SA 3.0: https://upload.wikimedia.org/wikipedia/commons/2/24/Poitou-female-one-year-old.jpg