For and against `let`
In this post I'm going to examine the case for (and perhaps against?) one of the new features coming in JavaScript ES6: the let
keyword. let
enables a new form of scoping not previously accessible generally to JS developers: block scoping.
Function Scope
Let's briefly review the basics of function scoping -- if you need more indepth coverage, check out my "You Don't Know JS: Scope & Closures" book, part of the "You Don't Know JS" book series.
Consider:
foo(); // 42 function foo() { var bar = 2; if (bar > 1 || bam) { var baz = bar * 10; } var bam = (baz * 2) + 2; console.log( bam ); }
You may have heard the term "hoisting" to describe how JS var
declarations are treated within scopes. It's not exactly a technical description for how it works, but more a metaphor. But for our purposes here, it's good enough to illustrate. That above snippet is essentially treated as if it had been written like:
function foo() { var bar, baz, bam; bar = 2; if (bar > 1 || bam) { baz = bar * 10; } bam = (baz * 2) + 2; console.log( bam ); } foo(); // 42
As you can see, the foo()
function declaration was moved (aka "hoisted", aka lifted) to the top of its scope, and similarly the bar
, baz
, and bam
variables were hoisted to the top of their scope.
Because JS variables have always behaved in this hoisting manner, many developers choose to automatically put their var
declarations at the top of each (function) scope, so as to match code style to its behavior. And it's a perfectly valid way of going about things.
But have you ever seen code which does that, but also will do things like this in the same code:
for (var i=0; i<10; i++) { // .. }
That is also extremely common. Another example that's fairly common:
var a, b; // other code // later, swap `a` and `b` if (a && b) { var tmp = a; a = b; b = tmp; }
The var tmp
inside the if
block sorta violates the ostensible "move all declarations to the top" coding style. Same of the var i
in the for
loop in the earlier snippet.
In both cases, the variables will "hoist" anyway, so why do developers still put those variable declarations deeper into the scope instead of at the top, especially if all the other declarations have already been manually moved?
Block Scoping
The most salient reason is because developers (often instinctively) want some variables to act as if they belong to a smaller, more limited section of the scope. In specific terms, there are cases where we want to scope a variable declaration to the block that it's solely associated with.
In the for (var i=..) ..
case, it's almost universal that the developer intends for the i
to only be used for the purposes of that loop, and not outside of it. In other words, the developer is putting the var i
declaration in the for
loop to stylistically signal to everyone else -- and their future self! -- that the i
belongs to the for
loop only. Same with the var tmp
inside that if
statement. tmp
is a temporary variable, and only exists for the purposes of that if
block.
Stylistically, we're saying: "don't use the variable anywhere else but right here".
Principle of Least Privilege
There's a software engineering called "principle of least privilege (or exposure)", which suggests that proper software design hides details unless and until it's necessary to expose them. We often do exactly this in module design, by hiding private variables and functions inside a closure, and exposing only a smaller subset of functions/properties as the public API.
Block scoping is an extension of this same mindset. What we're suggesting is, proper software puts variables as close as possible, and as far down in scoping/blocking as possible, to where it's going to be used.
You already instinctively know this exact principle. You already know that we don't make all variables global, even though in some cases that would be easier. Why? Because it's bad design. It's a design that will lead to (unintentional) collisions, which will lead to bugs.
So, you stick your variables inside the function they are used by. And when you nest functions inside of other functions, you nest variables inside those inner functions, as necessary and appropriate. And so on.
Block scoping simply says, I want to be able to treat a { .. }
block as a scope, without having to make a new function to encapsulate that scope.
You're following the principle by saying, "If I'm going to only use i
for this for
loop, I'll put it right in the for
loop definition."
JS Missing Block Scoping
Unfortunately, JS has not historically had any practical way to enforce this scoping style, so it's been up to best behavior to respect the style being signaled. Of course, the lack of enforcement means these things get violated, and sometimes it's OK while other times it leads to bugs.
Other languages (e.g., Java, C++) have true block scoping, where you can declare a variable to belong to a specific block instead of to the surrounding scope/function. Developers from those languages know well the benefits of using block scoping for some of their declarations.
They often feel JS has been lacking in expressive capability by missing a way to make an inline scope within a { .. }
block instead of the heavier-weight inline function definition (aka IIFE -- Immediately Invoked Function Expression).
And they're totally right. JavaScript has been missing block scoping. Specifically, we've been missing a syntactic way to enforce what we already are comfortable expressing stylistically.
Not Everything
Even in languages that have block scoping, not every variable declaration ends up block scoped.
Take any well-written code base from such a language, and you are certainly going to find some variable declarations that exist at the function level, and others which exist at smaller block levels. Why?
Because that's a natural requirement of how we write software. Sometimes we have a variable we're going to use everywhere in the function, and sometimes we have a variable that we're going to use in just a very limited place. It's certainly not an all-or-nothing proposition.
Proof? Function parameters. Those are variables that exist for the entire function's scope. To my knowledge, no one seriously advances the idea that functions shouldn't have explicit named-parameters because they wouldn't be "block scoped", because most reasonable developers know what I'm asserting here:
Block scoping and function scoping are both valid and both useful, not just one or the other. This kind of code would be quite silly:
function foo() { // <-- Look ma, no named parameters! // .. { var x = arguments[0]; var y = arguments[1]; // do something with `x` and `y` } // .. }
You almost certainly wouldn't write code like that, just to have a "block scoping only" mentality about code structure, anymore than you'd have x
and y
be global variables in a "global scoping only" mentality.
No, you'd just name the x
and y
parameters, and use them wherever in the function you need.
The same would be true of any other variable declarations you might create which you intend and need to use across the entire function. You'd probably just put a var
at the top of the function and move on.
Introducing let
Now that you understand why block scoping is important, and importantly swallowed the sanity check that it amends function/global scoping rather than replacing it, we can be excited that ES6 is finally introducing a direct mechanism for block scoping, using the let
keyword.
In its most basic form, let
is a sibling to var
. But declarations made with let
are scoped to the blocks in which they occur, rather than being "hoisted" to the enclosing function's scope as var
s do:
function foo() { a = 1; // careful, `a` has been hoisted! if (a) { var a; // hoisted to function scope! let b = a + 2; // `b` block-scoped to `if` block! console.log( b ); // 3 } console.log( a ); // 1 console.log( b ); // ReferenceError: `b` is not defined }
Yay! let
declarations not only express but also enforce block scoping!
Basically, any place a block occurs (like a { .. }
pair), a let
can create a block scoped declaration inside it. So wherever you need to create limited-scope declarations, use let
.
Note: Yeah, let
doesn't exist pre-ES6. But quite a few ES6-to-ES5 transpilers exist -- for example: traceur, 6to5, and Continuum -- which will take your ES6 let
usage (along with most of the rest of ES6!) and convert it to ES5 (and sometimes ES3) code that will run in all relevant browsers. The "new normal" in JS development, given that JS is going to start rapidly evolving on a feature-by-feature basis, is to use such transpilers as a standard part of your build process. This means that you should start authoring in the latest and greatest JS right now, and let tools worry about making that work in (older) browsers. No longer should you foregoe new language features for years until all previous browsers go away.
Implicit vs Explicit
It's easy to get lost in the excitement of let
that it's an implicit scoping mechanism. It hijacks an existing block, and adds to that block's original purpose also the semantics of being a scope.
if (a) { let b = a + 2; }
Here, the block is an if
block, but let
merely being inside it means that the block also becomes a scope. Otherwise, if let
was not there, the { .. }
block is not a scope.
Why does that matter?
Generally, developers prefer explicit mechanisms rather than implicit mechanisms, because usually that makes code easier to read, understand, and maintain.
For example, in the realm of JS type coercion, many developers would prefer an explicit coercion over an implicit coercion:
var a = "21"; var b = a * 2; // <-- implicit coercion -- yuck :( b; // 42 var c = Number(a) * 2; // <-- explicit coercion -- much better :) c; // 42
Note: To read more on this side-topic of implicit/explicit coercion, see my "You Don't Know JS: Types & Grammar" book, specifically Chapter 4: Coercion.
When an example shows a block with only one or a few lines of code inside it, it's fairly easy to see if the block is scoped or not:
if (a) { // block is obviously scoped let b; }
But in more real world scenarios, many times a single block can have dozens of lines of code, maybe even a hundred or more. Setting aside the preference/opinion that such blocks shouldn't exist -- they do, it's a reality -- if let
is buried way down deep in the middle of all that code, it becomes much harder to know if any given block is scoped or not.
Conversely, if you find a let
declaration somewhere in the code, and you want to know to which block it belongs, instead of just visually scanning upwards to the nearest function
keyword, you now need to visually scan to the nearest {
opening curly brace. That's harder to do. Not a lot harder, but harder nonetheless.
It's a bit more mental tax.
Implicit Hazards
But it's not only a mental tax. Whereas var
declarations are "hoisted" to the top of the enclosing function, let
declarations are not treated as having been "hoisted" to the top of the block. If you accidentally try to use a block-scoped variable in the block earlier than where its declaration exists, you'll get an error:
if (a) { b = a + 2; // ReferenceError: `b` is not defined // more code let b = .. // more code }
Note: The period of "time" between the opening {
and where the let b
appears is technically called the "Temporal Dead Zone" (TDZ) -- I'm not making that up! -- and variables cannot be used in their TDZ. Technically, each variable has its own TDZ, and they sort of overlap, again from the opening of the block to the official declaration/initialization.
Since we had previously put the let b = ..
declaration further down in the block, and then we wanted to come back and use it earlier in the block, we have a hazard -- a footgun -- where we forgot we needed to go find the let
keyword and move it to the earliest usage of the b
variable.
In all likelihood, developers are going to get bitten by this TDZ "bug", and they'll eventually learn from that bad experience to always put their let
declarations at the top of the block.
And there's another hazard to implict let
scoping: the refactoring hazard.
Consider:
if (a) { // more code let b = 10; // more code let c = 1000; // more code if (b > 3) { // more code console.log( b + c ); // more code } // more code }
Let's say later, you realize the if (b > 3)
part of the code needs to be moved outside the if (a) { ..
block, for whatever reason. You realize you also need to grab the let b = ..
declaration to move along with it.
But you don't immediately realize that the block relies on c
as well -- because it's a bit more hidden down in the code -- and that c
is block scoped to the if (a) { ..
block. As soon as you move the if (b > 3) { ..
block, now the code breaks, and you have to go find the let c = ..
declaration and figure out if it can move, etc.
I could keep coming up with other scenarios -- hypothetical yes, but also extremely informed by lots of experience not only with my own but with others own real world code -- but I think you get the point. It's awfully easy to get yourself into these hazard traps.
If there had been explicit scopes for b
and c
, it would probably have been a little bit easier to figure out what refactoring is necessary, rather than stumbling along to figure it out implicitly.
Explicit let
Scope
If I've convinced you that the implicit nature of let
declarations could be a problem/hazard -- if you're not extremely careful, as well as every other developer that ever works on your code! -- then what's the alternative? Do we avoid block scoping entirely?
No! There are better ways.
Firstly, you can force yourself into a style/idiom that not only puts your let
declarations at the top of the scope, but also that creates an explicit block for such scope. For example:
if (a) { // more code // make an explicit scope block! { let b, c; // more code b = 10; // more code c = 1000; // more code if (b > 3) { // more code console.log( b + c ); // more code } } // more code }
You'll see here I created a naked { .. }
pair, and put the let b, c;
declaration right at the very top, even on the same line. I'm making it as clear and explicit as possible that this is a scope block, and that it holds b
and c
.
If at a later time I need to move some b
code around, and I go find the combined scope for b
and c
, it's not only easier to recognize, but easier to accomplish, that I can move the entire { let b, c; .. }
block safely.
Is this perfect? Of course not. But it's better, and has less hazards and less mental tax (even by little bit) than the implicit style/idioms from earlier. I implore all of you, as you begin to use let
block scoping, please consider and prefer a more explicit form over the implicit form.
Always Explicit?
In fact, I'd say being explicit is so important that the only exception I've found to that "rule" is that I like and use for (let i=0; .. ) ..
. It's debatable if that's implicit or explicit. I'd say it's more explicit than implicit. But it's perhaps not quite as explicit as { let i; for (i=0; ..) .. }
.
There's actually a really good reason why for (let i=0; ..) ..
could be better, though. It relates to scope closures, and it's very cool and powerful!
{ let i; for (i=1; i<=5; i++) { setTimeout(function(){ console.log("i:",i); },i*1000); } }
That code will, like its more typical var
counterpart, not work, in that it'll print out i: 6
five times. But this code does work:
for (let i=1; i<=5; i++) { setTimeout(function(){ console.log("i:",i); },i*1000); }
It'll print out i: 1
, i: 2
, i: 3
, etc. Why?
Because the ES6 specification actually says that let i
in a for
loop header scopes i
not only to the for
loop, but to each iteration of the for
loop. In other words, it makes it behave like this:
{ let k; for (k=1; k<=5; k++) { let i = k; // <-- new `i` for each iteration! setTimeout(function(){ console.log("i:",i); },i*1000); } }
That's super cool -- it solves a very common problem developers have with closures and loops!
Note: This doesn't work in browsers yet, even those with let
. The ES6 spec requires it, but at time of writing, no browsers are compliant on this particular per-iteration nuance. If you want proof, try putting the code into ES6fiddle. See...
Even More Explicit let
Scope
OK, so maybe I've convinced you that explicit scopes are a bit better. The disadvantage of the above is that it's not enforceably required that you follow that style/idiom of { let b, c; .. }
, which means you or someone else on your team could mess up and not follow it.
There's another option. Instead of using the "let
declaration form", we could use the "let
block form":
if (a) { // make an explicit scope block! let (b, c) { // .. } }
It's a slight change, but look closely: let (b, c) { .. }
creates an explicit block of scope for b
and c
. It's syntactically requiring b
and c
to be declared at the top, and it's a block that's nothing but a scope.
In my opinion, this is the best way to use let
-based block scoping.
But there's a problem. The TC39 committee voted to not include this particular form of let
in ES6. It may come in later, or never, but it's definitely not in ES6.
Ugh. But this isn't the first, nor the last, that something that's more preferable loses out to an inferior option.
So, are we just stuck in the previous form?
Perhaps not. I've built a tool called "let-er", which is a transpiler for "let
block form" code. By default, it's in ES6-only mode, and it takes code like:
let (b, c) { .. }
And produces:
{ let b, c; .. }
That's not too awful, is it? It's a pretty simple transformation, actually, to get non-standard "let
block form" into standard "let
declaration form". After you run let-er for this transformation, you can then use a regular ES6 transpiler to target pre-ES6 environments (browsers, etc).
If you'd like to use let-er standalone without any other transpilers, only for let
-based block scoping, you can optionally set the ES3 mode/flag, and it will instead produce this (admittedly hacky junk):
try{throw void 0}catch( b ){try{throw void 0}catch( c ){ .. }}
Yeah, it uses the little-known fact that try..catch
has block scoping built into the catch
clause.
No one wants to write that code, and no one likes the degraded performance that it brings. But keep in mind, it's compiled code, and it's only for targeting really old browsers like IE6. The slower performance is unfortunate (to the tune of about 10% in my tests), but your code is already running pretty slowly/badly in IE6, so...
Anyway, let-er by default targets standard ES6, and thus plays well with other ES6 tools like standard transpilers.
The choice to make is would you rather author code with let (b, c) { .. }
style or is { let b, c; .. }
OK enough?
I use let-er in my projects now. I think it's the better way. And I'm hoping maybe in ES7, the TC39 members realize how important it is to add the "let
block form" into JS, so that eventually let-er can go away!
Either way, explicit block scoping is better than implicit. Please block scope responsibly.
let
Replaces var
?
Some prominent members of the JS community and the TC39 committee like to say, "let
is the new var
." In fact, some have literally suggested (hopefully in jest!?) to just do a global find-n-replace of var
for let
.
I cannot express how incredibly stupid that advice would be.
Firstly, the hazards we mentioned above would be enormously more likely to crop up in your code, as the odds are your code is not perfectly written with respect to var
usage. For example, this kind of code is extremely common:
if ( .. ) { var foo = 42; } else { var foo = "Hello World"; }
We can all probably agree it should have been written as:
var foo; if ( .. ) { foo = 42; } else { foo = "Hello World"; }
But it's not written that way yet. Or, you're accidentally doing things like:
b = 1; // .. var b;
Or you're accidentally relying on non-block-scoped closure in loops:
for (var i=0; i<10; i++) { if (i == 2) { setTimeout(function(){ if (i == 10) { console.log("Loop finished"); } },100); } }
So, if you just blindly replace var
with let
in existing code, there's a pretty good chance that at least some place will accidentally stop working. All of the above would fail if let
replaced var
, without other changes.
If you're going to retrofit existing code with block scoping, you need to go case by case, carefully, and you need to reason about and rationalize if it's a place where block scoping is appropriate or not.
There will certainly be places where a var
was used stylistically, and now a let
is better. Fine. I still don't like the implicit usage, but if that's your cup o' tea, so be it.
But there will also be places that, in your analysis, you realize the code has structural issues, where a let
would be more awkward, or would create more confusing code. In those places, you may choose to fix the code, but you may also quite reasonably decide to leave var
alone.
Here's what bugs me the most about "let
is the new var
": it assumes, whether they admit it or not, an elitist view that all JS code should be perfect and follow proper rules. Whenever you bring up those earlier cases, proponents will simply strike back, "well, that code was already wrong."
Sure. But that's a side point, not the main point. It's equally hostile to say, "only use let
if your scoping is already perfect, or you're prepared to rewrite it to make it perfect, and keep it perfect."
Other proponents will try to temper it with, "well, just use let
for all new code."
This is equivalently elitist, because again it assumes that once you learn let
and decide to use it, you'll be expected to write all new code without ever running into any hazard patterns.
I bet TC39 members can do that. They're really smart and really intimate with JS. But the rest of us are not quite so lucky.
let
is the new companion to var
The more reasonable and more realistic perspective, the one I take because my primary interface with JS is through the students/attendees that I speak to, teach, and work with, is to embrace refactoring and improving code as a process, not an event.
Sure, as you learn good scoping best practices, you should make code a little better each time you touch it, and sure, your new code should be a little better than your older code. But you don't just flip a switch by reading a book or blog post, and now all of a sudden you have everything perfect.
Instead, I think you should embrace both the new let
and the old var
as useful signals in your code.
Use let
in places you know you need block scoping, and you've specifically thought about those implications. But continue to use var
for variables that either cannot easily be block scoped, or which shouldn't be block scoped. There are going to be places in real world code where some variables are going to be properly scoped to the entire function, and for those variables, var
is a better signal.
function foo() { var a = 10; if (a > 2) { let b = a * 3; console.log(b); } if (a > 5) { let c = a / 2; console.log(c); } console.log(a); }
In that code, let
screams out at me, "hey, I'm block scoped!" It catches my attention, and I thus pay it more care. The var
just says, "hey, I'm the same old function-scoped variable, because I'm going to be used across a bunch of scopes."
What about just saying let a = 10
at the top level of the function? You can do that, and it'll work fine.
But I don't think it's a good idea. Why?
First, you lose/degrade the difference in signal between var
and let
. Now, it's just position that signals the difference, rather than syntax.
Secondly, it's still a potential hazard. Ever had a weird bug in a program, and started throwing try..catch
around things to try to figure it out? I sure do.
Oops:
function foo() { try { let a = 10; if (a > 2) { let b = a * 3; console.log(b); } } catch (err) { // .. } if (a > 5) { let c = a / 2; console.log(c); } console.log(a); }
Block scoping is great, but it's not a silver bullet, and it's not appropriate for everything. There are places where function scoping of var
s, and indeed of the "hoisting" behavior, are quite useful. These are not abject failures in the language that should be removed. They are things that should be used responsibly, as should let
.
Here's the better way to say it: "let
is the new block scoping var
". That statement emphasizes that let
should replace var
only when var
was already signaling block scoping stylistically. Otherwise, leave var
alone. It's still doing its job pretty well!
Summary
Block scoping is cool, and let
gives us that. But be explicit about your block scopes. Avoid implicit let
declarations strewn about.
let
+ var
, not s/var/let/
. Just frown then smirk at the next person who tells you, "let
is the new var
."
let
improves scoping options in JS, not replaces. var
is still a useful signal for variables that are used throughout the function. Having both, and using both, means scoping intent is clearer to understand and maintain and enforce. That's a big win!
About Kyle Simpson
Kyle Simpson is a web-oriented software engineer, widely acclaimed for his "You Don't Know JS" book series and nearly 1M hours viewed of his online courses. Kyle's superpower is asking better questions, who deeply believes in maximally using the minimally-necessary tools for any task. As a "human-centric technologist", he's passionate about bringing humans and technology together, evolving engineering organizations towards solving the right problems, in simpler ways. Kyle will always fight for the people behind the pixels.
A quite interesting take. I personally feel that
let
should be the new default instead ofvar
, but I completely agree that you can’t just blindly go and replace allvar
s withlet
for exactly the reasons you mention.However, I’m not sure I fully understand why you think function-scoped variables should use
var
? I mean a function is also a block, so declaring a variable withlet
would mean that it’s just block-scoped to the whole function.Same goes for the argumentation for using explicit blocks. I kinda get it that if you have a lot of code and you’re only using a variable in a small part of it, sure, then it might make sense to do that, but doing that won’t solve the problem with future refactorings moving bits out of the block because you might still have something you’re using elsewhere in the parent block.
Jani-
> I’m not sure I fully understand why you think function-scoped variables should use var?
I gave two main reasons in the article:
1. Having a different keyword for function-scoped variables (
var
) vs block-scoped variables (let
) in my opinion makes it easier to tell what the intended behavior is. If you uselet
everywhere, then you don’t have a different keyword to catch your attention, and thus it’s only the location that gives a signal. This is a weaker signal. Is it a massive difference? No. But it’s a lesser signal IMO, and there’s no reason why NOT to usevar
in those function-scoped positions, unless you’re of the “cult” of “let is the new var”. :)2. It’s too easy to accidentally wrap code in blocks (even temporarily) that can create unexpected issues with
let
wherevar
would have continued to work the same. The main case where I run into this issue is when I’m debugging and puttingtry..catch
into my code.vs.
Again, I don’t understand why people are in such a hurry to get rid of
var
? It’s not bad, and it’s not poorly designed.let
is useful to add to the toolbox available to us, but it has different behavior, andvar
s behavior is, IMO, sometimes preferable.That’s why I’m in the “let and var” cult.
Perhaps this is just because I’m more used to languages which have block level scoping :) I always tend to think of variables as block level even when not using
let
.For #1, I see
var
as an extra case I have to pay attention to. Instead of just thinking that all my variables are block level scoped, I also have to pay attention to which keyword was used to declare it.The second case you mention was never an issue for me, as I kind of automatically just move things into the correct blocks even when using
var
purely out of habit.I can see how for me this is probably because my background before JavaScript was languages which only have block level scoping, and I still use them. I can understand your point of view though, especially if thinking of people who don’t have a lot of experience in languages that have block scoping. The case with try-catch (and other similar things) is definitely something I’ve seen happen.
So ‘let’ buried deep in block will make that block non-reusable and non-refactorable.
Cool! Awesome! Another good thing implemented in JS with crappy side-effect. Is it made specially, just to not break JS-tradition to make everything a little bit crappy?
Thanks for head-up. I agree,
let (a,b) { }
could be most explicit variant of scope. It was too good for this language, that’s why they declined it.Now I wonder if they screwed up classes in some way also…
@Evgeniy — oh, they definitely screwed up
class
too. See: https://github.com/getify/You-Dont-Know-JS/blob/master/this%20&%20object%20prototypes/apA.md#appendix-a-es6-class :(Hey, just thought I’d let you know that the link in this comment is broken; I assume it’s mean to point to https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/this%20&%20object%20prototypes/apA.md#appendix-a-es6-class
(‘master’ is now ‘1st-ed’)
This article still gets passed around a lot and read by people so I figured it would be nice to keep it up to date.
Cheers
wouldn’t it have been better to write it like this:
I can’t begin to guess why someone decided to add “Temporal Dead Zone” to our vocabulary, other than that it probably followed a 17-hour binge-watch of ST:TNG accompanied by too much hipster brew.
Those of us who have been programming in Java, C++, and the like for the last two decades or so just call those “the place where I haven’t yet declared the variable I want to use.” I think life would have been better for all concerned if braces defined block scope and we could just be done with it. (Maybe not better for Brendan back in the day, but better now, IMHO.)
Your example with the loop works for me when I replace var with let:
This is because the function is within the for block scope?
So, why don’t we use functions to simulate block scope?, is way more explicit. e.g:
instead:
As of today, the
let
code that fixes the closure problem in a for loop *does* work in Google Chrome – woohoo!Hey man,
Really appreciate your article on this and it gave me exactly what I needed to make a decision on how I’ll be using let. Let-er looks interesting as well. Do you have any idea how I could get that working with Ember’s build process? Thanks!
I think that it does make sense to replace all vars with lets. It’s true that it might break existing code, but coming from a C++/C# background, it makes sense and behaves much more like every other language
Great post Kyle!
Explicit typing however is more verbose and I do not quite think it brings as much benefit. I definitely am in the ‘let and var’ group :). Let still doesn’t allow for global variables – something var does really well.
But declaring explicit blocks seems just too much. Great post all the same.
Why not make an exception for “try catch block”, so that it will not create new scope, I mean:
Similar thing is in python AFAIK.
I hate the use of foo and bar and baz and bam…. use some real examples!!!
This is an excellent post and you made your case quite well and provided a good history (I was looking at a video that included the “old”, disallowed explicit let syntax and wondering why it didn’t work until I read your answer).
I also think your caution against a blind search and replace is quite prudent. On balance, however, I have to side with the reader who mentioned that it’s wise to use let as extensively as possible going forward, but this is only because I am far more steeped in Java / C++ / C / C# scoping rules (which all have block scope), so to me it seems natural. But I can see where, if you’re steeped in the JS world as it is, the movement to block scope can appear to be more “elitist”. If it makes you feel better, however, I think the JS guys are the cool kids now. :)
Um,
let
is not new to ES6. It’s listed as something ambiguous and dangerous in Crockford’s “Javascript: The Good Parts”, and it’s been in JS for ages.I believe you are mistaken. JavaScript most definitely did not have
let
in it until ES6, that much I am sure. I know that spidermonkey had an experimental version of it a decade ago, but it was only in that one engine until ES6. Also, I don’t recall Crockford talking about it in his book, but even if he did, he was definitely not talking about something that was part of the standard at the time.How about a transpiler to convert existing code into canonical form, such that each variable is declared with “let” at the top of it’s minimal possible scope , and declarations are separated from assignment as necessary to avoid your try-catch mistake. Optionally, the transpiler could also introduce new scopes with brackets, with an ad hoc weighting to balance the number of scopes (less is better) with size of scopes (smaller is better).
That could be run on code before human refactoring, to reduce the risks you mentioned. Similar to pretty-printing – in a way.
Such a transpiler could be made without any new science (and may already have been made). According to your arguments about the dangers of refactoring, such a transpiler could be used before refactoring to reduce human error.
When ever new functions or options are entering a language, first question for me is, does it solve my problem? With let, it doesnt solve any of my problems , because i have no problems with var. Why is that?
So I think I call it that: “let is the new var, and var is the exception from that.”
let is how C# worked from the start. In a C#/JavaScript project, let is easier to understand because it just does the same as a variable in C# does. var could still be used in cases where the variable should also be accessible outside its block. But that should be the exception because it requires the reader to fully understand the special behaviour of var, which most just don’t.
How
let c = 3
is hoisted as we know that we cannot write:>What about just saying let a = 10 at the top level of the function? You can do
that, and it’ll work fine.
>But I don’t think it’s a good idea. Why?
>First, you lose/degrade the difference in signal between var and let. Now, it’s just position that signals the difference, rather than syntax.
what in the actual flurry did I just read. C and Java and other such language devs have been doing just fine with block scoping. The only reason you shouldn’t find and replace var=>let is because of legacy code that counts on function scoping to work.
your justification with the try catch block just reads like another attempt at grasping straws to justify the idiocy of var in 2020.
the only situation I would think of using var, would be when I’m lazy. It happened to me before,
Imagine this situation:
and then later I change the code to the following, without being careful with the scope
but if I used var, it would work fine, without needing to change the code to:
THe above is much more robust, using var is asking for trouble. Can’t change my mind on that.
Absolutely agree. I’ve been doing C and C# for ages now, and never had any problems with block scope.
And of course a function is a scope too – it has curly braces!
As of (around) 2018, in any new JS project I would not dream of ever using ‘var’ again.
So does this thing work
let (a,b) { }
now?And well, you say not to use let instead of var at the top of a function, but we most likely want to use “const” anyway, so if we use “const” which is like “let” but read-only, then it doesn’t make much sense to use “var” instead of “let” because it’s not coherent