How to Run JavaScript in a Jupyter Notebook

javascript
jupyter
Author

Christian Wittmann

Published

October 22, 2024

I decided it was time to learn some JavaScript. Since I’m used to working in Jupyter notebooks for exploratory coding in Python, and my blog is also entirely written in Jupyter notebooks, I explored how to run a JavaScript kernel within a Jupyter notebook. While I realize this is not a common way to write JavaScript code, it’s a practical solution for embedding real JavaScript code directly into my blog posts, which are themselves built with Jupyter.

First, we will install IJavascript, a Jupyter kernel that enables running JavaScript code directly within Jupyter notebooks. After the setup, we will explore some basics of JavaScript, and I’ll share some of my first lessons learned from using JavaScript in Jupyter notebooks, drawing comparisons to Python along the way.

Combination of Jupyter and JavaScript logo
Combination of Jupyter and JavaScript logo

Installation of IJavascript

Before we can install IJavascript, we need to set up some basic components for JavaScript development:

  • Node.js as the JavaScript runtime
  • NPM (Node Package Manager) for installing packages
  • NVM (Node Version Manager) to manage different versions of Node.js

While it is possible to install IJavascript directly after setting up these components, I encountered some dependency issues during the process. To resolve these, I switched to the latest LTS (Long-Term Support) version of Node.js. At the time of writing, this step was necessary, but it may become obsolete in future versions.

Note: Everything I describe in this blog post is based on macOS. If you’re using Windows or Linux and have adapted this approach, I’d love to hear about your experience.

Xcode Command Line Tools

The Xcode Command Line Tools are a set of macOS development tools that provide essential software for compiling code and performing development tasks.

To check if they are installed, run:

xcode-select -p

If they are not installed, you can install them by running:

xcode-select --install

NVM (Node Version Manager)

We will use NVM (Node Version Manager) to set up our JavaScript environment. This is the preferred method over installing via Homebrew because NVM allows you to easily switch between different versions of Node.js. Additionally, when installing IJavascript, I encountered dependency issues with the latest version of Node.js. Therefore, we need to install the latest LTS (Long-Term Support) version of Node.js in the next step.

To check if NVM is installed, run:

nvm -v

If NVM is not installed, you can install it by running:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

After installation, load nvm into your current terminal session by running:

source ~/.bashrc   # if you use bash
source ~/.zshrc    # if you use zsh

Installation of latest LTS (Long-Term Support) version of Node.js

If you are new to NVM and previously installed Node.js via Homebrew, it’s recommended to uninstall the Homebrew version to avoid conflicts:

brew uninstall node

To resolve dependency issues, I switched to the latest LTS (Long-Term Support) version of Node.js. Use nvm to install and activate this version:

nvm install --lts`
nvm use --lts`

You can verify the installed versions of Node.js and NPM by running:

node -v
npm -v

After installing the LTS version, it’s a good idea to update npm to the latest version:

npm install -g npm@latest

Installation of IJavascript

Once you’ve switched to the LTS version of Node.js, you can install IJavascript by running:

    npm install -g ijavascript

The npm command is similar to pip in the Python world, which is used for installing packages. The -g parameter ensures that the package is installed globally, making IJavascript available across all projects using the active Node.js version.

After the installation, register IJavascript as a Jupyter kernel with the following command:

ijsinstall

Using JavaScript in Jupyter Notebook (in VS Code)

After having completed all installation steps, you can run JavaScript code in a Jupyter notebook. Note that you need to restart VS Code.

When you run the following cell, you will be asked to select a kernel in VS Code. Select IJavascript.

console.log("Hello World!")
Hello World!

First steps in JavaScript

Let’s explore JavaScript beyond just a simple “Hello, World!”.

Declaring Variables

Unlike Python, variables in JavaScript need to be explicitly declared before they are used. Here is an example:

let hello;

In the context of Jupyter notebooks, this can lead to undesired behavior where cells containing variable declarations can only be executed once. The simplest way to avoid unwanted syntax errors is to separate the declaration of variables and the actual code into two separate cells.

Since JavaScript typically declares and initializes variables at the same time (for example, let hello = "Hello";), this separation of declaration and initialization does not feel very “JavaScripty” (similar to how “Pythonic” refers to clear and idiomatic Python code). However, this trade-off works well for using JavaScript in Jupyter notebooks, which, by itself, is not a very “JavaScripty” way of coding. Jupyter’s cell-based, interactive workflow differs significantly from how JavaScript is usually written and executed.

Here is an example to show the separation of declaration and initialization. Just be mindful not to execute the declaration cells multiple times.

let greeting1;
let greeting2;
greeting1 = "Hello";
greeting2 = 'World!';

console.log(greeting1, greeting2);
Hello World!

By the way, notice that just like in Python, in JavaScript a string represents a sequence of characters and can be enclosed in either single quotes (') or double quotes (").

As a best practice that applies to both Python and JavaScript:

  • Use single quotes (') by default.
  • Switch to double quotes (") when the string contains a single quote (e.g., an apostrophe) to avoid using escape characters.

Another interesting aspect of variables in JavaScript is how their scope is defined. If you declare variables using let, the scope of the variable is limited to the block (enclosed by curly braces {}) where it is declared. Unlike Python, this could include blocks like if statements, for loops, or any code wrapped in curly braces.

Here is a example:

if ('the stars align' === 'the stars align') {
    let light = 'shine bright';
}

// Checking if the light still shines beyond the block
console.log(typeof light === 'undefined' ? 'The light fades into the void...' : 'The light endures.');
The light fades into the void...

Apart from the definition of the scope that we can observe in the example above, there are a few other noteworthy elements.

First, the comparison is done with ===, which checks for strict equality. This means it checks both the value and the type, making it more precise than ==, which only checks for value equality and can sometimes lead to unexpected results due to type coercion. (In Python, there isn’t a direct equivalent to ===, but Python’s == behaves more like JavaScript’s strict === by default, without implicit type conversion.)

Additionally, JavaScript supports a ternary operator (just like Python, though it is more commonly used in JavaScript), which is a shorthand way of writing an if-else statement:

JavaScript:

condition ? expressionIfTrue : expressionIfFalse;

Python:

expressionIfTrue if condition else expressionIfFalse

For completeness, here’s the traditional way of writing out the condition from the example above:

if (typeof light === 'undefined') {
    console.log('The light fades into the void...');
} else {
    console.log('The light endures.');
}

Implementing a Pyramid Generator

To wrap up this blog post, here’s some slightly more complex code. In parallel, I’ve been following a tutorial on building a pyramid generator in JavaScript. Below is my Jupyter notebook version, followed by some observations.

const character = "#";
const rows = [];
let pyramidHeight;
let inverted;
let result;
rows.length = 0;  // Reset the array to an empty list
pyramidHeight = 8;
inverted = false;
result = "";

// Generates the spaces needed to center the pyramid row
function generateWhitespace(rowNumber, pyramidHeight) {
    return " ".repeat(pyramidHeight - rowNumber)
}

// Generates the pyramid characters (e.g., "###" or "#####")
function generatePyramid(rowNumber){
    return character.repeat(2 * rowNumber - 1)
} 

// Combines whitespace and pyramid characters to create a centered row
function generatePyramidRow(rowNumber, pyramidHeight) {
    const whitespace = generateWhitespace(rowNumber, pyramidHeight);
    const pyramid = generatePyramid(rowNumber);
    return `${whitespace}${pyramid}${whitespace}`;
}

for (let rowNumber = 1; rowNumber <= pyramidHeight; rowNumber++) {
    if (inverted) {
        rows.unshift(generatePyramidRow(rowNumber, pyramidHeight));
    } else {
        rows.push(generatePyramidRow(rowNumber, pyramidHeight));
    }
}
8

Notice the output of the cell, which is equal to the height of the pyramid. This is due to Jupyter’s behavior of displaying the result of the last evaluated statement. It can be suppressed by adding a dummy line at the end, like void 0; (which produces undefined).

console.log(rows);
[
  '         #         ',
  '        ###        ',
  '       #####       ',
  '      #######      ',
  '     #########     ',
  '    ###########    ',
  '   #############   ',
  '  ###############  ',
  ' ################# ',
  '###################'
]
result = rows.join("\n");
console.log(result);
       #       
      ###      
     #####     
    #######    
   #########   
  ###########  
 ############# 
###############

The code still looks somewhat unfamiliar compared to what I’m used to in Python. Here are some noteworthy points:

  • I separated the declaration of the variables and their initialization into a “declaration cell” and a “code cell” to be able to run the code cell multiple times in my notebook without re-declaring variables.
  • JavaScript differentiates strictly between variables and constants. In Python, the concept of a constant is not enforced, it’s just a naming convention to use uppercase (SPEED_OF_LIGHT = 299792458). In JavaScript, however, a constant truly is a constant, and the language enforces that it cannot be changed (const speedOfLight = 299792458;).
  • The fact that the list of rows is also defined as a constant still looks odd to me. However, using const for arrays and objects is considered a good practice in JavaScript because it prevents the variable from being accidentally reassigned. const ensures the variable refers to the same instance, but you can still modify the instance’s contents.
  • The comments describing the functions are not directly comparable to docstrings in Python. To create similar in-line documentation, you would use JSDoc in JavaScript.
  • ${whitespace}${pyramid}${whitespace} is a template literal, which is the JavaScript equivalent of a Python f-string. Template literals are enclosed in backticks (`) and allow you to inject dynamic content using ${name}.

Conclusion

My first few days with JavaScript have been an interesting ride. As stated above, my approach is likely quite unusual, but I feel it’s effective for my personal learning journey. The familiar environment of Jupyter notebooks allows me to see results quickly without having to focus too much on setup or infrastructure. I’m certain, however, that future JavaScript projects will look different as I explore more traditional ways of working with the language.