Building A Mental Model For Precedence And The "new" Operator In JavaScript
For the most part, when I write JavaScript code, I try to be mindful. Which means that when I write JavaScript code - or any code for that matter - I never want to rely on operator precedence. This is for me; but, it's just as much for the next engineer who has to maintain my code. As such, I am very liberal with my use of parenthesis so as to add clarity to the way in which an expression will be evaluated. That said, the one case in which I do tend to lean on operator precedence is when using the "new" operator for constructing object instances. But, the reality is, I've never felt quite comfortable with this. The "new" operator precedence has always felt a little bit like black magic. So, I wanted to take a minute to and try to build a better mental model around it.
If you look at the Mozilla documentation on Operator Precedence, you can see that the "new" operator has the same precedence - 19 - as member access and function calls.
|
|
|
||
|
|
|||
|
|
|
The one place where this concept of precedence and the "new" operator keeps coming up for me is when I create a new Date object in order to access the current UTC tick count:
new Date().getTime()
NOTE: In modern browsers, and Node.js, you can simply use Date.now().
How does JavaScript know that the Date object is a constructor? How does JavaScript know that Date() isn't just a function call that returns an object that contains a constructor called GetTime()? After all, the constructor can certainly live as an object property:
new global.Date().getTime()
If member access and function calls all have the same operator precedence - 19 - how does JavaScript know when to stop evaluating the right-hand side of the expression and instantiate a new object?
I think the trick here is that the associativity for the "new" operator is "N/A" (not applicable). Which, in my mind, means that the "new" operator is actually a little bit "black magic."
|
|
|
||
|
|
|||
|
|
|
The mental model that seems to work best for me (so far) is that JavaScript will evaluate the operators (in group 19) from left-to-right until it hits a Function with arguments (ie, using parenthesis). At that point, JavaScript will apply the "new" operator and then continuing evaluating the rest of the expression on the result of the instantiation. As such, we can think of the expression:
new global.Date().getTime()
... as being equivalent to:
( new global.Date() ).getTime()
JavaScript found the first Function invocation in the expression, stopped to apply the "new" operator, and then continued on, evaluating .getTime() on the result of the instantiation.
Now, going back to my earlier question on returning a constructor from a function, how would JavaScript know to evaluate something like this:
new echoValue( Date )().getTime()
... where echoValue() is a function that returns its first argument?
Well, it won't. JavaScript will end up applying the "new" operator to the echoValue() Function since it's the first Function with arguments in the expression. If we really needed to return a constructor from a Function, we would have to explicitly give the function call higher precedence. And, in JavaScript, the only thing with higher precedence is the Grouping operator:
new (echoValue( Date ))().getTime()
As you can see, we've wrapped our echoValue() call in parenthesis in order to indicate that it has a higher precedence - 20 - than the "new" operator - 19. Now, the echoValue() Function will be evaluated first and echo back the Date object, which becomes the left-most newable object in the expression.
This might look really silly; but, one place this has practical implications is when you need to access a constructor returned by a require() call in Node.js:
new (require( "my-module" )).MyModule();
In order to prevent JavaScript from applying the "new" operator to the require() Function, we can wrap it in parenthesis. This tells JavaScript that the require() call has a higher precedence than the "new" operator; which makes the MyModule Function the first newable Function in the evaluated expression.
AN ASIDE ON TEAMWORK: Parenthesis are your friend. Operator precedence is an interesting topic; but, you should absolutely never assume that it is second nature to your teammates. Your 6th grade Algebra teacher may have demanded that you know that multiplication comes before addition (remember PEMDAS); but, when you're working with a team, never never ever depend on operator precedence! Always use parenthesis to spell out exactly what you are expecting to happen.
The "new" operator is a tricky operator. And to be clear, this post is basically me guessing at how and why JavaScript is evaluating expressions that contain a mix of different operators with the same precedence. That said, this mental model is working well for me so far. And, hopefully, it works well for you in case you also had a less-than-stellar understanding of when an expression is "newed" into existence.
Reader Comments
This was a great article, Ben. I'm using Javascript for almost 4 years and the idea of how JS knows a function has a constructor never crossed my mind.
Now it's all cristal clear.
Kyle Simpson shed's light on that `new` magic here https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch2.md#new-binding
@Farzad,
Excellent sir -- glad you enjoyed it.
@Derek,
Kyle Simpson is an awesome resource! I keep meaning to read his books. He always forces me to think more deeply about how things work and about what kind of assumptions I'm making.
I wish the YDKJS series and Eloquent JS existed when I started JS, both are just insanely great resources.
Your comments at the beginning and end of this article, regarding liberal use of parens for clarity and readability, echo statements I've made to my own team numerous times. Even though certain statements make for perfectly legal assignments according to the language itself, parens are free, will be removed during minification/obfuscation, and greatly aid in readability and signaling the actual intent of how a statement should execute, both to other engineers, and to future you, when you have to come back and maintain it 6+ months from now :) I was glad to see that mentality validated here :)
"until it hits a Function with arguments (ie, using parenthesis)."
Huh? Your example of "Date()" uses parentheses but doesn't include any arguments. The part of MDN you quoted said "argument list", where I guess a "list" could be empty, but "with arguments" implies a non-empty argument list.
@Richard,
It's like my Dad used to say me when crossing the street - always let the cars go. With regard to "rightive way", he always said, "You might be right -- dead right" (indicating that I might be right to cross, but the car will still kill me).
Ok, maybe not a great analogy - but the point is, doing "good" for your team is not the same as being "right" in the code.
@Ben,
I'm not sure how to interpret that :D Do you mean sometimes being "dead right" in the code can be bad for the team if it's done too militantly? Or too nitpicky? If so, I would agree, and always try to phrase it in a constructive fashion (or make my biggest nitpicks enforceable via linting so that I don't have to have that conversation in the first place). On some occasions, if there have been a few back-and-forths in a code review, with subsequent commits to clean things up, sometimes I'll let the lesser items go, and fix them myself at a later time, rather than seem like I'm trying to beat them down with review comments over and over :)
If you meant something else, I missed it :)
@Nathan,
Good catch - I think I miswrote there. I think what I mean to write was a "function with parenthesis". I was trying to differentiate accessing a function as an object as opposed to invoking it. For example, if "Foo" is a constructor, you can certainly store properties on that constructor (since it inherits from Object):
new window.Foo.thing();
In this case, "Foo" is a function, but it has no parenthesis - it's being accessed an object. Which is different than:
new window.Foo().thing();
... in which it is being invoked.
Good catch though, I think I was caught up in trying to thinking about it that I didn't get the right words down on paper.
@Richard,
Yeah - that's what I meant. Just wasn't getting the right words down. Just saying your code can be "right", but it doesn't mean it's "right for the team."
It's not magic.
Level 19 new operator syntax:
new <sub-expression-1> ( <sub-expression-2> )
The sub-expression-2 can be anything.
The sub-expression-1 can be level 19-20 operators (chain), because the precedence, and a result must be an object with a constructor. But the syntax of the function call matching with the new operator syntax ending. Resulting three cases:
And, the bracket and the new operator must be first in a subexpession, because they are non-associative, not "chainable" to left-to-right.
@Mlaci,
But, how is the JavaScript engine differentiating between a "constructor" and a "function". After all, can't all Functions essentially be used as a constructor? Even an object method can be used as a constructor if its invoked with the
new
operator.This might be what you are saying -- I think maybe I am not fully understanding your explanation. But, given my confusion about the topic at its onset, this is not surprising :D
@Ben,
Sorry, my answer misleading, it's nothing to do with types, it just how to parse the javascript syntax to syntax tree.
I wrote incorrectly that
<sub-expression-1>
could contain function calls, I messed up something in a console when i tested. Instead, try some example from your blog post on https://astexplorer.net, it's much better tool for it.@Mlaci,
That site looks really interesting. I know of AST (Abstract Syntax Tree); but, I've never really thought much about it, other than it probably makes for better work-flows than parsing via Regular Expressions :D