Overview and Practicalities

Optional JavaScript Primer


Learning Objectives

  • You know of the basics of JavaScript.

Variables and scope

Variables are defined using let and const, where the former introduces a variable with a value that can be changed while the latter introduces a constant with a value that cannot be changed. Below, we create two variables and log them to the console.

const yearOfBirth = 1961;
let name = "B. Eich";

name = "Brendan Eich";

console.log(`${name} was born in ${yearOfBirth}.`);

You can run the program from the command line with Deno using the command deno run app.js. The output of the program is as follows.

Brendan Eich was born in 1961.

Variables created with let and const are block-scoped. Block scoping means that the variables exist only within the block where they have been defined (and, also, within blocks nested within the block).

Functions

Functions in JavaScript are created using the arrow syntax () => {...}. In the following example, we define a function called hello that, when called, logs the message Hello world!. In the example, the function is called after it is defined.

const hello = () => {
  console.log("Hello world!");
};

hello();

The output of the above application is as follows.

Hello world!

The arrow syntax was introduced to JavaScript in 2015 in the 6th edition of the ECMAScript specification (ES6).

Function parameters

Function parameters are added to the function parentheses. In the following example, we define a function greet that takes in a name as a parameter. The function then logs the message Hello name!, where name has been replaced with the value of the parameter.

const greet = (name) => {
  console.log(`Hello ${name}!`);
};

greet("Brendan Eich");

When running the function, the output is as follows.

Hello Brendan Eich!

If a parameter value is not passed to the function, the value of the parameter will be undefined.

const greet = (name) => {
  console.log(`Hello ${name}!`);
};

greet();
Hello undefined!

Parameters are separated from each other using commas. In addition, default parameter values can be set in the parameter definition.

The following example creates another version of the greet function, which has two parameters with default values. Calling the function without setting parameters produces the output Hello John Doe!, while the parameter values can also be set as shown in the below example.

const greet = (greeting = "Hello", name = "John Doe") => {
  console.log(`${greeting} ${name}!`);
};

greet();
greet("Oh hi");
greet("Hello", "Brendan Eich");
greet("Hello there!", "General Kenobi");
Hello John Doe!
Oh hi John Doe!
Hello Brendan Eich!
Hello there! General Kenobi!

Returning a value from a function

Returning from a function happens either at the end of the function or with explicitly using the return statement. If a value or a variable is added after the return statement, then that value is returned by the function. The following example shows a function that returns a greeting.

const greeting = (who = "John Doe") => {
  return `Hello ${who}!`;
};

const output = greeting("you");
console.log(output);
Hello you!

Although some programming languages automatically return the last value in a function, JavaScript does not work like that. In the following example, the output from the function is undefined as the explicit return statement is missing.

const greeting = (who = "John Doe") => {
  `Hello ${who}!`;
};

const output = greeting("you");
console.log(output);
undefined

Conditional statements and comparison operators

Comparing two values in JavaScript is done with an if-statement with three equals-signs. The statement a === b compares the values of a and b. If a and b are the same, the result of the comparison is true, and if a and b are not the same, the result of the comparison is false.

The following example shows a simple if - else -structure, where the printed message depends on the outcome of the comparison.

let a = "5";
let b = "6";

if (a === b) {
  console.log("The same");
} else {
  console.log("Not the same");
}

a = "6";

if (a === b) {
  console.log("The same");
} else {
  console.log("Not the same");
}

It is also possible to use two equals-signs for comparison, but this is not recommended as it can lead to unexpected results. The statement a == b compares the values of a and b but also tries to convert the values to the same type before the comparison. For instance, the statement 5 == "5" is true, but the statement 5 === "5" is false.

The other comparison operators that you might be familiar with such as >, <, >=, <=, and !== work as expected.

Data structures

The most commonly used data structures for storing data are arrays, maps, and sets.

Arrays

The following example defines an array with two values, which are then logged. Indexing an array is done with brackets and an index number (indexes start at zero).

const array = ["One", "Two"];
console.log(array);
console.log(array[0]);
console.log(array[1]);

Note that the statement const array = ["One, Two"]; is a shorthand for const array = new Array("One", "Two"); — in the materials, we use the shorthand whenever possible.

Most relevant methods for arrays are as follows.

  • push(value)- adds a value to the end of the array.
  • shift() - removes the first value from the array.
  • pop() - removes the last value from the array.
  • splice(start, end) - removes one or more values between provided indices.

All of these methods return the removed value(s).

const array = [];
array.push("One");
array.push("Two");
array.push("Three");

console.log(array);
const shifted = array.shift();
console.log(array);
const popped = array.pop();
console.log(array);

console.log(`removed: ${shifted}, ${popped}`);
["One", "Two", "Three"]
["Two", "Three"]
["Two"]
removed: One, Three
const array = [1, 2, 3, 4, 5];
console.log(array);

// parameters are the start index and end index for removal range
const removed = array.splice(1,3);
console.log(array);
console.log(`removed: ${removed}`);
[1, 2, 3, 4, 5]
[1, 5]
removed: 2, 3, 4

Maps

Maps are created with the new keyword that creates a new map, which is accessed with a set of methods. The relevant methods for maps are as follows.

  • set(key, value) - adds a key-value -pair to the map. If the key already exists in the map, the old value is replaced with the new value.
  • get(key) - retrieves a value for a given key, returns either the value or undefined if the value does not exist.
  • has(key) - checks if a key exists; returns either true or false.
  • delete(key) - removes a key-value -pair from the map based on a given key.

The following example demonstrates the basic map functionality.

const translations = new Map();

translations.set("yksi", "one");
translations.set("kaksi", "two");
translations.set("kolme", "three");

console.log(translations.has("neljä"));

translations.set("neljä", "four");

console.log(translations.has("neljä"));

translations.delete("yksi");

console.log(translations);

console.log(translations.get("kaksi"));
console.log(translations.get("two"));
false
true
Map { "kaksi" => "two", "kolme" => "three", "neljä" => "four" }
two
undefined

Sets

Sets are created with the new keyword that creates a new set, which — similar to the map — is accessed with a set of methods. The relevant methods for sets are as follows.

  • add(value) - adds a value to the set.
  • has(value) - checks if a value exists; returns either true or false.
  • delete(value) - deletes a value from the set.

The following example demonstrates the basic set functionality.

const uniques = new Set();

uniques.add("one");
uniques.add("two");
uniques.add("two");
uniques.add("two");

console.log(uniques);

uniques.delete("two");

console.log(uniques);
console.log(uniques.has("two"));
Set { "one", "two" }
Set { "one" }
false

Objects and JSON

Objects are variables that can contain many values, each with a specific label (a key). Objects are similar to maps — however, while maps allow practically any type of keys, objects use strings for keys. The way how data is accessed also differs between objects and maps.

The following example defines an object called person that has two keys: name and yearOfBirth. The value of name is Brendan Eich and the value of year of birth is 1961.

const person = {
  name: "Brendan Eich",
  yearOfBirth: 1961,
};

As shown above, an object definition starts with an opening curly bracket and ends with a closing curly bracket. Key-value pairs in the object are defined using name: value. Each key and value is separated by a colon, and each key-value pair is separated by a comma. An object’s key-value pairs are often referred to as properties or methods when the value is a function.

Accessing the value of a property can be done in two ways, which are both shown next.

const person = {
  name: "Brendan Eich",
  yearOfBirth: 1961,
};

console.log(person.name);
console.log(person["name"]);
Brendan Eich
Brendan Eich

While the former way of accessing a property is the preferred way, the latter is required when the property name comes from a variable or the name contains problematic characters that have special meaning in javascript, for instance a space or a dash.

const person = {
  name: "Brendan Eich",
  "year-of-birth": 1961,
};
const personNameKey = "name";
console.log(person[personNameKey]);
console.log(person['year-of-birth']);
Brendan Eich
1961

Adding properties to an object can be done using an assignment operator and an accessor. In the following example, after creating the person-object, we add a property inventorOf to the person.

const person = {
  name: "Brendan Eich",
  yearOfBirth: 1961,
};

person.inventorOf = "Mocha";

console.log(person.name);
console.log(person.inventorOf);
Brendan Eich
Mocha

In the context of web applications, JavaScript objects are often represented using JavaScript Object Notation (JSON). JSON is used for passing JavaScript Objects in a textual format. The following is an example of a JSON document that outlines the name and the year of birth of a person. The main difference between the JSON documents and the JavaScript objects in terms of notation is that the keys in JSON documents are also within quotes.

{
    "name": "Jane Doe",
    "yearOfBirth": 1900
}

When using JavaScript, a JSON document (which is a string) is translated into a JSON object using the function JSON.parse(document). Similarly, an object can be translated into a string using the function JSON.stringify(object).

const jsonString = '{"name": "Jane Doe", "yearOfBirth": 1900}';
const obj = JSON.parse(jsonString);
console.log(obj.name);

const string = JSON.stringify(obj);
console.log(string);
Jane Doe
{"name":"Jane Doe","yearOfBirth":1900}

More about objects can be found in MDN’s introductions to objects and also map reference, which contains a detailed comparison between Javascript’s object and map.

Iteration and functional programming

JavaScript comes with the common looping structures such as for, while, and for ... of loops.

The following highlights iterating over arrays, maps, and sets using the for ... of loop.

const array = ["One", "Two", "Three"];
for (const value of array) {
  console.log(value);
}


const map = new Map();
map.set("One", 1);
map.set("Two", 2);
map.set("Three", 3);
for (const [key, value] of map) {
  console.log(`${key}: ${value}`);
}

const set = new Set();
set.add("One");
set.add("Two");
set.add("Three");
for (const value of set) {
  console.log(value);
}

In addition, JavaScript has a number of methods that can be used to work with data in arrays, maps, and sets. The most common methods are forEach, map, filter, and reduce. As an example, the following demonstrates the use of the map method to create a new array where each value in the original array is multiplied by two.

const array = [1, 2, 3, 4, 5];
const doubled = array.map(value => value * 2);
console.log(doubled);

Similarly, the following shows how the filter method can be used to create a new array that contains only the values that are greater than two.

const array = [1, 2, 3, 4, 5];
const greaterThanTwo = array.filter(value => value > 2);
console.log(greaterThanTwo);

Working with multiple files

Javascript values, including functions, can be exported from the files in which they have been defined. This allows importing the exported values into use in other files. Exporting a value can be done in two ways: as a default export, or as a named export. The main difference between the two is that a module (file) can have only a single default export but arbitrarily many named exports.

Default export and import

Let us assume that we have two files, app.js and fun.js. The file fun.js contains a function greeting which is used to print a greeting. The function is exported as a default value, shown below.

const greeting = (who = "John Doe") => {
  return `Hello ${who}!`;
};

export default greeting;

Importing a default value is done using an import statement, which outlines a name for the exported value and a location from where it is imported from. In the example below, which outlines the contents of app.js, the function greeting is imported from the fun.js file and then called.

import greeting from "./fun.js";

const msg = greeting("exporting and importing");
console.log(msg);

The line import greeting from "./fun.js"; can be interpreted as import a default value from the file fun.js that is in the current directory, and label the exported default value from the file fun.js as greeting in the current file.

Now, when we run the app.js file, the output is as follows. The example below first verifies that the files are in the same directory using the tree command, after which the program is run.

tree --dirsfirst
.
├── app.js
└── fun.js

0 directories, 2 files
deno run app.js
Hello exporting and importing!

Named exports

In the following example, we define two functions: hello and greeting. Both functions are exported from the file.

const hello = () => {
  console.log("Hello world!");
};

const greeting = (who = "John Doe") => {
  return `Hello ${who}!`;
};

export { greeting, hello };

Let us assume that the above functions are defined in a file fun.js. Now, the functions can be imported into another file as follows.

import { greeting, hello } from "./fun.js";

hello();
const msg = greeting();
console.log(msg);

In the example below, we again assume that the above file is called app.js and that both fun.js and app.js reside in the same directory.

Hello world!
Hello John Doe!

It is also possible to import a single function. In the example below, we import only the function hello in the app.js file.

import { hello } from "./fun.js";

hello();
Hello world!

Importing all functions with a namespace

There are situations where multiple files’ export functions that have the same name. In such a case, one can create a namespace for the functions that are imported from a file. In the example below, all the functions are imported from the file fun.js so that they belong to the namespace fun. Calling the functions is now done using fun.functionName, as shown below.

import * as fun from "./fun.js";

fun.hello();
fun.greeting("all");
Hello world!
Hello all!

Working with a variable defined in a separate file

Let’s take a brief look at working with a variable defined in a separate file through an example where we create a simple counter. The following example defines a variable called value and two functions that can be used to access and modify the variable.

let value = 0;

const get = () => {
  return value;
};

const increment = () => {
  value++;
};

export { get, increment };

In practice, the functions get and increment encapsulate the variable value, leading to a situation where the variable cannot be accessed in a separate file other than through the functions get and increment.

Let’s assume that the above code has been placed in a file called counter.js. Now, we can use the counter in a separate file as follows. In the next example, we import the functions from the file counter.js and access and modify the value using them.

import * as counter from "./counter.js";

console.log(counter.get());
counter.increment();
console.log(counter.get());
counter.increment();
counter.increment();
counter.increment();
console.log(counter.get());

Assuming that the above code is placed in a file called app.js and that the file is in the same directory with the counter.js file, the output of the application is as follows.

0
1
4

The value in the counter.js has only one instance that can be shared in multiple locations within the program. That is, if the functions from counter.js are imported in separate locations in the program, or even more than once in the same file, any changes to the value are reflected to all locations. This is demonstated in the following example, where the counter is imported twice.

import * as counter from "./counter.js";
import * as another from "./counter.js";

console.log(counter.get());
counter.increment();
console.log(counter.get());
counter.increment();
counter.increment();
counter.increment();
console.log(counter.get());

console.log(another.get());
another.increment();

console.log(counter.get());
console.log(another.get());
0
1
4
4
5
5

Files in folders

Working with files in folders does not differ significantly from the examples above. If a file from which we import functions is in a subdirectory, then that directory is added as a part of the path given in the import statement.

Let’s assume that we have two files, app.js and nameService.js and that the file nameService.js is within a folder called services. The contents of the nameService.js are as follows.

let name = "Anonymous";

const set = (newName) => {
  name = newName;
};

const get = () => {
  return name;
};

export { get, set };

Now, we can work with the functions as follows.

import * as nameService from "./services/nameService.js";

console.log(nameService.get());
nameService.set("John Doe");
console.log(nameService.get());
tree --dirsfirst
.
├── services
│   └── nameService.js
└── app.js

1 directory, 2 files
Anonymous
John Doe

Note that similar to the example with the counter, the variable name in the nameService.js is also “global”. If the nameService.js would be used in multiple JavaScript files, the function call get would always retrieve the value last set to the name variable. That is, when importing the nameService.js, the same functions (and content) is imported in all files. In other words, no new instance of the file is created during import, if it already has been imported previously.

For more details and examples on exports and imports, see the MDN Web Docs pages on export and import.

External functionality

In addition to our own code, we rely on functionality implemented by others. Deno supports importing code from practically any online location.

As an example, let us use the code at https://raw.githubusercontent.com/FITech101/simplejs/main/fun-code.js, which is as follows:

const hello = () => {
  console.log("Hello external function!");
};

export { hello };

Importing external functions with Deno works similarly to importing functions from local files. Now, instead of using a file as the location from where functions are imported from, we use the address of the file that has the functions that we wish to import. The example below shows how the code at https://raw.githubusercontent.com/FITech101/simplejs/main/fun-code.js can be imported and used.

import { hello } from "https://raw.githubusercontent.com/FITech101/simplejs/main/fun-code.js";

hello();

Let’s assume that the above code is in a file called app.js. When we run the app.js for the first time, the code that needs to be imported is downloaded to the local computer and then executed. On subsequent runs, there is no longer a need to download the code, as it has already been downloaded.

Download https://raw.githubusercontent.com/FITech101/simplejs/main/fun-code.js
Hello external function!
Hello external function!

If the code in the online location changes but the address stays the same, our application does not know about it. In such a case, we need to clear the code that has been downloaded to our computer. We can identify the location into which code is downloaded using the command deno info.

deno info
DENO_DIR location: "/path-to-cache/.cache/deno"
Remote modules cache: "/path-to-cache/.cache/deno/deps"
TypeScript compiler cache: "/path-to-cache/.cache/deno/gen"

The remote modules (or functions) are — in our case — in the path /path-to-cache/.cache/deno/deps. If we would wish to clear the file that we just downloaded, we would have to remove it from the directory.

Typically, however, addresses also contain information about the version of the functions. The version numbers are used to keep track of changes and also to avoid the problem with downloaded code being out of date.

On code at online locations

Note that any code can be posted online. It is up to you to make sure that the code that your application depends on is safe.

Asynchronous functions

The difference between synchronous and asynchronous functions is that when a synchronous function is called from program code, the execution of the code from where the function is called waits until the synchronous function has finished execution. For asynchronous functions, the execution of code from where the asynchronous function is called does not need to wait and can continue execution.

JavaScript has both synchronous and asynchronous functions.

Defining asynchronous functions

Asynchronous functions are defined using the async keyword that is placed before the parameters in the function definition. The following is an example of an asynchronous function that waits for the amount of seconds given as a parameter (don’t worry about the function itself, we’ll use it just for an example) and then logs the message to the console.

const waitAndPrint = async(message, seconds) => {
  await new Promise(resolve => setTimeout(resolve, seconds * 1000));
  console.log(message);
}

In the following example, we call the asynchronous function three times.

const waitAndPrint = async (message, seconds) => {
  await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  console.log(message);
};

waitAndPrint("First call!", 3);
waitAndPrint("Second call!", 2);
waitAndPrint("Third call!", 1);

When we run the program, we see the following output. The line with Third call! takes approximately one second to appear, the line with Second call! takes approximately two seconds to appear, and the line with First call! takes approximately three seconds to appear.

Third call!
Second call!
First call!

In practice, all of the functions are executed simultaneously, which leads to a situation where the total time that it takes to execute the program is approximately three seconds.

Waiting for an asynchronous function

Now, if we would want to wait in our program until the asynchronous function has completed the execution, we’d have to use the await keyword when calling the function. In the following example, we wait for the first waitAndPrint function call to finish before we continue with the execution of the program.

const waitAndPrint = async (message, seconds) => {
  await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  console.log(message);
};

await waitAndPrint("First call!", 3);
waitAndPrint("Second call!", 2);
waitAndPrint("Third call!", 1);

Now, the output of the program is be as follows.

First call!
Third call!
Second call!

Note that contrary to the previous example, it would take approximately 3 seconds for the First call! to appear. After this, it would take approximately 1 second for the Third call! to appear (a total of 4 seconds), and approximately 2 seconds for the Second call! to appear (a total of 5 seconds).

Similarly, we could wait for each call in turn to finish before moving to the next call.

const waitAndPrint = async (message, seconds) => {
  await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  console.log(message);
};

await waitAndPrint("First call!", 3);
await waitAndPrint("Second call!", 2);
await waitAndPrint("Third call!", 1);

Now, the output of the program would be as follows.

First call!
Second call!
Third call!

In the above example, it would take approximately 3 seconds for the First call! to appear, after which it would take approximately 2 seconds for the Second call! to appear (a total of 5 seconds). After this, it would take approximately 1 second for the Third call! to appear (a total of 6 seconds).

Await within a function

When we use await within a function, we need to define the function in which await is used as asynchronous. The following example shows a program where await is used within a function that is not asynchronous — the example does not work.

const waitAndPrint = async (message, seconds) => {
  await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  console.log(message);
};

const example = () => {
  await waitAndPrint("First call!", 3);
  await waitAndPrint("Second call!", 2);
  await waitAndPrint("Third call!", 1);
};

example();

When we run the program, we see the following output.

error: Uncaught SyntaxError: Unexpected reserved word
  await waitAndPrint('First call!', 3);
  ~~~~~
    at <anonymous> (file:///path-to-file/app.js:7:3)

Changing the function to an asynchronous function solves the problem.

const waitAndPrint = async (message, seconds) => {
  await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  console.log(message);
};

const example = async () => {
  await waitAndPrint("First call!", 3);
  await waitAndPrint("Second call!", 2);
  await waitAndPrint("Third call!", 1);
};

example();
First call!
Second call!
Third call!

Function execution order

When working with synchronous functions, determining the execution order of a set of functions was easy. Whenever a function was called, it was placed on the top of the call stack, and whatever function was at the top of the call stack was executed.

When working with asynchronous functions, whenever an asynchronous function is called, it is also placed on top of the call stack. However, instead of waiting for the function to be completed, a promise of the completion of the function is returned and the responsibility for the execution of the asynchronous function is given to a corresponding Web Platform API, which can be used to e.g. execute code in a separate thread.

Whenever an asynchronous function has been executed (e.g. by a Web API), the associated (completed) promise is placed into a queue, which is processed by the JavaScript engine. If the queue holds completed promises, they are eventually brought back to the call stack and processed; oldest completed promises are given priority over new ones. Note that the when and how asynchronous functions are executed partially depend on the used JavaScript engine (V8 for Deno).

In general, if the await keyword is used, the execution of the program waits until the promise has been fulfilled before continuing with the execution. On the other hand, if asynchronous functions are called without the await keyword, the program execution does not wait for the function to finish; in such a case, one cannot always determine the order in which the function calls will be completed.

As an example, the output of the program below depends on the duration of the (simulated) heavy calculation.

const simulatedHeavyCalculation = async () => {
  await new Promise((resolve) => setTimeout(resolve, Math.random() * 100));
};

const logMessage = async (message) => {
  await simulatedHeavyCalculation();
  console.log(message);
};

logMessage("Hello world!");
logMessage("Hello world again!");
Hello world again!
Hello world!

Promises and callbacks

The keywords async and await are an updated take on working with Promises, which are used to represent the eventual execution of asynchronous functions. Before the introduction of async and await, one wrote asynchronous functions with promises and the keyword then, which was passed a function that was to be completed after the promise was fulfilled. As an example, the above application could be implemented without async and await as follows.

const simulatedHeavyCalculation = () => {
  return new Promise((resolve) => setTimeout(resolve, Math.random() * 100));
};

const logMessage = (message) => {
  simulatedHeavyCalculation().then(() => {
    console.log(message);
  });
};

logMessage("Hello world!");
logMessage("Hello world again!");

In the above example, the logMessage function calls the simulatedHeavyCalculation function, which returns a promise. Promises have a method then, which is given a function that is called once the promise is fulfilled.

Before promises, the same effect was realized by giving functions as parameters to the code that was to be completed. In the example below, the function simulatedHeavyCalculation is given a callback function as a parameter, which is to be called once the execution of the function is completed.

The setTimeout function that we have used for demonstration purposes effectively works so that it is given a function — the first parameter — that is called after a specific amount of milliseconds — the second parameter — has elapsed.

const simulatedHeavyCalculation = (callback) => {
  setTimeout(callback, Math.random() * 100);
};

const logMessage = (message) => {
  simulatedHeavyCalculation(() => {
    console.log(message);
  });
};

logMessage("Hello world!");
logMessage("Hello world again!");

In practice, when working with more complex code, this approach often led to a so-called callback hell, where understanding (and debugging) the flow of the code was not trivial. In these materials, we mostly stick with more up to date conventions for writing asynchronous code, in this case using async and await.

Formatting and linting

Deno comes with tools for formatting and linting code. Code formatting refers to formatting the source code to follow a set of code conventions, i.e. code style, and linting refers to automated code analysis that can be used to identify errors, bugs, suspicious code, and other issues.

Source code can be automatically formatted to follow Deno code style with the deno fmt command.

Let’s assume that we have the following code in a file called app.js.

const name

  ='My name!';
        let age =         180

console.log(`My name is ${name} and I am ${age} years old`);

Running the command deno fmt followed by the name of the JavaScript file that is to be formatted formats the file. The output for the command is as follows.

deno fmt app.js
/path-to-file/app.js
Checked 1 file

When we now look at the contents of the app.js file, the code is now as follows.

const name = "My name!";
let age = 180;

console.log(`My name is ${name} and I am ${age} years old`);
Deno Style Guide

Deno has a style guide that can be used as a reference for formatting and style.

Deno’s linter is run with the command deno lint, followed by the file that is to be checked. Let’s assume that we have the following code in the file called app.js.

const first = "test";
const second = "test";

if (x = y) {
  console.log("Hello world!");
} else {
}

Running the command deno lint --unstable app.js highlights four issues in the code, which are shown below.

deno lint app.js
(no-cond-assign) Expected a conditional expression and instead saw an assignment
if (x = y) {
    ^^^^^
    at /path-to-file/app.js:4:4

    hint: Change assignment (`=`) to comparison (`===`) or move assignment out of condition

(no-undef) x is not defined
if (x = y) {
    ^
    at /path-to-file/app.js:4:4

(no-undef) y is not defined
if (x = y) {
        ^
    at /path-to-file/app.js:4:8

(no-empty) Empty block statement
} else {
       ^
}
^
    at /path-to-file/app.js:6:7

    hint: Add code or comment to the empty block

Found 4 problems
Checked 1 file

First of all, the if-statement, which we look into in more detail when looking into control structures, has an assignment instead of a comparison. Second and third, the variables x and y are not defined, and finally, the code has an empty block.

After the issues are fixed, the source code looks as follows.

const first = "test";
const second = "test";

if (first === second) {
  console.log("Hello world!");
}

Now, when we run the linter again, no issues are found.

deno lint app.js
Checked 1 file