Object Methods and ES6 Modules

Introduction

We are going to recap what we’ve learnt about Objects as well as dive a little deeper.

An object is one of the JavaScript data types. It allows you to store data using “key/value pairs”.

javascript
	const person = {
	  firstName: 'Ola',
	  lastName: 'Nordmann',
	  module: 'JavaScript'
	};

To access a value from the object, we use a key. There are two ways we can use the key to access our values:

  1. Dot notation
  2. Bracket notation

1. Dot Notation

With dot notation, we put a . “dot” (also known as a full stop or period) after the object name and then the key we want to use.

javascript
	const name = person.firstName;
	
	console.log(name);
	// Returns:
	// Ola

2. Bracket Notation

With bracket notation, we put the key, as a string with quotation marks, in square brackets right after the Object’s name.

javascript
	const name = person['firstName'];
	
	console.log(name);
	// Returns:
	// Ola

It would help if you usually used dot notation to access the values in your objects. However, bracket notation is still critical to know because it lets us dynamically access values in our Objects. This is because we can generate our keys with variables and string concatenation (adding of strings together).

In this example, we have multiple keys that are the same except for a number at the end. We create our key by combining the string ingredient_ with a number e.g. "ingredient_" + 1.

javascript
	const recipe = {
	  ingredient_0: 'Egg',
	  ingredient_1: 'Milk',
	  ingredient_2: 'Flour'
	};
	
	const ingredient0 = recipe['ingredient_' + 0];
	const ingredient1 = recipe['ingredient_' + 1];
	const ingredient2 = recipe['ingredient_' + 2];
	
	console.log(ingredient0);
	console.log(ingredient1);
	console.log(ingredient2);
	
	// Returns:
	// Egg
	// Milk
	// Flour

Let’s do another example with the same data as the last example. This time, we will use a for loop and the index from the loop. We will use this index to automatically increment the number in the Object key we are generating.

javascript
	const recipe = {
	  ingredient_0: 'Egg',
	  ingredient_1: 'Milk',
	  ingredient_2: 'Flour'
	};
	
	for (let index = 0; index <= 2; index++) {
	  // We use
	  const ingredient = recipe['ingredient_' + index];
	  console.log(ingredient);
	}
	
	// Returns:
	// Egg
	// Milk
	// Flour

Looping with an object

An array is a list of elements that are in a specific order. In an array, we can access an element at a certain position by using an index.

An object is not a list like an array. The keys in an object are not in any specific order, so we cannot simply use a for loop with an index to loop through key/value pairs in an object.

There are other ways to loop through the values in an object.

for...in loop

The for...in loop used with an object will iterate over the keys of the object.

javascript
	for (const myKey in myObject) {
	  // We are looping through the keys of the object. To get the value you
	  //    need to use the key (myKey) as a way to reference the key/value pair.
	  console.log(myObject[myKey]);
	}

The following code is a more practical example of the for...in loop with an object. Note how we are using the key as an accessor in the object so that we can get the value.

javascript
	const person = {
	  firstName: 'Ola',
	  lastName: 'Nordmann',
	  module: 'JavaScript'
	};
	
	for (const key in person) {
	  // Use the key to access the value from the key/value pair
	  console.log('key:', key, 'value:', person[key]);
	}
	// Returns:
	// key: firstName value: Ola
	// key: lastName value: Nordmann
	// key: module value: JavaScript

Object Methods

Object.keys()

The object Object.keys() method returns an array containing all of the keys that are in that object.

The syntax is fairly basic. Object is an actual keyword you need to use, giving us access to the .keys method. You then pass in the object as the parameter. In the example below, we are passing in the myObject object.

javascript
	const myObject = {
	  // Key/value pairs here
	};
	
	Object.keys(myObject);

Let’s have a look at the previous example we did with for...in. You can see that we set keysArray to the result of Object.keys(person). The end result is then an array of keys from the object.

javascript
	const person = {
	  firstName: 'Ola',
	  lastName: 'Nordmann',
	  module: 'JavaScript'
	};
	
	const keysArray = Object.keys(person);
	
	console.log(keysArray);
	// Returns:
	// ['firstName', 'lastName', 'module']

The array being returned from Object.keys() is actually of significance because it means that we can now use our array methods, such as map(), filter() and reduce().

forEach() example

In this example we will use the forEach array method to loop through the keys of the object. The forEach method is for when we simply want to loop and don’t need a new array being returned.

javascript
	const person = {
	  firstName: 'Ola',
	  lastName: 'Nordmann',
	  module: 'JavaScript'
	};
	
	Object.keys(person).forEach((key) => {
	  console.log(`key: ${key}, value: ${person[key]}`);
	});
	
	// Logs:
	// key: firstName, value: Ola
	// key: lastName, value: Nordmann
	// key: module, value: JavaScript

map() example

In this example, we use map to create a new array. Each element in the array will contain the key and value for each of the key/value pairs.

javascript
	const person = {
	  firstName: 'Ola',
	  lastName: 'Nordmann',
	  module: 'JavaScript'
	};
	
	// .map will return a new array that contains the same number
	// of elements as there were in the initial array
	const keysAndValues = Object.keys(person).map((key) => {
	  return `key: ${key}, value: ${person[key]}`;
	});
	
	console.log(keysAndValues);
	// Logs:
	// 0: "key: firstName, value: Ola"
	// 1: "key: lastName, value: Nordmann"
	// 2: "key: module, value: JavaScript"

Object.values()

The Object.values() method will create an array of only the values from the object. It can be seen as the opposite of the Object.keys() object method which creates an array of the keys in the object.

javascript
	const person = {
	  firstName: 'Ola',
	  lastName: 'Nordmann',
	  module: 'JavaScript'
	};
	
	console.log(Object.values(person));
	// Returns:
	// ['Ola', 'Nordmann', 'JavaScript']

Object.entries()

Object.entries() returns an array containing both the key and value.

javascript
	const person = {
	  firstName: 'Ola',
	  lastName: 'Nordmann',
	  module: 'JavaScript'
	};
	
	Object.entries(person).forEach((keyValuePair) => {
	  console.log(keyValuePair);
	});
	// Returns:
	// ['firstName', 'Ola']
	// ['lastName', 'Nordmann']
	// ['module', 'JavaScript']

We can use array destructuring for the key/value pair as the value returned will always be the key and the value. Note how we do not have to access the value using the key as we already have the value given to us as index 1 of the array being returned:

javascript
	const person = {
	  firstName: 'Ola',
	  lastName: 'Nordmann',
	  module: 'JavaScript'
	};
	
	Object.entries(person).forEach(([key, value]) => {
	  console.log('key:', key, 'value:', value);
	});
	// Returns:
	// key: firstName value: Ola
	// key: lastName value: Nordmann
	// key: module value: JavaScript

ES6 Modules

Introduction to ES6 Modules

In the earlier days of web development, JavaScript usage on websites was minimal, primarily addressing minor functionalities. This scenario involved handling smaller JavaScript code blocks, often lacking complexity.

However, the landscape of web development has evolved significantly. Presently, we encounter web applications entirely crafted using JavaScript, necessitating a structured approach to managing and maintaining code. This shift led to the critical need for code modularity.

Initially, modules were a feature exclusive to environments like Node.js. Yet, with advancements in web technologies, modern browsers have embraced native support for modules. This pivotal change was ushered in by the introduction of ES6 modules.

ES6 modules revolutionize the way we handle JavaScript code. They enable developers to seamlessly import and export code elements, such as variables and functions, using import and export keywords. This feature greatly enhances code organization and reusability.

Here’s a straightforward illustration of employing ES6 modules through import and export:

javascript
	// Importing the 'addNumbers' function from a module
	import { addNumbers } from './math.mjs';
	
	// Using the imported function
	const result = addNumbers(10, 10);
	
	// Exporting the result for use in other modules
	export { result };

This example encapsulates the essence of ES6 modules, showcasing how they facilitate code segmentation and collaboration in modern JavaScript development.

Setup

Before we begin to use modules, we need to look at some minor setup that needs to be done.

Naming of module files

It’s the convention to use the .mjs extension for JavaScript module files instead of the .js extension e.g.

bash
	myModule.mjs

It is not mandatory to name a module file with the .mjs extension, but it is becoming common practice and is recommended. This is because:

  1. It lets developers know that the file they are working with is a module instead of a normal script.

  2. It ensures the file is correctly parsed as modules when tools are used, such as Babel.

Importing of Module files in a browser

To make use of a module in a browser, you need to add type="module" when importing the JavaScript module file.

Below is how we would usually import a normal JavaScript file:

html
	<script src="myFile.js"></script>

Below is how we would import a JavaScript module.

html
	<script type="module" src="myModule.mjs"></script>

If you tried to use import or export and did not specify type="module", you would likely get the following error:

txt
	Uncaught SyntaxError: Cannot use import statement outside a module (at main.mjs:1:1)

title: Modules basics keywords: modules tags: JavaScript 2 sidebar: javascript-2 permalink: javascript-2/basics.html folder: javascript-2


Imports and exports

import and export keywords are the way you will be able to use modules throughout your code.

When we use import we will be importing code from a module so we can put the code in another file. This code can be functions or variables. This code has to be exported from another file; otherwise, we won’t be able to import it.

When we use export we will be exporting code from a module so that it can be imported into other files using the import keyword.

Named and default imports/exports

There are two main ways we can use the import and export keywords, which are “named imports/exports” and “default imports/exports”.

We are going to look at the two different types below:

Named imports/exports

Named imports/exports allow us to import and export specific portions of our code.

For example, we might have three functions in a module, but we only want to expose two of them to other developers. In this case, we would then only export the two functions, e.g.

javascript
	// module.mjs
	function functionOne() {
	  // code here
	}
	
	function functionTwo() {
	  // code here
	}
	
	function functionThree() {
	  // code here
	}
	
	// We only export functionOne and functionTwo
	export { functionOne, functionTwo };

When we want to import these functions into another file, we will use the following code.

javascript
	// main.mjs
	import {functionOne, functionTwo} from './module.mjs';

If we tried to import functionThree like this, then it will throw an error:

javascript
	import {functionOne, functionTwo, functionThree};

The error will look as follows:

txt
	Uncaught SyntaxError: The requested module './module.mjs' does not provide an export named 'functionThree' (at main.mjs:1:36)

This is because we didn’t export functionThree from the module.mjs file.


Aliasing

When using “named imports/exports”, we have to use the name of the function or variable we are importing. If we use a named export to export the function named functionOne, we need to import it as this name. This is why it’s called a “named import/export”.

There is a way around this, though, by using “aliases”. This lets us change the named import. We do this by adding as after the variable name and then the name you want to use.

In the example below, we alias functionOne to have the name newFunctionName:

NOTE: When you use an alias, you can’t use the original name where you’ve added the alias.

javascript
	import { functionOne as newFunctionName } from './module.mjs';
	
	// ✅ This works
	newFunctionName();
	
	// ❌ This won't work as we have aliased 'functionOne' to be 'newFunctionName'.
	functionOne();

Default imports/exports

The other way we can import/export is using “default imports/exports”.

Default imports/exports involve exporting code using the default keyword after the export keyword, e.g.

javascript
	function mainFunction() {
	  // code here
	}
	
	export default mainFunction;

When we import a “default export”, we don’t use the {} braces, we simply write the name we want to use:

javascript
	import mainFunction from './module.mjs';

Default imports can be named anything

You can name your default import whatever you would like. You do not have to use the same name like you do with “named imports/exports”.

In the example below, we default export a function functionOne but we import it as newFunctionName:

javascript
	// module.mjs
	
	function functionOne() {
	  // code here
	}
	
	// We are using a default export, not a named export
	// You can tell by the 'default' keyword and lack of
	// {} curly braces
	default export functionOne;
javascript
	// main.mjs
	
	// Here we import the default export from 'module.mjs'
	// and called it 'newFunctionName'
	import newFunctionName from './module.mjs';

Combining named exports/imports with default exports/imports

You can combine named and default imports/exports, which is not uncommon.

We are using both named and default imports/exports in the example below:

javascript
	// module.mjs
	
	function functionOne() {
	  // code here
	}
	
	function mainFunction() {
	  // code here
	}
	
	// Named export
	export { functionOne };
	
	// Default export
	export default mainFunction;
javascript
	// main.mjs
	
	// Here we are using the default import (lack of curly braces)
	// as well as the named import { functionOne }
	import renamedDefaultFunction, { functionOne } from './module.mjs';

If you are familiar with React, you will likely have already seen default and named exports being used together:

jsx
	import React, { useEffect, useState } from 'react';

Other ways to export default and named exports

So far, you have only seen exports used in one way, where we add the exports at the bottom of the file.

javascript
	function myFunction() {
	  // code here
	}
	
	export { myFunction };

Another way we can export our code is to add the export keyword when you create the function. The advantage of this is that you don’t have an extra line of code that you need to keep updating as you add more code.

Named export:

Below is another way we could do a named export:

javascript
	export function myFunction() {
	  // code here
	}

Default export:

Below is an alternative way to do a default export:

javascript
	export default myFunction() {
	  // code here
	}

Splitting out code

As mentioned previously, modules allow us to split our code easily. We can choose what code (such as functions and variables) we can export from a module.

This means we can create a “separation of concerns”, which means that we have different modules for different parts of our application.

For example, if we had a game, we could have our player code in a player.js file, our enemy code in an enemy.js file and our main game in a game.js file.

This makes your code more manageable as your code is independent. Any changes to the player.js file will not have side-effects that impact your enemy.js file.

There are additional benefits to this. When you start using a bundler, such as Webpack, code-splitting lets you load only certain files that would be needed, allowing your website to load faster.

Practical example

NOTE: The code for this practical example can be found in the following repo.

In this example, we will emulate a part of a simple online shop. There is an item with a set price that will have tax added. It will be formatted, and the final amount will be displayed to the user.

Here is the list of files:

  1. index.mjs: This is our main entry point file that will call the functions from the modules.

  2. tax.mjs: This contains a function calculateTax which calculates the tax.

  3. utils.mjs: This contains a function formatCurrency which will make our item look more presentable e.g. 115 becomes “115.00 kr”.

  4. display.mjs: This contains the function displayAmount which will display a message to the user about how much the item will cost after tax is applied.

1. index.mjs

javascript
	// 1. index.mjs
	
	import { calculateTax } from './tax.mjs';
	import { formatCurrency } from './utils.mjs';
	import { displayAmount } from './display.mjs';
	
	// Our item initially costs 20
	const price = 100;
	const taxPercentage = 15;
	
	// We need to add tax to our item.
	const priceWithTax = calculateTax(price, taxPercentage);
	// priceWithTax = 115
	
	// We need to now format the item amount
	// so it has 2 decimal spaces but also so
	// it shows a currency symbol
	const formattedPriceWithTax = formatCurrency(priceWithTax, 'kr');
	// formattedPriceWithTax = '115.00 kr'
	
	// We finally display a message to the user:
	displayAmount(formattedPriceWithTax);
	// Logs:
	// The item costs 115.00 kr.

2. tax.mjs

javascript
	// 2. tax.mjs
	
	/**
	 * Calculates the tax for a given amount. Tax
	 * defaults to 15%.
	 * @param {number} amount
	 * @param {number} taxPercentage
	 */
	export function calculateTax(amount, taxPercentage = 15) {
	  return amount + amount * (taxPercentage / 100);
	}

3. utils.mjs

javascript
	// 3. utils.mjs
	
	/**
	 * Formats an amount to be a currency display amount e.g.
	 * "500" becomes "500.00 kr"
	 * @param {number} amount Currency amount
	 * @param {string} currencySymbol The currency symbol
	 * @returns
	 */
	export function formatCurrency(amount, currencySymbol = 'kr') {
	  const formattedAmount = amount.toFixed(2);
	  return `${formattedAmount} ${currencySymbol}`;
	}

4. display.mjs

javascript
	// 4. display.mjs
	
	/**
	 * Displays the cost of the item with tax applied to the user
	 * @param {string} amount
	 */
	export function displayAmount(formattedAmount) {
	  console.log(`The item costs ${formattedAmount}.`);
	}

Additional Info

Modules are deferred by default

When using a <script> tag with a normal JavaScript file, you run into the issue where the file loads before the HTML elements have been added to the page. This leads to errors when trying to select the HTML elements with JavaScript because these HTML elements don’t exist when the JavaScript is running.

The solution to this is to add defer or add the <script> import right before the end of the </body> tag.

Modules, on the other hand, are deferred by default. This means that you can add your module imports to the <head> and without needing to use the defer keyword.

Dynamic imports

You don’t have to load all of your modules upfront. Instead, you can load them when needed.

Below is an example of a dynamic import:

javascript
	// math.mjs
	
	export addNumbers(a, b) {
	  return a + b;
	}
javascript
	// main.mjs
	async function doSum() {
	  const mathModule = './math.mjs';
	
	  const { addNumbers } = await import(mathModule);
	
	  const result = addNumbers(10, 10);
	  console.log(result);
	  // Logs:
	  // 20
	}
	
	doSum();

Additional Resources

V8.dev: Modules - Dynamic imports


Lesson Task

Brief

Develop a simple web application utilizing ES6 modules. Your application will consist of a main module that imports functions from two separate modules and uses them to perform tasks.

Requirements:

  1. Module 1 (stringUtils.mjs): Create a module that exports two functions:

    • capitalize: Takes a string and returns it with the first letter capitalized.
    • lowerCase: Takes a string and converts it to lowercase.
  2. Module 2 (mathUtils.mjs): Create a module that exports a function:

    • add: Takes two numbers and returns their sum.
  3. Main Module (app.mjs): Import the functions from the above modules and use them to:

    • Capitalize a string.
    • Convert a string to lowercase.
    • Add two numbers.
  4. Display the results of these operations in the console.

Expected Outcome:

On running the main module, it should display the results of the operations in the console.

Solution:

File: stringUtils.mjs

javascript
	// Exporting a function to capitalize the first letter of a string
	export function capitalize(str) {
	  return str.charAt(0).toUpperCase() + str.slice(1);
	}
	
	// Exporting a function to convert a string to lowercase
	export function lowerCase(str) {
	  return str.toLowerCase();
	}

File: mathUtils.mjs

javascript
	// Exporting a function to add two numbers
	export function add(num1, num2) {
	  return num1 + num2;
	}

File: app.mjs

javascript
	// Importing functions from the stringUtils and mathUtils modules
	import { capitalize, lowerCase } from './stringUtils.mjs';
	import { add } from './mathUtils.mjs';
	
	// Using the imported functions
	const capitalizedString = capitalize('hello');
	const lowerCaseString = lowerCase('WORLD');
	const sum = add(10, 5);
	
	// Displaying the results in the console
	console.log(`Capitalized: ${capitalizedString}`); // Output: Hello
	console.log(`Lowercase: ${lowerCaseString}`); // Output: world
	console.log(`Sum: ${sum}`); // Output: 15

Task for Students:

Create the above modules and run the main module (app.mjs). Ensure your environment supports ES6 module syntax. Observe the outputs in the console and understand how the import and export statements are used to share functionality across different modules.