Categories
JavaScript Basics

Use Modules to Build a Modular JavaScript App

One of the big features of ES6 is JavaScript supporting built-in modules. Modules allow us to share code between files by using the export and import syntax. This is a big improvement over using script tags and global variables to share code across files.

Using script tags was error prone since the load order matters. Having scripts in the wrong order could cause our program to execute code that hasn’t been declared yet. It also forced us to write spaghetti code with no real structure or logical composition. This problem doesn’t exist with modules because everything is exported and imported directly between files. Also, we can know the definition of the imported code easily since it’s explicit in which modules is being imported and referenced.

Exports and Imports

To make code in a JavaScript file importable, we have to export them explicitly with the export statement. To do this, we just put export in front of the variable or constant you want to expose to other files. For example, we can write:

export let num = 1;

This exports the num variable so that other modules can import it and use it.

We can export anything declared with var, let, const, and also functions and classes. The items exported must be declared at the top level. export cannot be used anywhere else, like inside functions and classes.

We can export multiple members at once. All we have to do is wrap all the members in curly brackets separated by commas. For example, we can write:

const num1 = 1;
const num2 = 2;
const num3 = 3;
export {num1, num2, num3};

This will let us import num1, num2, and num3 in other JavaScript files.

Now that we have exported the members, we can import them in other JavaScript files. We can use the import statement to import one or more members into a module and work with them. For example, if we have the following in moduleA.js:

const num1 = 1;
const num2 = 2;
const num3 = 3;
export {num1, num2, num3};

Then in moduleB.js we can write the following code to import the items from moduleA.js:

import {num1, num2, num3} from './moduleA'

The path after the from keyword starts with a period. The period means that we’re in the current folder.

This assumes that moduleA.js and moduleB.js are in the same folder. If we have them in different folder, then we have the specify the path of moduleA.js relative to moduleB.js if we want to import the exported members of moduleA.js into moduleB.js. For example, if moduleA.js is one level above moduleB.js, then in moduleB.js we write:

import {num1, num2, num3} from '../moduleAFolder/moduleA'

The 2 periods before the path means that we go up one folder level and then get the moduleAFolder and then get moduleA.js.

We can also use JavaScript modules in script tags. To do this, we must set the type attribute of the script tag to module to use them. For example, if we want to use moduleA.js in our HTML file, we can write:

<script type='module' src='moduleA.js'></script>

We can use import and export in JavaScript modules. They won’t work with regular scripts.

Scripts run in strict mode automatically, so we can’t accidentally declare global variables and do other things that can be done without strict mode being enabled. They also load asynchronously automatically so that we won’t have to worry about long scripts holding up a page from loading. Also, import and export only happens between 2 scripts, so no global variables are set. Therefore, they can’t be viewed in the console directly.

Default Exports

There’s also a default export option for exporting a module’s members. We previously exported the variable in a way where we import them by the name. There’s also the default export which exports a single member from a module without needing to reference it explicitly by name when you’re importing it. For example, if we have a single member in a module that we want to export, we can write the following in moduleA.js:

const num = 1;
export default num;

There are no curly braces when you use export default .

Then in the file where you want to import the member. In moduleB.js we write:

import num from './moduleA'

Once again, we omit the curly braces. This is because only one default export is allowed per module. Alternatively, we can write the following in moduleB.js :

import {default as num} from './moduleA'

Renaming Imports and Exports

If we have many modules and their exported members have the same name, then there will be conflicts if we try to import multiple modules. This is will be a problem that we need to fix. Fortunately, JavaScript has the as keyword to let us rename exports and imports so we can avoid name conflicts in our code. To use the as keyword to rename exports, we write the following in moduleA.js:

export {
  num1 as numOne,
  num2 as numTwo
}

Then in moduleB.js, we can import them by writing:

import { numOne, numTwo } from './`moduleA'`

Alternatively, we can do the renaming when we import instead. To do this, in moduleA.js, we put:

export {
  num1,
  num2
}

Then in moduleB.js, we put:

import { num1 as numOne, num2 as numTwo } from './`moduleA'`

If we try to import members with modules where the members have the same name, like:

`import {` num1, num2 `} from './moduleA';
import {` num1, num2 `} from './moduleB';
import {` num1, num2 `} from './moduleC';`

We will see that we get SyntaxError. So we have to rename them so that the module will run:

`import {` num1 as num1A, num2 `as num2A } from './moduleA';
import {` num1 as num1B, num2 `as num2B } from './moduleB';
import {` num1 as num1C, num2 `as num2C } from './moduleC';`

A cleaner way to import from multiple modules that have members with the same names is to import all of the module’s exported members as one object. We can do that with an asterisk. For example, instead of:

`import {` num1 as num1A, num2 `as num2A } from './moduleA';
import {` num1 as num1B, num2 `as num2B } from './moduleB';
import {` num1 as num1C, num2 `as num2C } from './moduleC';`

We can instead write:

import * as moduleA from './moduleA';
import * as moduleB from './moduleB';
import * as moduleC from './moduleC';

Then in code below the imports, we can write:

moduleA.num1;
moduleA.num2;
moduleB.num1;
moduleB.num2;
moduleC.num1;
moduleC.num2;

We can also export and import classes. So if we have a file that contains one or more classes, like the Person.js file with the class below:

class Person {
  constructor(firstName, lastName) {
    this._firstName = firstName;
    this._lastName = lastName;
  }
  get fullName() {
    return `${this.firstName} ${this.lastName}`
  }
  get firstName() {
    return this._firstName
  }
  get lastName() {
    return this._lastName
  }
  sayHi() {
    return `Hi, ${this.firstName} ${this.lastName}`
  }
  set firstName(firstName) {
    this._firstName = firstName;
  }
  set lastName(lastName) {
    this._lastName = lastName;
  }
}

Then we write the following to export a class:

export { Person };

This exports the Person class, and then to import it, we write:

import { Person } from './person';

Dynamic Module Loading

JavaScript modules can be loaded dynamically. This lets us only load modules when they’re needed rather than loading all of them when the app runs. To do this, we use the import() function, which returns a promise. When the module in the argument is loaded, then the promise is fulfilled. The promise resolves to a module object, which can then be used by the app’s code. If we have the Person class in Person.js, then we can dynamically import it with the following code:

import('./Person')
.then((module)=>{
  const Person = module.Person;
  const person = new Person('Jane', 'Smith');
  person.sayHi();
})

Or using the async and await syntax, we can put that in a function:

const importPerson = async ()=>{
  const module = await import('./Person');
  const Person = module.Person;
  const person = new Person('Jane', 'Smith');
  person.sayHi();
}

importPerson();

As you can see, JavaScript modules are very useful for organizing code. It allows us to export things that we want to expose to other modules, eliminating the need for global variables. In addition, exports and imports can be renamed to avoid conflict when importing multiple modules. Also, all the exported members can be imported all at once so that we get the whole module as an object instead of importing individual members. Finally, we can use export default if we only want to export one thing from our module.

Categories
JavaScript Basics

Handling Exceptions in JavaScript

Like any programs, JavaScript will encounter error situations, for example, like when JSON fails to parse, or null value is encounter unexpectedly in a variable. This means that we have to handle those errors gracefully if we want our app to give users a good user experience. This means that we have to handle those errors gracefully. Errors often come in the form of exceptions, so we have to handle those gracefully. To handle them, we have to use the try...catch statement to handle these errors so they do not crash the program.

Try…Catch

To use the try...catch block, we have to use the following syntax:

try{
  // code that we run that may raise exceptions
  // one or more lines is required in this block
}
catch (error){
  // handle error here
  // optional if finally block is present
}
finally {
  // optional code that run either
  // when try or catch block is finished
}

For example, we can write the following code to catch exceptions:

try {
  undefined.prop
} catch (error) {
  console.log(error);
}

In the code above, we were trying to get a property from undefined , which is obviously not allowed, so an exception is thrown. In the catch block, we catch the ‘TypeError: Cannot read property ‘prop’ of undefined’ that’s caused by running undefined.prop and log the output of the exception. So we get the error message outputted instead of crashing the program.

The try...catch statement has a try block. The try block must have at least one statement inside and curly braces must always be used, event for single statements. Then either the catch clause or finally clause can be included. This means that we can have:

try {
  ...
}
catch {
  ...
}

try {
  ...
}
finally{
  ...
}

try {
  ...
}
catch {
  ...
}
finally {
  ...
}

The catch clause has the code that specifies what to do when an exception is thrown in the try block. If they try block didn’t succeed and an exception is thrown, then the code in the catch block will be ran. If all the code in the try block is ran without any exception thrown, then the code in the catch block is skipped.

The finally block executes after all the code the try block or the catch block finishes running. It always runs regardless if exceptions are thrown or not.

try blocks can be nested within each other. If the inner try block didn’t catch the exception and the outer one has a catch block, then the outer one will catch the exception thrown in the inner try block. For example, if we have:

try {
  try {
    undefined.prop
  } finally {
    console.log('Inner finally block runs');
  }
} catch (error) {
  console.log('Outer catch block caught:', error);
}

If we run the code above, we should see ‘Inner finally block runs’ and ‘Outer catch block caught: TypeError: Cannot read property ‘prop’ of undefined’ logged, which is what we expect since the inner try block didn’t catch the exception with a catch block so the outer catch block did. As we can see the inner finally block ran before the outer catch block. try...catch...finally runs sequentially, so the code that’s added earlier will run before the ones that are added later.

The catch block that we wrote so far are all unconditional. That means that they catch any exceptions that were thrown. The error object holds the data about the exception thrown. It only holds the data inside the catch block. If we want to keep the data outside it then we have to assign it to a variable outside the catch block. After the catch block finishes running, the error object is no longer available.

The finally clause contains statements that are excepted after the code in the try block or the catch block executes, but before the statements executed below the try...catch...finally block. It’s executed regardless whether an exception was thrown. If an exception is thrown, then statements in the finally block is executed even if no catch block catches and handles the exception.

Therefore, the finally block is handy for making our program fail gracefully when an error occurs. For example, we can put cleanup code that runs no matter is an exception is thrown or not, like for close file reading handles. The remaining code in a try block doesn’t executed when an exception is thrown when running a line in the try block, so if we were excepted to close file handles in the try and an exception is thrown before the line that closes the file handle is ran, then to end the program gracefully, we should do that in the finally block instead to make sure that file handles always get cleaned up. We can just put code that runs regardless of whether an exception is thrown like cleanup code in the finally block so that we don’t have to duplicate them in the try and catch blocks. For example, we can write:

openFile();
try {
  // tie up a resource
  writeFile(data);
}
finally {
  closeFile();
  // always close the resource
}

In the code above, the closeFile function always run regardless of whether an exception is thrown when the writeFile is run, eliminating duplicate code.

We can have nested try blocks, like in the following code:

try {
  try {
    throw new Error('error');
  }
  finally {
    console.log('finally runs');
  }
}
catch (ex) {
  console.error('exception caught', ex.message);
}

If we look at the console log, we should see that ‘finally runs’ comes before ‘exception caught error.’ This is because everything in the try...catch block is ran line by line even if it’s nested. If we have more nesting like in the following code:

try {
  try {
    throw new Error('error');
  }
  finally {
    console.log('first finally runs');
  }

 try {
    throw new Error('error2');
  }
  finally {
    console.log('second finally runs');
  }
}
catch (ex) {
  console.error('exception caught', ex.message);
}

We see that we get the same console log output as before. This is because the first inner try block didn’t catch the exception, so the exception is propagated to and caught by the outer catch block. If we want to second try block to run, then we have to add a catch block to the first try block, like in the following example:

try {
  try {
    throw new Error('error');
  }
  catch {
    console.log('first catch block runs');
  }
  finally {
    console.log('first finally runs');
  }

 try {
    throw new Error('error2');
  }
  finally {
    console.log('second finally runs');
  }
}
catch (ex) {
  console.error('exception caught', ex.message);
}

Now we see the following message logged in order: ‘first catch block runs’, ‘first finally runs’, ‘second finally runs’, ‘exception caught error2’. This is because the first try block has a catch block, so the the exception caused by the throw new Error('error') line is now caught in the catch block of the first inner try block. Now the second inner try block don’t have an associated catch block, so error2 will be caught by the outer catch block.

We can also rethrow errors that were caught in the catch block. For example, we can write the following code to do that:

try {
  try {
    throw new Error('error');
  } catch (error) {
    console.error('error', error.message);
    throw error;
  } finally {
    console.log('finally block is run');
  }
} catch (error) {
  console.error('outer catch block caught', error.message);
}

As we can see, if we ran the code above, then we get the following logged in order: ‘error error’, ‘finally block is run’, and ‘outer catch block caught error’. This is because the inner catch block logged the exception thrown by throw new Error(‘error’) , but then after console.error(‘error’, error.message); is ran, we ran throw error; to throw the exception again. Then the inner finally block is run and then the rethrown exception is caught by the outer catch block which logged the error that was rethrown by the throw error statement in the inner catch block.

Since the code runs sequentially, we can run return statements at the end of a try block. For example, if we want to parse a JSON string into an object we we want to return an empty object if there’s an error parsing the string passed in, for example, when the string passed in isn’t a valid JSON string, then we can write the following code:

const parseJSON = (str) => {
  try {
    return JSON.parse(str);
  }
  catch {
    return {};
  }
}

In the code above, we run JSON.parse to parse the string and if it’s not valid JSON, then an exception will be thrown. If an exception is thrown, then the catch clause will be invokes to return an empty object. If JSON.parse successfully runs then the parsed JSON object will be returned. So if we run:

console.log(parseJSON(undefined));
console.log(parseJSON('{"a": 1}'))

Then we get an empty object on the first line and we get {a: 1} on the second line.

Try Block in Asynchronous Code

With async and await , we can shorten promise code. Before async and await, we have to use the then function, we make to put callback functions as an argument of all of our then functions. This makes the code long is we have lots of promises. Instead, we can use the async and await syntax to replace the then and its associated callbacks as follows. Using the async and await syntax for chaining promises, we can also use try and catch blocks to catch rejected promises and handle rejected promises gracefully. For example , if we want to catch promise rejections with a catch block, we can do the following:

(async () => {
  try {
    await new Promise((resolve, reject) => {
      reject('error')
    })
  } catch (error) {
    console.log(error);
  }

})();

In the code above, since we rejected the promise that we defined in the try block, the catch block caught the promise rejection and logged the error. So we should see ‘error’ logged when we run the code above. Even though it looks a regular try...catch block, it’s not, since this is an async function. An async function only returns promises, so we can’t return anything other than promises in the try...catch block. The catch block in an async function is just a shorthand for the catch function which is chained to the then function. So the code above is actually the same as:

(() => {
  new Promise((resolve, reject) => {
      reject('error')
    })
    .catch(error => console.log(error))
})()

We see that we get the same console log output as the async function above when it’s run.

The finally block also works with the try...catch block in an async function. For example, we can write:

(async () => {
  try {
    await new Promise((resolve, reject) => {
      reject('error')
    })
  } catch (error) {
    console.log(error);
  } finally {
    console.log('finally is run');
  }
})();

In the code above, since we rejected the promise that we defined in the try block, the catch block caught the promise rejection and logged the error. So we should see ‘error’ logged when we run the code above. The the finally block runs so that we get ‘finally is run’ logged. The finally block in an async function is the same as chaining the finally function to the end of a promise so the code above is equivalent to:

(() => {
  new Promise((resolve, reject) => {
      reject('error')
    })
    .catch(error => console.log(error))
    .finally(() => console.log('finally is run'))
})()

We see that we get the same console log output as the async function above when it’s run.

The rules for nested try...catch we mentioned above still applies to async function, so we can write something like:

(async () => {
  try {
    await new Promise((resolve, reject) => {
      reject('outer error')
    })
    try {
      await new Promise((resolve, reject) => {
        reject('inner error')
      })
    } catch (error) {
      console.log(error);
    } finally {

    }
  } catch (error) {
    console.log(error);
  } finally {
    console.log('finally is run');
  }
})();

This lets us easily nest promises and and handle their errors accordingly. This is cleaner than chaining the then , catch and finally functions that we did before we have async functions.

To handle errors in JavaScript programs, we can use the try...catch...finally blocks to catch errors. This can be done with synchronous or asynchronous code. We put the code that may throw exceptions in the try block, then put the code that handles the exceptions in the catch block. In the finally block we run any code that runs regardless of whether an exception is thrown. async functions can also use the try...catch block, but they only return promises like any other async function, but try...catch...finally blocks in normal functions can return anything.

Categories
JavaScript Basics

Easily Iterate Over JavaScript Collections with the For-Of Loop

Starting with ES2015, we have a new kind of loop to loop over iterable objects. The new for…of loop is a new kind of loop that lets us…

Starting with ES2015, we have a new kind of loop to loop over iterable objects. The new for...of loop is a new kind of loop that lets us loop over any iterable objects without using a regular for loop, while loop, or using the forEach function in the case of arrays. It can be used directly to iterate through any iterable objects, which include built in objects like Strings, Arrays, array-like objects like arguments and NodeList , TypedArray , Map , Set and any user-defined iterables. User-defined iterables include entities like generators and iterators.

If we want to use the for...of loop to iterate over an iterable object, we can write it with the following syntax:

for (variable of iterable){
  // run code
}

The variable in the code above is the variable representing each entry of the iterable object that are being iterated over. It can be declared with const , let or var . The iterable is the object where the properties are being iterated over.

For example, we can use it to iterate over an array like in the following code:

const arr = [1,2,3];

for (const num of arr) {
  console.log(num);
}

The code above, the console.log statements will log 1, 2, and 3. We can replace const with let if we want to assign the variable we used for iteration in the for...of loop. For example, we can write:

const arr = [1,2,3];

for (let num of arr) {
  num *= 2 ;
  console.log(num);
}

The code above, the console.log statements will log 2, 4, and 6 since we used the let keyword to declare the num so we can modify num in place by multiplying each entry by 2. We cannot reassign with const so we have to use let or var to declare the variable we want to modify in each iteration.

We can also iterate over strings. If we do that we all get each character of the string in each iteration. For example, if we have the code below:

const str = 'string';

for (const char of str) {
  console.log(char);
}

Then we get the individual characters of 'string' logged in each line.

Likewise, we can iterate over TypedArrays, which contains binary data represented by a series of numbers in hexadecimal format. For example, we can write the following code:

const arr = new Uint8Array([0x00, 0x2f]);`

for (const num of arr) {
  console.log(num);
}`

In the example above, console.log will log 0 and 47. Note that the logged value is in decimal format but the entered value is in hexadecimal format.

If we iterate over Maps, then we get each entry of the Map. For example, we can write the following code to iterate over Maps:

const map = new Map([['a', 2], ['b', 4], ['c', 6]]);

for (const entry of map) {
  console.log(entry);
}

If we log the entries, we get ['a', 2], ['b', 4], and ['c', 6] . Maps consists of key-value pairs as their entries. When we iterate over a Map, we get the key as the first element and the value as the second element is each entry. To get the key and value of each entry into its own variable we can use the destructuring operator, like in the following code:

const map = new Map([['a', 2], ['b', 4], ['c', 6]]);

for (const [key, value] of map) {
  console.log(key, value);
}

Then when we log the entries, we get 'a' 2, 'b' 4, and 'c' 6 .

We can also use the for...of loop for Sets. For example, we can loop over a Set by doing the following:

const set = new Set([1, 1, 2, 3, 3, 4, 5, 5, 6]);

for (const value of set) {
  console.log(value);
}

We set that we get 1, 2, 3, 4, 5, and 6 logged since the Set constructor automatically eliminates duplicate entries by keeping the first occurrence a value in the Set and discarding the later occurrence of the same value.

The for...of loop also works for iterating over the arguments object, which is an global object that has the arguments that were passed into the function when the function is called. For example, if we write the following code:

(function() {
  for (const argument of arguments) {
    console.log(argument);
  }
})(1, 2, 3, 4, 5, 6);

We see that we see 1, 2, 3, 4, 5, and 6 logged since this is what we passed in when we called the function. Note that this only works for regular functions since the context of this has to be changed to the function being called instead of window . Arrow functions doesn’t change the content of this , so we won’t get the correct arguments when we run the same loop inside an arrow function.

Also, we can iterate over a list of DOM Node objects, called a NodeList . For example, is a browser implemented the NodeList.prototype[Symbol.iterator] , then we can use the for...of loop like in the following code:

const divs = document.querySelectorAll('div');

for (const div of divs) {
  console.log(div);
}

In the code above we logged all the div elements that are in the document.

With for...of loops, we can end the loop by using the break , throw or return statements. The iterator will close in this case, but the execution will continue outside the loop. For example, if we write:

function* foo(){
  yield 'a';
  yield 'b';
  yield 'c';
};

for (const o of foo()) {
  console.log(o);
  break;
}

console.log('finished');

In the code above, we only log ‘a’ because we have a break statement at the end of the for...of loop, so after the first iteration, the iterator will close and the loop ends.

We can loop over generators, which are special functions that returns a generator function. The generator function returns the next value of an iterable object. It’s used for letting us iterate through a collection of objects by using the generator function in a for...of loop.

We can also loop over a generator that generate infinite values. We can have an infinite loop inside the generator to keep returning new values. Because the yield statement doesn’t run until the next value is requested, we can keep an infinite loop running without crashing the browser. For example, we can write:

function* genNum() {
  let index = 0;
  while (true) {
    yield index += 2;
  }
}

const gen = genNum();
for (const num of gen) {
  console.log(num);
  if (num >= 1000) {
    break;
  }
}

If we run the code above, we see that we get numbers from 2 to 1000 logged. Then num is bigger than 1000, so that the break statement is ran. We cannot reuse the generator after it’s closed, so if we write something like the following:

function* genNum() {
  let index = 0;
  while (true) {
    yield index += 2;
  }
}

const gen = genNum();
for (const num of gen) {
  console.log(num);
  if (num >= 1000) {
    break;
  }
}

for (const num of gen) {
  console.log(num);
  if (num >= 2000) {
    break;
  }
}

The second loop won’t run because the iterator that was generated by the generator is already closed by the first loop with the break statement.

We can iterate over other iterable objects that have the method denoted with the Symbol.iterator Symbol defined. For example, if we have the following iterable object defined:

const numsIterable = {
  [Symbol.iterator]() {
    return {
      index: 0,
      next() {
        if (this.index < 10) {
          return {
            value: this.index++,
            done: false
          };
        }
        return {
          value: undefined,
          done: true
        };
      }
    };
  }
};

Then we can run the loop below to show log the generated results:

for (const value of numsIterable) {
  console.log(value);
}

When we run it, should see 0 to 9 logged when console.log is run in the loop above.

It’s important that we don’t confuse the for...of loop with the for...in loop. The for...in loop if for iterating over the top-level keys of objects including anything up the prototype chain, while the for...of loop can loop over any iterable object like arrays, Sets, Maps, the arguments object, the NodeList object, and any user-defined iterable objects.

For example, if we have something like:

Object.prototype.objProp = function() {};
Array.prototype.arrProp = function() {};

const arr = [1, 2, 3];
arr.foo = 'abc';

for (const x in arr) {
  console.log(x);
}

Then we get 0, 1, 2, ‘foo’, ‘arrProp’ and ‘objProp’ logged, which are keys of objects and methods that are defined for the arr object. It included all properties and methods up the prototype chain. It inherited all properties and methods from Object and Array that were added to Object and Array’s prototype so we get all the things in the chain inheritance in the for...in loop. Only enumerable properties are logged in the arr object in arbitrary order. It logs index and properties we defined in Object and Array like objProp and arrProp .

To only loop through properties that aren’t inheritance from an object’s prototype, we can use the hasOwnPropetty to check if the property is defined on the own object:

Object.prototype.objProp = function() {};
Array.prototype.arrProp = function() {};

const arr = [1, 2, 3];
arr.foo = 'abc';

for (const x in arr) {
  if (arr.hasOwnProperty(x)){
    console.log(x);
  }
}

objProp and arrProp are omitted because they’re they’re inherited from Object and Array objects respectively.

The for...of loop is a new kind of loop that lets us loop over any iterable objects without using a regular for loop, while loop, or using the forEach function in the case of arrays. It can be used directly to iterate through any iterable objects, which include built in objects like Strings, Arrays, array-like objects like arguments and NodeList , TypedArray , Map , Set and any user-defined iterables. User-defined iterables include entities like generators and iterators. This is a handy loop because it lets us over any iterable object rather than just arrays. Now we have a loop statement that works with iterable object.

Categories
JavaScript JavaScript Basics

Using JavaScript BigInt to Represent Large Numbers

To represent integers larger than 2**53 – 1 in JavaScript, we can use the BigInt object to represent the values.

It can be manipulated via normal operations like arithmetic operators — addition, subtraction, multiplication, division, remainder, and exponentiation.

It can be constructed from numbers and hexadecimal or binary strings. Also, it supports bitwise operations like AND, OR, NOT, and XOR. The only bitwise operation that doesn’t work is the zero-fill right-shift operator (>>> ) because BigInts are all signed.

Also, the unary + operator isn’t supported to not break asm.js. These operations are only done when all the operands are BigInts. We can’t have some operands being BigInts and some being numbers.

In JavaScript, a BigInt is not the same as a normal number. It’s distinguished from a normal number by having an n at the end of the number. We can define a BigInt with the BigInt factory function.

It takes one argument that can be an integer number or a string representing a decimal integer, hexadecimal string, or binary string. BigInt cannot be used with the built-in Math object.

Also, when converting between number and BigInt and vice versa, we have to be careful because the precision of BigInt might be lost when a BigInt is converted into a number.


Usage

To define a BigInt, we can write the following if we want to pass in a whole number:

const bigInt = BigInt(1);  
console.log(bigInt);

It would log 1n when we run the console.log statement. If we want to pass a string into the factory function instead, we can write:

const bigInt = BigInt('2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222');  
console.log(bigInt);

We can also pass an hexadecimal number string with a string that starts with 0x into the factory function:

const bigHex = BigInt("0x1fffffffffffff111111111");  
console.log(bigHex);

The code above would log 618970019642690073311383825n when we run console.log on bigHex.

Likewise, we can pass in a binary string with a string that starts with 0b and binary string in the remainder of the string to get a BigInt:

const bigBin = BigInt("0b111111111111111000000000011111111111111111111111");  
console.log(bigBin);

The code above would get us 281466395164671n when we run console.log on it. Passing in strings would be handy if the BigInt that we want to create is outside of the range that can be accepted by the number type.

We can also define a BigInt with a BigInt literal, we can just attach an n character to the whole number. For example, we can write:

const bigInt = 22222222222222222222222222222222n;  
console.log(bigInt);

Then, we get 22222222222222222222222222222222n as the value of bigInt when we log the value.

BigInt has its own data type in JavaScript. When you run the typeof operator on a BigInt variable, constant or value, we would get bigint. For example, when we run:

typeof 2n === 'bigint';  
typeof BigInt(2) === 'bigint';

Both lines of code would evaluate to true. However, if we wrap it with the Object factory function, we get that the type is an object:

typeof Object(2n) === 'object';

The code above would evaluate to true.

We can apply number operations to BigInts.

This includes the arithmetic operators — addition, subtraction, multiplication, division, remainder, and exponentiation. Also, it supports bitwise operations like AND, OR, NOT, and XOR.

The only bitwise operation that doesn’t work is the zero-fill right-shift operator (>>> ) because BigInts are all signed. Also, the unary + operator isn’t supported to not break asm.js.

These operations are only done when all the operands are BigInts. We can’t have some operands being BigInts and some being numbers. For example, if we have:

const bigInt = BigInt(Number.MAX_SAFE_INTEGER);  
console.log(bigInt);
const biggerInt = bigInt + BigInt(1);  
console.log(biggerInt);
const evenBiggerInt = bigInt + BigInt(2);  
console.log(evenBiggerInt);
const multi = bigInt * BigInt(2);  
console.log(multi);
const subtr = bigInt - BigInt(10);  
console.log(subtr);
const remainder = bigInt % BigInt(1);  
console.log(remainder);
const bigN = bigInt ** BigInt(54);  
console.log(bigN);
const veryNegativeNum = bigN * -BigInt(1)  
console.log(veryNegativeNum);

We get 9007199254740991n for bigInt, 9007199254740992n for biggerInt, 9007199254740993n for evenBiggerInt, 18014398509481982n for multi, 9007199254740981n for subtr, 0n for remainder, 3530592467533273200243077170885155617107348521747142286627863260349958518655132050034081285541183004983189865471543609006121689601641796259277395721973496380268998810860889580999688899063966604079229944616948651888866122410855207004436640389001057851295873774080462273415460559916461808220601907673652198075821210257903676343961872269549414664419834643799298966710366275919846143068708381391506113181640387818197335712192797007703730122048543818655729529755334964590919971124632271934272078761071878238334159341746985273963326734748343552398522547662400805644304911487571159654254814460707275228515191584712593238883953404971043549757327554636466197405269908317698331974392008288867249576664013677011521696874812515379689360830743272800013459321098384864332719963035293422648481458040217707301509007592199565531403472471705983351384755965631442881685949576642561n for bigN, and -3530592467533273200243077170885155617107348521747142286627863260349958518655132050034081285541183004983189865471543609006121689601641796259277395721973496380268998810860889580999688899063966604079229944616948651888866122410855207004436640389001057851295873774080462273415460559916461808220601907673652198075821210257903676343961872269549414664419834643799298966710366275919846143068708381391506113181640387818197335712192797007703730122048543818655729529755334964590919971124632271934272078761071878238334159341746985273963326734748343552398522547662400805644304911487571159654254814460707275228515191584712593238883953404971043549757327554636466197405269908317698331974392008288867249576664013677011521696874812515379689360830743272800013459321098384864332719963035293422648481458040217707301509007592199565531403472471705983351384755965631442881685949576642561n for veryNegativeNum.

Note that when we get fractional results, the fractional parts will be truncated when result has it. BigInt is a big integer and it’s not for storing decimals. For example, in the examples below:

const expected = 8n / 2n;  
console.log(expected)const rounded = 9n / 2n;  
console.log(rounded)

We get 4n for expected and rounded. This is because the fractional part is removed from the BigInt.

Comparison operators can be applied to BigInts. The operands can be either BigInt or numbers. We can use the bigger than, bigger than or equal to, less than, less than or equal to, double equals, and triple equals operators with BigInts.

For example, we can compare 1n to 1:

1n === 1

The code above would evaluate to false because BigInt and numbers aren’t the same types. But when we replace triple equals with double equals, like in the code below:

1n == 1

The statement above evaluates to true because only the value is compared.

Note that in both examples, we mixed BigInt operands with number operands. This is allowed for comparison operators.

BigInts and numbers can be compared together with other operators as well, like in the following examples:

1n < 9  
// true9n > 1  
// true9 > 9n  
// false  
  
9n > 9  
// false  
  
9n >= 9  
// true

Also, they can be mixed together in one array and sorted together. For example, if we have the following code:

const mixedNums = [5n, 6, -120n, 12, 24, 0, 0n];  
mixedNums.sort();  
console.log(mixedNums)

We get [-120n, 0, 0n, 12, 24, 5n, 6]. We also sort it in descending order with the following code:

const mixedNums = [5n, 6, -120n, 12, 24, 0, 0n];  
mixedNums.sort((a, b) => {  
  if (a > b) {  
    return -1  
  } else if (a < b) {  
    return 1  
  }  
  return 0  
});  
console.log(mixedNums)

In the code above, we used the comparison operators to compare the value of the numbers and return -1 if the first number is bigger than the second number, return 1 if the first number is less than the second number, and return 0 otherwise.

If we wrap BigInts with Object, then they’re compared as objects, so two objects are only considered the same if the same instance is referenced. For example, if we have:

0n === Object(0n);  
Object(0n) === Object(0n);  
const o = Object(0n);  
o === o;

Then the first three lines in the code above would evaluate to false while the last line would evaluate to true.

When BigInts are coerced into booleans, they act the same as if they’re numbers. For example, Boolean(0n) would return false, and anything else would return true.

For example, we can coerce them into booleans like in the following code:

0n || 11n  
// 11n  
  
0n && 11n  
// 0n  
  
Boolean(0n)  
// false  
  
Boolean(11n)  
// true  
  
!11n  
// false  
  
!0n  
// true

BigInt Methods

The BigInt object has several methods. There are the static asIntN() and asUintN() methods, and the toLocaleString(), toString(), and valueOf() instance methods.

The asIntN method wraps a BigInt value between -2 to the width minus 1 and 2 to the width minus 1. For example, if we have:

const bigNum = 2n ** (62n - 1n) - 1n;  
console.log(BigInt.asIntN(62, bigNum));

Then we get the literal of the actual value of bigNum modulo 62 returned, which is 2305843009213693951n. However, if we have:

const bigNum = 2n ** (63n - 1n) - 1n;  
console.log(BigInt.asIntN(62, bigNum));

We get -1n since the 2n ** (63n — 1n) modulo 2n ** 63n is returned.

The asUintN() method wraps a BigInt value between 0 and 2 to the width minus 1. For example, if we have:

const bigNum = 2n ** 11n - 1n;  
console.log(BigInt.asUintN(11, bigNum));

Then we get the literal of the actual value of bigNum returned, which is 2305843009213693951n. However, if we have:

const bigNum = 2n ** 11n - 1n;  
console.log(BigInt.asUintN(11, bigNum));

We get 2047n since the 2n ** 11n — 1n modulo 2n ** 11n is returned.

BigInt has a toLocaleString() method to return the value of the string for the BigInt depending on the locale we pass in. For example, if we want to get the French representation of a BigInt, we can write:

const bigNum = 2n ** 60n - 1n;  
console.log(bigNum.toLocaleString('fr'));

The code above will log 1 152 921 504 606 846 975.

The toString() method will convert a BigInt to a string. For example, we can write:

const bigNum = 2n ** 60n - 1n;  
console.log(bigNum.toString());

The code above will log 1152921504606846975.

The valueOf method will get the value of the BigInt object. For example, if we run:

const bigNum = 2n ** 60n - 1n;  
console.log(bigNum.valueOf());

Then we get 1152921504606846975n logged.

BigInts aren’t supported by JSON, so a TypeError would be raised if we try to convert it to JSON with JSON.stringify().

However, we can convert it to something supported like a string, then it can be stored as JSON. We can override the toJSON method of a BigInt by writing:

BigInt.prototype.toJSON = function() {  
  return this.toString();  
}

const bigIntString = JSON.stringify(88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888n)  
console.log(bigIntString);

Then, we get: “88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888”.

BigInts are useful for representing integers larger than 2 to the 53rd power minus 1 in JavaScript, we can use the BigInt object to represent the values.

It can be manipulated via normal operations like arithmetic operators — addition, subtraction, multiplication, division, remainder, and exponentiation.

BigInts support most bitwise operations like AND, OR, NOT, and XOR.

They can also be converted from hexadecimal or binary numbers.

These operations are only done when all the operands are BigInts. We can’t have some operands being BigInts and some being numbers.

In JavaScript, a BigInt is not the same as a normal number. It’s distinguished from a normal number by having an n at the end of the number. We can define a BigInt with the BigInt factory function.

It takes one argument that can be an integer number or a string representing a decimal integer, hexadecimal string, or binary string. BigInt cannot be used with the built-in Math object.

Categories
JavaScript JavaScript Basics

Comparing Non-English Strings with JavaScript Collators

With the combination of the double equal or triple equal operator with string methods, we can compare strings easily in a case-sensitive or case insensitive manner. However, this doesn’t take into account the characters that are in non-English strings like French or Italian. These languages have alphabets that may contain accents, something that isn’t recognized in normal string comparisons.

To handle this scenario, we can use the Intl.Collator object to compare strings with accents or for different locales. The Intl.Collator object is a constructor for collators, which are objects that let us compare characters in a language-sensitive way. With Collators, we can compare the order of single characters according to the language that is specified.

Basic Collator Usage for String Equality Comparison

To use a collator, we can construct a Collator object and then use its compare method. The compare method does a comparison of the alphabetical order of the entire string based on the locale. For example, if we want to compare two strings in the German using its alphabet’s order, we can write the following code:

const collator = new Intl.Collator('de');  
const order = collator.compare('Ü', 'ß');  
console.log(order);

We created the Collator object by writing new Intl.Collator(‘de’) to specify that we are comparing strings in the German alphabet. Then we use the created compare method, which takes two parameters as the two strings that you want to compare in string form.

Then a number is returned from the compare function. 1 is returned if the string in the first parameter comes after the second one alphabetically, 0 if both strings are the same, and -1 is returned if the string in the first parameter comes before the second string alphabetically.

So if we flip the order of the strings like in the code below:

const collator = new Intl.Collator('de');  
const order = collator.compare('ß', 'Ü');  
console.log(order);

Then the console.log outputs -1.

If they’re the same, like in the following code:

const collator = new Intl.Collator('de');  
const order = collator.compare('ß', 'ß');  
console.log(order);

Then we get 0 returned for order.

To summarize: If the strings are equal, the function returns 0. If they are not equal the function returns either 1 or -1 which also indicates the alphabetical order of the strings.

Advanced Usage

The Collator is useful because we can put it in the Array.sort method as a callback function to sort multiple strings in the array. For example, if we have multiple German strings in an array, like in the code below:

const collator = new Intl.Collator('de');  
const sortedLetters = ['Z', 'Ä', 'Ö', 'Ü', 'ß'].sort(collator.compare);  
console.log(sortedLetters);

Then we get [“Ä”, “Ö”, “ß”, “Ü”, “Z”].

The constructor takes a number of options that take into account the features of the alphabets of different languages. As we can see above, the first parameter in the constructor is the locale, which is BCP-47 language tag, or an array of such tags. This is an optional parameter. An abridged list of BCP-47 language tags include:

  • ar — Arabic
  • bg — Bulgarian
  • ca — Catalan
  • zh-Hans — Chinese, Han (Simplified variant)
  • cs — Czech
  • da — Danish
  • de — German
  • el — Modern Greek (1453 and later)
  • en — English
  • es — Spanish
  • fi — Finnish
  • fr — French
  • he — Hebrew
  • hu — Hungarian
  • is — Icelandic
  • it — Italian
  • ja — Japanese
  • ko — Korean
  • nl — Dutch
  • no — Norwegian
  • pl — Polish
  • pt — Portuguese
  • rm — Romansh
  • ro — Romanian
  • ru — Russian
  • hr — Croatian
  • sk — Slovak
  • sq — Albanian
  • sv — Swedish
  • th — Thai
  • tr — Turkish
  • ur — Urdu
  • id — Indonesian
  • uk — Ukrainian
  • be — Belarusian
  • sl — Slovenian
  • et — Estonian
  • lv — Latvian
  • lt — Lithuanian
  • tg — Tajik
  • fa — Persian
  • vi — Vietnamese
  • hy — Armenian
  • az — Azerbaijani
  • eu — Basque
  • hsb — Upper Sorbian
  • mk — Macedonian
  • tn — Tswana
  • xh — Xhosa
  • zu — Zulu
  • af — Afrikaans
  • ka — Georgian
  • fo — Faroese
  • hi — Hindi
  • mt — Maltese
  • se — Northern Sami
  • ga — Irish
  • ms — Malay (macrolanguage)
  • kk — Kazakh
  • ky — Kirghiz
  • sw — Swahili (macrolanguage)
  • tk — Turkmen
  • uz — Uzbek
  • tt — Tatar
  • bn — Bengali
  • pa — Panjabi
  • gu — Gujarati
  • or — Oriya
  • ta — Tamil
  • te — Telugu
  • kn — Kannada
  • ml — Malayalam
  • as — Assamese
  • mr — Marathi
  • sa — Sanskrit
  • mn — Mongolian
  • bo — Tibetan
  • cy — Welsh
  • km — Central Khmer
  • lo — Lao
  • gl — Galician
  • kok — Konkani (macrolanguage)
  • syr — Syriac
  • si — Sinhala
  • iu — Inuktitut
  • am — Amharic
  • tzm — Central Atlas Tamazight
  • ne — Nepali
  • fy — Western Frisian
  • ps — Pushto
  • fil — Filipino
  • dv — Dhivehi
  • ha — Hausa
  • yo — Yoruba
  • quz — Cusco Quechua
  • nso — Pedi
  • ba — Bashkir
  • lb — Luxembourgish
  • kl — Kalaallisut
  • ig — Igbo
  • ii — Sichuan Yi
  • arn — Mapudungun
  • moh — Mohawk
  • br — Breton
  • ug — Uighur
  • mi — Maori
  • oc — Occitan (post 1500)
  • co — Corsican
  • gsw — Swiss German
  • sah — Yakut
  • qut — Guatemala
  • rw — Kinyarwanda
  • wo — Wolof
  • prs — Dari
  • gd — Scottish Gaelic

For example, de is for German or fr-ca for Canadian French. So, we can sort Canadian French strings by running the following code:

const collator = new Intl.Collator('fr-ca');  
const sortedLetters = ['ç', 'à', 'c'].sort(collator.compare);  
console.log(sortedLetters);

The constructor to Collator can also take an array of strings for multiple locale comparison — new Intl.Collator([/* local strings */]). The array argument allows us to sort strings from multiple locales. For example, we can sort both Canadian French alphabet and the German alphabet at the same time:

const collator = new Intl.Collator(['fr-ca', 'de']);  
const sortedLetters = [  
  'Ü', 'ß', 'ç', 'à', 'c'  
].sort(collator.compare);
console.log(sortedLetters);

Then we get [“à”, “c”, “ç”, “ß”, “Ü”] from the console.log statement.

Additional Options

Unicode extension keys which include "big5han", "dict", "direct", "ducet", "gb2312", "phonebk", "phonetic", "pinyin", "reformed", "searchjl", "stroke", "trad", "unihan" are also allowed in our locale strings. They specify the collations that we want to compare strings with. However, when there are fields in the options in the second argument that overlaps with this, then the options in the argument overrides the Unicode extension keys specified in the first argument.

Numerical collations can be specified by adding kn to your locale string in your first argument. For example, if we want to compare numerical strings, then we can write:

const collator = new Intl.Collator(['en-u-kn-true']);  
const sortedNums = ['10', '2'].sort(collator.compare);  
console.log(sortedNums);

Then we get [“2”, “10”] since we specified kn in the locale string in the constructor which makes the collator compare numbers.

Also, we can specify whether upper or lower case letters should be sorted first with the kf extension key. The possible options are upper, lower, or false. false means that the locale’s default will be the option. This option can be set in the locale string by adding as a Unicode extension key, and if both are provided, then the option property will take precedence. For example, to make uppercase letters have precedence over lowercase letters, we can write:

const collator = new Intl.Collator('en-ca-u-kf-upper');  
const sorted = ['Able', 'able'].sort(collator.compare);  
console.log(sorted);

This sorts the same word with upper case letters first. When we run console.log, we get [“Able”, “able”] since we have an uppercase ‘A’ in ‘Able’, and a lowercase ‘a’ for ‘able’. On the other hand, if we instead pass in en-ca-u-kf-lower in the constructor like in the code below:

const collator = new Intl.Collator('en-ca-u-kf-lower');  
const sorted = ['Able', 'able'].sort(collator.compare);  
console.log(sorted);

Then after console.log we get [“able”, “Able”] because kf-lower means that we sort the same word with lowercase letters before the ones with uppercase letters.

The second argument of the constructor takes an object that can have multiple properties. The properties that the object accepts are localeMatcher, usage, sensitivity, ignorePunctuation, numeric, and caseFirst. numeric is the same as the kn option in the Unicode extension key in the locale string, and caseFirst is the same as the kf option in the Unicode extension key in the locale string. The localeMatcher option specifies the locale matching algorithm to use. The possible values are lookup and best fit. The lookup algorithm searches for the locale until it finds the one that fits the character set of the strings that are being compared. best fit finds the locale that is at least but possibly more suited that the lookup algorithm.

The usage option specifies whether the Collator is used for sorting or searching for strings. The default option is sort.

The sensitivity option specifies the way that the strings are compared. The possible options are base, accent, case, and variant.

base compares the base of the letter, ignoring the accent. For example a is not the same as b, but a is the same as á, a is the same as Ä.

accent specifies that a string is only different if there is a base letter or their accents are unequal then they’re unequal, ignoring case. So a isn’t the same as b, but a is the same as A. a is not the same as á.

The case option specifies that strings that are different in their base letters or case are considered unequal, so a wouldn’t be the same as A and a wouldn’t be the same as c, but a is the same as á.

variant means that strings that are different in the base letter, accent, other marks, or case are considered unequal. For example a wouldn’t be the same as A and a wouldn’t be the same as c. But also a wouldn’t be the same as á.

The ignorePunctuation specifies whether punctuation should be ignored when sorting strings. It’s a boolean property and the default value is false.

We can use the Collator constructor with the second argument in the following way:

const collator = new Intl.Collator('en-ca', {  
  ignorePunctuation: false,  
  sensitivity: "base",  
  usage: 'sort'  
});  
console.log(collator.compare('Able', 'able'));

In the code above, we sort by checking for punctuation and only consider letters different if the base letter is different, and we keep the default that upper case letters are sorted first, so we get [‘Able’, ‘able’] in the console.log.

We can search for strings as follows:

const arr = ["ä", "ad", "af", "a"];  
const stringToSearchFor = "af";
const collator = new Intl.Collator("fr", {  
  usage: "search"  
});  
const matches = arr.filter((str) => collator.compare(str, stringToSearchFor) === 0);  
console.log(matches);

We set the usage option to search to use the Collator to search for strings and when the compare method returns 0, then we know that we have the same string. So we get [“af”] logged when we run console.log(matches).

We can adjust the options for comparing letter, so if we have:

const arr = ["ä", "ad", "ef", "éf", "a"];  
const stringToSearchFor = "ef";
const collator = new Intl.Collator("fr", {  
  sensitivity: 'base',  
  usage: "search"  
});
const matches = arr.filter((str) => collator.compare(str, stringToSearchFor) === 0);
console.log(matches);

Then we get [“ef”, “éf”] in our console.log because we specified sensitivity as base which means that we consider the letters with the same base accent as the same.

Also, we can specify the numeric option to sort numbers. For example, if we have:

const collator = new Intl.Collator(['en-u-kn-false'], {  
  numeric: true  
});  
const sortedNums = ['10', '2'].sort(collator.compare);  
console.log(sortedNums);

Then we get [“2”, “10”] because the numeric property in the object in the second argument overrides the kn-false in the first argument.

Conclusion

JavaScript offers a lot of string comparison options for comparing strings that aren’t in English. The Collator constructor in Intl provides many options to let us search for or sort strings in ways that can’t be done with normal comparison operators like double or triple equals. It lets us order numbers, and consider cases, accents, punctuation, or the combination of those features in each character to compare strings. Also, it accepts locale strings with key extensions for comparison.

All of these options together make JavaScript’s Collator constructor a great choice for comparing international strings.