When developing software , most of our time is spent reading code . ES6 offers _let_ and _const_ as new flavors of variable declaration, and part of the value in these statements is that they can signal how a variable is used. When reading a piece of code, others can take cues from these signals in order to better understand what we did. Cues like these are crucial to reducing the amount of time someone spends interpreting what a piece of code does, and as such we should try and leverage them whenever possible. A statement indicates that a variable can’t be used before its declaration, due to the Temporal Dead Zone rule. This isn’t a convention, it is a fact: if we tried accessing the variable before its declaration statement was reached, the program would fail. These statements are block-scoped and not function-scoped; this means we need to read less code in order to fully grasp how a variable is used. _let_ _let_ The statement is block-scoped as well, and it follows TDZ semantics too. The upside is that bindings can only be assigned during declaration. const const Note that this means that the variable binding can’t change, but it doesn’t mean that the value itself is immutable or constant in any way. A binding that references an object can’t later reference a different value, but the underlying object can indeed mutate. const In addition to the signals offered by , the keyword indicates that a variable binding can’t be reassigned. This is a strong signal. You know what the value is going to be; you know that the binding can’t be accessed outside of its immediately containing block, due to block scoping; and you know that the binding is never accessed before declaration, because of TDZ semantics. let const You know all of this just by reading the _const_ declaration statement and without scanning for other references to that variable. Constraints such as those offered by and are a powerful way of making code easier to understand. Try to accrue as many of these constraints as possible in the code you write. The more declarative constraints that limit what a piece of code could mean, the easier and faster it is for humans to read, parse, and understand a piece of code in the future. let const Granted, there’s more rules to a declaration than to a declaration: block-scoped, TDZ, assign at declaration, no reassignment. Whereas statements only signal function scoping. Rule-counting, however, doesn’t offer a lot of insight. It is better to weigh these rules in terms of complexity: does the rule add or subtract complexity? In the case of , block scoping means a narrower scope than function scoping, TDZ means that we don’t need to scan the scope backwards from the declaration in order to spot usage before declaration, and assignment rules mean that the binding will always preserve the same reference. const var var const The more constrained statements are, the simpler a piece of code becomes. As we add constraints to what a statement might mean, code becomes less unpredictable. This is one of the biggest reasons why statically typed programs are generally easier to read than dynamically typed ones. Static typing places a big constraint on the program writer, but it also places a big constraint on how the program can be interpreted, making its code easier to understand. With these arguments in mind, it is recommended that you use where possible, as it’s the statement that gives us the least possibilities to think about. const if (condition) {// can't access `isReady` before declaration is reachedconst isReady = true// `isReady` binding can't be reassigned}// can't access `isReady` outside of its containing block scope When isn’t an option, because the variable needs to be reassigned later, we may resort to a statement. Using carries all the benefits of , except that the variable can be reassigned. This may be necessary in order to increment a counter, flip a boolean flag, or to defer initialization. const let let const Consider the following example, where we take a number of megabytes and return a string such as . We’re using , as the values need to change if a condition is met. 1.2 GB let function prettySize (input) {let value = inputlet unit = `MB`if (value >= 1024) {value /= 1024unit = `GB`}if (value >= 1024) {value /= 1024unit = `TB`}return `${ value.toFixed(1) } ${ unit }`} Adding support for petabytes would involve a new branch before the statement. if return if (value >= 1024) {value /= 1024unit = `PB`} If we were looking to make easier to extend with new units, we could consider implementing a function that computes the and for any given and its current unit. We could then consume in to return the formatted string. prettySize toLargestUnit unit value input toLargestUnit prettySize The following code snippet implements such a function. It relies on a list of supported instead of using a new branch for each unit. When the input is at least and there’s larger units, we divide the input by and move to the next unit. Then we call with the updated values, which will continue recursively reducing the until it’s small enough or we reach the largest unit. units value 1024 1024 toLargestUnit value function toLargestUnit (value, unit = `MB`) {const units = [`MB`, `GB`, `TB`]const i = units.indexOf(unit)const nextUnit = units[i + 1]if (value >= 1024 && nextUnit) {return toLargestUnit(value / 1024, nextUnit)}return { value, unit }} Introducing petabyte support used to involve a new branch and repeating logic, but now it’s only a matter of adding the string at the end of the array. if PB units The function becomes concerned only with how to display the string, as it can offload its calculations to the function. This separation of concerns is also instrumental in producing more readable code. prettySize toLargestUnit function prettySize (input) {const { value, unit } = toLargestUnit(input)return `${ value.toFixed(1) } ${ unit }`} Whenever a piece of code has variables that need to be reassigned, we should spend a few minutes thinking about whether there’s a better pattern that could resolve the same problem without reassignment. This is not always possible, but it can be accomplished most of the time. Once you’ve arrived at a different solution, compare it to what you used to have. Make sure that code readability has actually improved and that the implementation is still correct. Unit tests can be instrumental in this regard, as they’ll ensure you don’t run into the same shortcomings twice. If the refactored piece of code seems worse in terms of readability or extensibility, carefully consider going back to the previous solution. Consider the following contrived example, where we use array concatenation to generate the array. Here, too, we could change from to by making a simple adjustment. result let const function makeCollection (size) {let result = []if (size > 0) {result = result.concat([1, 2])}if (size > 1) {result = result.concat([3, 4])}if (size > 2) {result = result.concat([5, 6])}return result}makeCollection(0) // <- []makeCollection(1) // <- [1, 2]makeCollection(2) // <- [1, 2, 3, 4]makeCollection(3) // <- [1, 2, 3, 4, 5, 6] We can replace the reassignment operations with , which accepts multiple values. If we had a dynamic list, we could use the spread operator to push as many as necessary. Array#push ...items function makeCollection (size) {const result = []if (size > 0) {result.push(1, 2)}if (size > 1) {result.push(3, 4)}if (size > 2) {result.push(5, 6)}return result}makeCollection(0) // <- []makeCollection(1) // <- [1, 2]makeCollection(2) // <- [1, 2, 3, 4]makeCollection(3) // <- [1, 2, 3, 4, 5, 6] When you do need to use , you should probably use instead, to keep it simpler. Array#concat [...result, 1, 2] The last case we’ll cover is one of refactoring. Sometimes, we write code like the next snippet, usually in the context of a larger function. let completionText = `in progress`if (completionPercent >= 85) {completionText = `almost done`} else if (completionPercent >= 70) {completionText = `reticulating splines`} In these cases, it makes sense to extract the logic into a pure function. This way we avoid the initialization complexity near the top of the larger function, while clustering all the logic about computing the completion text in one place. The following piece of code shows how we could extract the completion text logic into its own function. We can then move out of the way, making the code more linear in terms of readability. getCompletionText const completionText = getCompletionText(completionPercent)// ...function getCompletionText(progress) {if (progress >= 85) {return `almost done`}if (progress >= 70) {return `reticulating splines`}return `in progress`} What’s your stance in vs. vs. ? const let var This article was extracted from , a book I’m writing. It’s openly available online under format, and on GitHub as . It recently in funding and is available as an by the publisher, O’Reilly Media. Practical ES6 HTML AsciiDoc raised over $12,000 💰 on Indiegogo Early Release Originally published at ponyfoo.com .