Hamburger Icon
  • Labs icon Lab
  • Core Tech
Labs

Guided: Understanding Variables and Data Types in JavaScript

In this lab, without any prior knowledge of JavaScript, you’ll create a recipe ingredient converter from scratch where recipe data can be held and modified depending on the number of servings to make. Along the way, you’ll learn how to use variables of different data types, organize data with collections (Arrays, Sets, and Maps), and reuse code by writing your first functions.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 1h 47m
Published
Clock icon Apr 26, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    This lab will guide your exploration of various data types and collections in JavaScript, assuming minimal prior programming experience. You'll see some of the pros and cons of representing data in different ways, and discover which way could be suitable for a particular use case: Creating a recipe ingredient converter. To cap off the lab, you'll write two functions as a first step toward code maintainability and coding efficiency.

    Some notes:

    • You'll do all your coding in the file main.js below the first line of code, which imports some utilities required for this lab.
    • If you get stuck on a task, you can consult the files within your solutions folder.

    As you make changes, you'll want to see how they affect your program. To see your program's output at any time, type node src/main.js at the command prompt in the Terminal pane, and hit Enter. (For subsequent runs, you can simply press the up arrow to recall the previous command so you don't have to type it out again.) The output will appear in the terminal pane. If you try it now, before writing code as described in the tasks, there won't be any output yet.

  2. Challenge

    Primitive Variables

    Variables and data types are fundamental to using JavaScript.  This lab will get you comfortable with some of the most common ones.

    A variable has a name and stores a value.  That value may be simple, like a number, or it may be complicated, like a structure holding other variables.  The simple ones are called primitive value types, and often the most useful ones are numbers, strings, and booleans.

    Declaring variables

    You can create (declare) variables using the keyword var followed by what you want to name your variable, like this:

    var myIngredient = "water";
    

    Here, the = "water" provides an initial value for myIngredient to hold, and that value is a string.  JavaScript knows it's a string — and you're able to know this too — because the value is surrounded by quotation marks.  (Apostrophes work too, but you need to use quotes to pass the checks in this lab.) The final semicolon (;) simply ends the statement, like a period does in English.

    You can use let similarly:

    let myOtherIngredient = "lemon juice concentrate";
    

    The biggest difference:

    • var is function-scoped, visible throughout the innermost function containing the declaration
    • let is block-scoped, visible only within the innermost pair of curly braces ({}) containing the declaration

    You'll learn more about the insides of functions and see some blocks later in this lab, but there are several other nuances between var and let that are too advanced to cover here. So for simplicity, in this lab you're expected to always use let, which is often the better choice anyway.

    Throughout this lab, you can choose variable names as you see fit, but their values must be as requested in order to pass this lab's tasks.

    Calling functions

    Functions consist of reusable code. They can be run multiple times, or once, or never — although, there's not much point in writing functions that are never used.

    You run (or invoke or call) a function by putting () after its name. Some functions use parameters, which are values you can (or sometimes are required to) put inside the () for the function to use.

    For example, console.log is a function, and calling it with console.log("hello"); will output hello to the terminal. Here, the code passes one parameter (the string "hello") to console.log when calling it. (If you're wondering about the . in console.log, it's because the log function is being accessed as part of the console object.)

    If a function takes multiple parameters, it's common that it will expect them in a particular order. So myfunction(myIngredient, myQuantity) won't work the same as myfunction(myQuantity, myIngredient). Note how parameters are separated by commas.

    In this lab, you'll make use of a function called templates.displayFirstIngredient provided by the lab. It formats the data you pass to it and outputs it to the terminal. It takes two parameters: first, an ingredient name, and second, a quantity. ## About the provided function

    Displaying your data in templates.displayFirstIngredient works something like this:

    console.log(`
    My Recipe
    ---------
    
    Ingredients (for 1 serving):
    
    - ${ingredientQuantity} ${ingredientName}
    `);
    
    1. There is only one statement here, a console.log call.
    2. In this case, there's a special, multiline string in between the (). The fact that it's surrounded with backticks (`), rather than quotes (") or apostrophes ('), allows it to be written cleanly over multiple lines. It also allows the incorporation of the values of variables by putting their names inside ${}.

    See if you can map how it's written in code to how the output looks in the terminal.

  3. Challenge

    Arrays

    You could continue with the same approach to declaring variables as your recipe grows. But you'll soon have a lot of similar variables — perhaps ingredient1Name, ingredient2Name, and so on. And you'll have to change your output code every time you add an ingredient. That's not only inefficient, but also insufficient: In a real-world program, your users would add ingredients while the program is running (at run time), not while you're programming it.

    One solution for holding multiple variables of similar meaning — several ingredient names, for instance — is to use arrays. An array variable has one name and many values.

    You create arrays by surrounding a comma-separated list of values with square brackets ([]):

    let myEmptyArray = [];
    let aSingleValuedArray = ["water"];
    let thisArrayHasTwoValues = ["water", "lemon juice concentrate"];
    let thisOneHasThree = [2, "three", 5.0];
    ``` ## Limitations
    
    Note that one problem with this approach is that duplicate ingredients are allowed.  For example, you (or in a fancier program, your users) could add `"water"` ten times to `ingredientNames` and `"1 cup"` ten times to `ingredientQuantities`.  (Try it out!)  It's a type of human error that a different structure could disallow, perhaps by raising an error if there are duplicates, or by automatically deduplicating.
    
    ## About the provided function
    
    `templates.displaySiloedArrays` is more complicated inside because it needs to handle arrays.  It prepares a formatted list in a variable called `ingredientList`, which is later used in `console.log()`.  Here's how the preparation looks:
    
    

    let ingredientList = ''; for (let i = 0; i < ingredientNames.length; i++) { let ingredientName = ingredientNames[i]; let ingredientQuantity = ingredientQuantities[i]; ingredientList += - ${ingredientQuantity} ${ingredientName}\n; }

    
    You know what `let ingredientList = '';` does, but the next line is more complicated.
    
    1. First, `for` means to _loop_, or run the same code — whatever is between the curly braces (`{}`) — repeatedly.
    2. The next three statements, together in parentheses, let us describe how the repetition works:
        1. `let i = 0;` sets up a number, `i`, to keep track of how many iterations (repetitions) have been done.
        2.  `i < ingredientNames.length;` is the condition that's checked just before each repetition.
            1. If it's a true statement, the code will run again.
            2. If not, the `for` loop is done, and the program will continue running whatever code comes after the closing `}`.  (In this case, the only thing after the `}` in the file is the `console.log()` statement.)
        3.  The last statement gets run after each iteration of the loop.  In this case, `i++` increases the counter `i` by one.
    
    So when `ingredientNames` holds two values — i.e., `ingredientNames.length` is `2` — this means `i` starts off as `0`, the code loops, then `i` is `1`, it loops a second time, and then `i` is `2`.  At this point, `i < ingredientNames.length` is false (because `2` is not less than `2`) so the loop ends, having run twice in total.
    
    What about the code in the `{}`?  This is where the list is built:
    
    1. `let ingredientName = ingredientNames[i];` gets a single value from `ingredientNames` and stores it in `ingredientName`.  The values of `ingredientNames` are numbered (_indexed_), starting at `0`, and `[i]` indicates which value.  For example, when `i` is `0`, the first time through the loop, `ingredientNames[i]` is `ingredientNames[0]` which is the first value, `"water"`.
    1. Similarly, the corresponding value from `ingredientQuantities` is stored in `ingredientQuantity` in the next line.
    1. `ingredientList +=` means to take the string value of `ingredientList` and append whatever comes next.  In this case, it's `- ${ingredientQuantity} ${ingredientName}\n`, which can be found verbatim in the previous task, except for the fact that it ends with `\n`.  In a string, `\n` is a special _escape sequence_ that gets interpreted as a newline character — the equivalent of pressing Enter.
    
    In other words, each time this code loops, it adds to `ingredientList` in the same format used for the single ingredient in the previous task: hyphen, space, quantity, space, name, newline.  But it's done so it works within a loop using the values at index `i` in each of the two arrays.
  4. Challenge

    Sets

    Arrays aren't the only type of variable that can hold multiple variables within them. Another option is the Set.

    Sets and arrays come with different feature sets — neither is inherently superior — but arrays are used much more often in the real world. However, a Set ignores duplicate values, which might help your users to avoid accidentally adding incorrect data.

    To create a Set, you use a constructor after the keyword new, like this:

    let myNiftySet = new Set();
    

    Note the () after Set — they're not technically required when they're empty like this, but they are required in the form you'll see next.

    Sets are so similar to arrays that the cleanest way of declaring a Set is to give its constructor an array:

    let myNiftySet = new Set([1, 2, 3, 2]);
    

    In this case, the duplicate 2 would be ignored, so myNiftySet would contain only 1, 2, and 3. ## Limitations

    Real recipes often have two ingredients with the same quantity (e.g., 1 cup of beans and 1 cup of rice). Unfortunately, having a Set just for quantities doesn't let you support this use case, because you can't store "1 cup" twice in your quantities Set. Automatic deduplication seemed like a good way to avoid a potential issue, but in this case, it created a real issue instead.

    Also, in this task and the previous one, you were asked to create two multi-value variables. You can think of them as silos, because they're independent: One variable holds all the names, the other holds all the quantities. This opens the door for bugs, though, because this siloed data can get out of sync.

    For example, try the beans-and-rice recipe here and you'll see incorrect output, since your names Set would contain two values, and your quantities Set would only contain one ("1 cup").

    But this can happen with siloed arrays, too: If you were to write some code to add or delete an ingredient, you might forget to modify both arrays in the same way, and end up with names and quantities that don't match.

    About the provided function

    The difference in templates.displaySiloedSets is that the Set data is converted back to array format at the start of the for loop:

    let ingredientNames = Array.from(ingredientNamesSet);
    let ingredientQuantities = Array.from(ingredientQuantitiesSet);
    

    It's not the only way to accomplish this, but because of the siloed data, this approach makes it easier to access the same index in both variables at the same time.

  5. Challenge

    Objects and Maps

    To have the structure of your data match the recipe you're trying to represent, it makes more sense to use something that associates an ingredient name with a quantity. This way the structure itself goes a long way toward keeping names and quantities in sync.

    In JavaScript, an object is one such structure. It's like a group of variables, but:

    1. The variable names are called keys.
    2. When providing initial key-value pairs during declaration, instead of =, you use :.
    3. You can change how many key-value pairs you have at run time.

    Like a Set, duplicate keys are impossible in an object. Setting the value of a key a second time just overwrites its earlier value.

    Like an array, JavaScript can see that a value is an object just by the syntax. You can tell an array because it's surrounded by []; likewise, you can tell an object by the {}:

    let myNiftyObject = {
        myFirstKey: "my first value",
        mySecondKey: "my second value",
    };
    

    (The comma after "my second value" is optional, but it's good form to include it, because you can add or remove lines without having to think about applying different rules to different lines within your {} — they simply all end in a comma.)

    Careful with [] versus {}, though — [] also has a special, non-array meaning when used a certain way with an object. Namely, putting [] just after an object variable name lets you get or set the value of a key, even if you only know the key at run time:

    let whichKey = "mySecondKey";
    myNiftyObject[whichKey] = "my new second value";
    

    If you do know the key before run time, you can also use . to access its associated value, but only if it doesn't have any spaces, e.g. myNiftyObject.myFirstKey. If the key does have spaces, you can still use the [] form from above, using a string instead of a variable, e.g., myRecipe["lemon juice"]. ## Limitations

    Note that the order of the list is not necessarily always preserved in older JavaScript engines that don't implement at least the ECMAScript 2020 specification (ES2020) — but any engine currently in long-term support will work fine.

    You may also encounter difficulty if you happen to need keys that are the same name as a built-in Object property, like constructor. Workarounds for this and other limitations are beyond the scope of this lab, though.

    About the provided function

    The code in templates.displayIngredientsObject is simpler than in the previous two tasks, showing how object syntax can be nice to work with. The new lines were:

    for (let ingredientName in ingredients) {
        let ingredientQuantity = ingredients[ingredientName];
    

    Here you can see a different type of for loop, particular to objects. It uses for...in syntax. In each loop iteration, JavaScript sets ingredientName to one of the keys of ingredients.

    Then the first statement in the loop itself gets the associated value using the [] syntax discussed above, and sets ingredientQuantity to that value. After that, everything is prepared to use the same formatting code as previous tasks to build the list.

    Maps vs. objects

    Maps and objects are conceptually similar, but their syntax and features differ. To change your approach to a Map that associates ingredient names with quantities, you'll use a syntax that may seem like a combination of your Set and object solutions. You might expect to be able to convert an object to a Map in a similar way to how you converted an array to a Set, with new Map({ followed by your key-value pairs followed by }). The new Map() part is correct, but despite the similarity between Map and object, you can't convert an object directly this way.

    It may seem odd, but to provide an initial value to convert into a Map, you can use an array of arrays like this:

    let myNiftyMap = new Map([
        ["first map key", "value for first map key"],
        ["second map key", "value for second map key"],
    ]);
    ``` ## Limitations
    
    Beware that the object ways of getting and setting values (e.g., `myRecipe.water = "1 cup"` and `myRecipe["lemon juice concentrate"] = "1.5 tbsp"`) will _seem_ to work with `Map` to some extent, but that doing so uses `Map` incorrectly and breaks the functionality of standard `Map` functions (like `get`, `size`, and `has`).  This is fairly insidious, so it's worth paying special attention to syntax when working with `Map`s.
    
    ## About the provided function
    
    While objects are more common than `Map`s, the code in `templates.displayIngredientsSimpleMap` is even simpler than it was with the object solution.
    
    There's just one new line, taking the place of two lines in the previous task:
    
    

    for (let [ingredientName, ingredientQuantity] of ingredients) {

    
    First, note that this uses yet another type of `for` loop.  With an object you can use `for...in` syntax, but with a `Map`, you use `for...of`.
    
    The other difference is that this line uses _destructuring_.  JavaScript hands us each key-value pair in the `Map` as a two-value array, just like the ones you used to construct the `Map` in the first place in your solution.  The nice part of the syntax here is that by writing `[ingredientName, ingredientQuantity]`, JavaScript takes that two-value array and assigns its first value to a variable called `ingredientName` and its second value to one called `ingredientQuantity`, for each iteration of the loop.  Like this, the exact same variables are available for the list-building code, so the code to append to `ingredientList` remains as it was in all the previous tasks.
    
    ## A better structure for quantities
    
    In all the previous tasks, the way you've represented quantities — as a string like `"1 cup"` — means that they can't be easily multiplied because they're strings and not numbers.
    
    To address this, you'll need to improve your data structure by representing the two parts of each quantity (the number and the unit of measurement) separately, using an object having two keys, `quantity` and `unitOfMeasurement`, like this:
    
    

    { quantity: 1, unitOfMeasurement: "cup(s)"}

    
    You'll use an object like this as the value in each key-value pair of your `Map`, as a drop-in replacement for the simpler string values like `"1 cup"` you used previously.
    
    Please note:
    
    1. Earlier, you were told that you can choose variable names as you see fit.  In this case, `quantity` and `unitOfMeasurement` are _not_ variable names — they're keys, and therefore they're data, and therefore they must be spelled and ordered as shown in order to pass the remaining task checks.
    1. Since your ultimate goal is to be able to multiply the quantities, the units of measurement have ambiguous plurality (`"cup(s)"`) so that they'll make grammatical sense no matter what the number part of the quantity is.  (Also note that "tbsp" is a valid plural already, i.e., "1 tbsp" and "2 tbsp" are correct, so no "(s)" is needed.) ## About the provided function
    
    `templates.displayIngredientsMap` is a bit more complicated to accommodate your new data structure, but it doesn't use any syntax you haven't seen earlier:
    
    

    for (let [ingredientName, ingredientQuantityObject] of ingredients) { let ingredientQuantity = ${ingredientQuantityObject.quantity} ${ingredientQuantityObject.unitOfMeasurement};

    
    The `for...of` syntax for `Map`s is used again, but this time, the value in the key-value pair is an object.  The first statement within the loop then uses that object's data to reconstruct a quantity string in the format you've seen in all the previous steps so that the rest of the display code can remain the same.
    
    Another way this could be done is to destructure both the array and the object within it, by naming the keys the object will have:
    
    

    for (let [ingredientName, {quantity, unitOfMeasurement}] of ingredients) { ingredientList += - ${quantity} ${unitOfMeasurement} ${ingredientName}\n;
    }

  6. Challenge

    Functions

    Now that you have a more sophisticated data structure in place, you're ready to add code to multiply your recipe quantities.

    But mind the capitalization: You're about to encounter a set that's completely unrelated to the Set data type you saw earlier. In this case, set is a function you'll call on your ingredients variable (i.e., ingredients.set(), assuming ingredients is the name of your Map variable) to provide new values for it.

    You can write ingredients.set because ingredients, like all Maps, comes with the set function, just like console has its log function. Using ingredients.set(), you can associate a new object with a given ingredient name.

    Map.set takes two parameters: first, the name of the key, and second, the value to set it to. For example, if you wanted to triple only the quantity of water in the recipe, you could write:

        ingredients.set("water", {
            quantity: ingredients.get("water").quantity * 3,
            unitOfMeasurement: ingredients.get("water").unitOfMeasurement,
        });
    

    You can see that ingredients.get("water") is called twice here. The Map.get function returns a value (something that neither console.log nor Map.set do). This means that the expression ingredients.get("water") gets replaced with a value — in this case, { quantity: 1, unitOfMeasurement: "cup(s)"}, which is the object associated with the "water" key. From there, the code above retrieves the values associated in the returned object using .quantity and .unitOfMeasurement.

    So that's how you would associate a new value with just the "water" key. But you'll want to work on all key-value pairs to triple the whole recipe. ## A more direct way

    For this task, you were told to use Map.set and create a whole new ingredient quantity object to associate with each key. You can accomplish the same goal, but instead tripling quantity in place, with this simple line:

        ingredientQuantityObject.quantity *= 3;
    

    Wait, weren't you warned earlier about using this form (.key =) with Maps? Yes, and the line above does not stray from that advice. That's because it's not setting the value associated with a Map key (e.g. ingredientName), it's setting the value associated with an object key (quantity).

    However, using Map.set is closer to what's needed for the next task, so the approach in this task has prepared you to more easily build on your solution.

    About the provided function

    You use the same function, but pass it 3 as a second parameter — it handles this by changing the output from 1 serving to 3 servings. The s is added on the end using logic that checks whether the number of servings equals 1 or not.

    (It's a more sophisticated way of dealing with plurals than appending nouns with (s) like you did with "cup(s)", but it's beyond the scope of this lab to explain the inner workings here. The reason it's not used for the ingredients themselves is that it becomes quite a bit more complicated if you don't know the noun beforehand: As you saw with cup(s) and tbsp, it's not always appropriate to add an s. Other classes of examples include box(es) and ox(en).)

    Reusing code

    The for loop from your previous task can be turned into generic, reusable code, by putting it inside a function.

    In general, a function lets you take more specific code and reuse it. Take this code, for example:

    let five = 5;
    let tripleFive = five * 3;
    

    The act of tripling a number can be generalized, so that you're not limited to tripling five but can triple any given number. The "given number" simply becomes a parameter of a function, and the result is the function's return value:

    function triple(numberToTriple) {
        return numberToTriple * 3;
    }
    

    Note that the function took the original, overly specific logic (five * 3) and made it work more generally as numberToTriple * 3. The function then returns the value by having it immediately follow the return keyword.

    Also, note that the code isn't run until the function is called. Defining the function, as above, simply prepares it to be run.

    However, once the function is defined, you can call it as many times (and with as many different numbers) as you like, rather than having to copy and paste code to reuse its logic:

    let tripleFive = triple(5);
    let tripleSeven = triple(7);
    let tripleEleven = triple(11);
    let tripleUserProvidedNumber = triple(userProvidedNumber);
    

    In the case of your project, creating a function will prepare your code for reuse in the final task.

    The function you need to make will take two parameters (how many servings and a Map of ingredients) and it will return an object containing the serving count and a new Map, ingredientsForXServings, with appropriately multiplied quantities:

    function multipliedRecipe(servingCount, ingredientsForOneServing) {
        
        return { servingCount, ingredientsForXServings };
    }
    

    You might have noticed two new things:

    1. The object you're returning is anonymous. You know it's an object because of the {}, but there's no variable declared to hold it. It's ad-hoc.
    2. There's no : or value with servingCount. This is shorthand for servingCount: servingCount, i.e., you're saying the object should have a key called "servingCount" and its associated value should be the value of the servingCount parameter that was passed to the function.
    3. The same goes for ingredientsForXServings, except that the value won't come from a function parameter, it will come from a variable you'll have to declare within the function.

    Now, all that's missing from the function you'll create is the declaration of ingredientsForXServings and the code to populate it with the correct data, by turning your for loop into generic, reusable code. ## About the provided function

    templates.displayRecipeObject is quite similar to before. In fact, it simply calls the previous task's function, passing it the data it finds in the recipe object. It's the same as writing this:

    templates.displayIngredientsMap(recipe.ingredientsForXServings, recipe.servingCount);
    

    templates.displayRecipeObject is used identically in the next task.

    Combining lines

    In the task above, you called multipliedRecipe and stored the result in a variable called recipeForThree. Then you passed this variable to displayRecipe. But these two steps can be combined. In the next task, you'll skip this storage step, and just use the result of your multipliedRecipe call as the value you pass directly to displayRecipe:

        templates.displayRecipeObject(multipliedRecipe(servingCount, ingredients));
    

    This will be done inside a wrapper function — one that conveniently wraps up the functionality of the multiplying and displaying functions. The recipe data will also be wrapped up inside the function.

Kevin has 25+ years in full-stack development. Now he's focused on PostgreSQL and JavaScript. He's also used Haxe to create indie games, after a long history in desktop apps and Perl back ends.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.