Totally!
Let's take this, the equivalent of 1d6!
const getD6e = () => {
let acc=[];
while(acc.push(randomInteger(6)) && acc[acc.length-1]===6) /* {} */; // empty body
return acc;
};
First off, this is defining the function getD6e(). It's using the
const declaration type (see the excruciating discussion below!) because the value of getD6e is never going to get reassigned, it will always be this function.
() => {
/* ... */
};
Means this is a function taking no arguments.
let acc=[];
This declares an array that we'll use for accumulation. I'm using
let here, but this could (and probably should) be
const as the acc variable will always point to this array, and we will be changing the contents of the array but not the actual array object, so it would be fine.
while(acc.push(randomInteger(6)) && acc[acc.length-1]===6);
This is where all the work is happening. Normally, it's a good idea not to use loop constructs without a block because it can be confusing. However, sometimes it just seems like the most elegant solution, so that's what I did here. Loops execute the next instruction for each iteration they perform. That instruction is usually a block denoted by { }, which is a collection of instructions, but is sometimes a single instruction (which I never do because it can too easily cause issues), or it can be and empty instruction denoted by a tailing ;
This line could be expanded to this equivalent form:
do {
acc.push(randomInteger(6));
} while(acc[acc.length-1]===6)
this will:
1) Add a random integer from 1-6 to the end of the acc array using the push(value) method.
Array.push(value) adds the value to the end and returns the number of items now in the array.
2) Check if the last value in the array, the one just pushed, is a 6. Since arrays are zero-biased, i.e indexed starting at 0, the last item in the array will have the index (number of items - 1), so acc[acc.length-1] is the last item. If it is a 6, it will return to step 1 and push on another value.
Since Array.push() returns the number of items in the array, it will always be greater than 1, which means it will always evaluate to the truthy value true.
&& is the boolean AND operator, which evaluates to true if both sides of the operator are true. That means that this expression:
acc.push(randomInteger(6)) && acc[acc.length-1]===6
Will always be true on the left side (push returns 1 or more) and will be true on the right side if the last value pushed was a 6. The practical upshot of all that is that the one line while statement:
while(acc.push(randomInteger(6)) && acc[acc.length-1]===6);
Keeps adding random numbers from 1-6 until the last thing added wasn't a 6. This does the exploding.
The last line just returns the accumulated values in acc:
return acc;
Excruciating Detail, round 1!
Aside: var vs let vs const
These all define variables,
var is the old way. It creates a function scope variable which can be changed anywhere in the function and is "hoisted", meaning it's considered to be defined from the beginning of the function scope. So:
function d6rlt3(){
while(!bar || bar<3) {
var bar = randomInteger(6);
}
return bar;
}
Even though bar is declared inside the while(){ }, it's declaration is "hoisted" to the top of the function scope. The first time the while condition bar<3 is checked, the value of bar is undefined, because it has yet to be assigned. This might not look too weird to you if you've never programmed in something besides javascript, but it is actually quite weird and the source of some strange problems, particularly when the same variable is used across many blocks of code in a function.
Contrast that with
let, which is block scoped and not hoisted. This would give you several errors:
function d6rlt3(){
while(!bar || bar<3) {
let bar = randomInteger(6);
}
return bar;
}
First, it would tell you "ReferenceError: bar is not defined" for that while condition of bar<3, if you redefined it to be this:
function d6rlt3(){
let valid=false;
while(!valid) {
let bar = randomInteger(6);
valid = (bar>3);
}
return bar;
}
It would tell you "ReferenceError: bar is not defined" for the return statement. This all seems like a bunch of messing about for nothing new, but eventually, you land on something like this:
function d6rlt3(){
let bar;
do {
bar = randomInteger(6);
} while(bar<3);
return bar;
}
which is better from a variable and scope point of view (but slightly weird because contrived examples are hard).
Finally
const is block scoped and not "hoisted" just like
let, but it's value can only be assigned once. This lets the interpreter do nice things with it behind the scenes and also prevents you from accidentally changing something you meant to compare to and the like. This makes const ideally suited to declaring functions. The above rewritten with const would break outright because you can't declare a
const without assigning it a value (defining):
function d6rlt3(){
const bar;
do {
bar = randomInteger(6);
} while(bar<3);
return bar;
}
This results in "SyntaxError: Missing initializer in const declaration". Adding an initialization to 0 then goes to the next issue:
function d6rlt3(){
const bar = 0;
do {
bar = randomInteger(6);
} while(bar<3);
return bar;
}
which is: "TypeError: Assignment to constant variable." when you try to assign the result of randomInteger(6) to bar.
While slightly tedious if you're used to the freedom of
var, this actually protects you in many ways from all sorts of problems. It's a good habit to get in to always use
const when declaring variables by default, then consciously make the decision to relax to using
let when you need to change the value at a later point. And of course, to never use
var ever again. =D