Looking at JavaScript with “new” eyes: Digging into the specs to learn more about the new operator

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:

Beautiful JavaScript cover with a mini donkey montage

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:

Comments

Contact Us

We'd love to hear from you. Get in touch!

Boston

201 South Street, Boston, MA 02111

New York

315 Church St, New York, NY 10013

Phone & Email

(617)379-2752 hello@bocoup.com