M.Hiroi's Home Page

JavaScript Programming

お気楽 JavaScript プログラミング超入門


Copyright (C) 2017-2025 Makoto Hiroi
All rights reserved.

リストで遊ぼう

前回作成した連結リストを使った簡単な問題集です。

  1. リスト xs はリスト ys よりも長いか調べるメソッド longer(xs, ys) を定義してください。
  2. リスト xs の最後尾から n 個の要素を取り除くメソッド butlast(xs, n) を定義してください。
  3. リスト xs を長さ n の部分リストに分割するメソッド group(xs, n) を定義してください。
  4. リスト xs の n 番目から m - 1 番目までの要素を部分リストとして取り出すメソッド subList(xs, n, m) を定義してください。
  5. 2 つのリスト xs, ys の要素をリストに格納し、それをリストに格納して返すメソッド zip(xs, ys) を定義してください。
  6. zip() で生成したリスト xs を 2 つリストに分離するメソッド unzip(xs) を定義してください。
  7. 連想リスト xs からキー key を探索するメソッド assoc(xs, key) を定義してください。
  8. リスト xs の要素を述語 pred で二分割するメソッド partition(pred, xs) を定義してください。
  9. リスト xs をクイックソートするメソッド quickSort(xs) を定義してください。
  10. 整列済みの 2 つのリスト xs, ys をマージするメソッド mergeList(xs, ys) を定義してください。
  11. リスト xs をマージソートするメソッド mergeSort(xs) を定義してください。
  12. リスト xs から n 個の要素を選ぶ順列を求めるメソッド permutations(n, xs) を定義してください。
  13. リスト xs から n 個の要素を選ぶ組み合わせを求めるメソッド combinations(n, xs) を定義してください。
  14. リスト xs のべき集合を求めるメソッド powerSet(xs) を定義してください。
  15. n 以下の素数を求めるメソッド sieve(n) を定義してください。

●解答

//
// playlist.js : 「リストで遊ぼう」の解答
//
// Copyright (c) 2017-2025 Makoto Hiroi
//
// Released under the MIT license
// https://opensource.org/license/mit/
//

// ES2015 モジュール
import List from "./list.js";

const nil = List.nil();

// Q01
function longer(xs, ys) {
  while (List.isCons(xs) && List.isCons(ys)) {
    xs = xs.rest;
    ys = ys.rest;
  }
  return !List.isNil(xs);
}

// Q02
function butlast(xs, n) {
  return xs.reverse().drop(n).nreverse();
}

// Q03
function group(xs, n) {
  let ys = nil;
  while (!List.isNil(xs)) {
    ys = List.cons(xs.take(n), ys);
    xs = xs.drop(n);
  }
  return ys.nreverse();
}

// Q04
function subList(xs, n, m) {
  return xs.drop(n).take(m - n);
}

// Q05
function zip(xs, ys) {
  return xs.map((x, y) => List.list(x, y), ys);
}

// Q06
function unzip(zs) {
  let xs = nil, ys = nil;
  for (let z of zs) {
    xs = List.cons(z.first, xs);
    ys = List.cons(z.rest.first, ys);
  }
  return [xs.nreverse(), ys.nreverse()];
}

// Q07
function assoc(xs, key) {
  return xs.find(x => x.first == key)
}

// Q08
function partition(pred, xs) {
  let a = nil, b = nil;
  for (let x of xs) {
    if (pred(x)) 
      a = List.cons(x, a);
    else
      b = List.cons(x, b);
  }
  return [a.nreverse(), b.nreverse()];
}

// Q09
function quickSort(xs) {
  if (List.isNil(xs)) return xs;
  let pivot = xs.first,
      [a, b] = partition(x => x < pivot, xs.rest),
      ys = quickSort(a),
      zs = quickSort(b);
  return ys.append(List.cons(pivot, zs));
}

// Q10
function mergeList(xs, ys) {
  let zs = nil;
  while (List.isCons(xs) && List.isCons(ys)) {
    if (xs.first <= ys.first) {
      zs = List.cons(xs.first, zs);
      xs = xs.rest;
    } else {
      zs = List.cons(ys.first, zs);
      ys = ys.rest;
    }
  }
  return zs.nreverse(List.isNil(xs) ? ys : xs);
}

// Q11
function mergeSortSub(xs, n) {
  if (n == 0) {
    return nil;
  } else if (n == 1) {
    return List.list(xs.first);
  } else {
    let m = Math.floor(n / 2),
        ys = mergeSortSub(xs, m),
        zs = mergeSortSub(xs.drop(m), n - m);
    return mergeList(ys, zs);
  }
}

function mergeSort(xs) {
  return mergeSortSub(xs, xs.length());
}

// Q12
function permutations(n, xs) {
  if (n == 0)
    return List.list(nil);
  else
    return xs.flatMap(x => permutations(n - 1, xs.filter(y => x != y)).map(z => List.cons(x, z)));
}

// Q13
function combinations(n, xs) {
  if (n == 0) {
    return List.list(nil);
  } else if (n == xs.length()) {
    return List.list(xs);
  } else {
    return combinations(n - 1, xs.rest)
      .map(ys => List.cons(xs.first, ys))
      .append(combinations(n, xs.rest));
  }
}

// Q14
function powerSet(xs) {
  if (List.isNil(xs)) {
    return List.list(nil);
  } else {
    let ys = powerSet(xs.rest);
    return ys.append(ys.map(zs => List.cons(xs.first, zs)));
  }
}

// Q15
function sieve(n) {
  let xs = List.iterate(n - 1, 2, x => x + 1),
      ys = nil;
  while (true) {
    let p = xs.first;
    if (p * p > n) break;
    ys = List.cons(p, ys);
    xs = xs.rest.filter(x => x % p != 0);
  }
  return ys.nreverse(xs);
}

function iota(n, m) {
  return List.iterate(m - n + 1, n, x => x + 1);
}

// 簡単なテスト
var xs = iota(1, 8);
var ys = iota(1, 9);
console.log("----- Q01 -----");
console.log(longer(ys, xs));
console.log(longer(xs, ys));
console.log(longer(xs, xs));
console.log("----- Q02 -----");
console.log('%s', butlast(xs, 0));
console.log('%s', butlast(xs, 1));
console.log('%s', butlast(xs, 7));
console.log('%s', butlast(xs, 8));
console.log("----- Q03 -----");
console.log('%s', group(xs, 2));
console.log('%s', group(ys, 3));
console.log('%s', group(xs, 4));
console.log('%s', group(ys, 5));
console.log("----- Q04 -----");
console.log('%s', subList(xs, 2, 5));
console.log('%s', subList(xs, 0, 8));
console.log("----- Q05 -----");
var nList1 = zip(iota(1,5), iota(11,15));
var nList2 = zip(iota(1,6), iota(11,15));
var nList3 = zip(iota(1,5), iota(11,16));
console.log('%s', nList1);
console.log('%s', nList2);
console.log('%s', nList3);
console.log("----- Q06 -----");
console.log('%s', unzip(nList1));
console.log("----- Q07 -----");
var aList = List.list(
  List.list("foo", 10),
  List.list("bar", 20),
  List.list("baz", 30),
  List.list("oops", 40));
console.log('%s', assoc(aList, "foo"));
console.log('%s', assoc(aList, "oops"));
console.log(assoc(aList, "FOO"));
console.log("----- Q08 -----");
console.log('%s', partition(x => x % 2 == 0, xs));
console.log('%s', partition(x => x % 2 != 0, ys));
console.log("----- Q09 -----");
var zs = List.list(5,6,4,7,3,8,2,9,1,0);
console.log('%s', quickSort(zs));
console.log('%s', quickSort(ys));
console.log('%s', quickSort(ys.reverse()));
console.log("----- Q10 -----");
var a = List.list(1,3,5,7,9);
var b = List.list(2,4,6,8,10);
console.log('%s', mergeList(a, b));
console.log('%s', mergeList(b, a));
console.log('%s', mergeList(a, a));
console.log("----- Q11 -----");
console.log('%s', mergeSort(zs));
console.log('%s', mergeSort(ys));
console.log('%s', mergeSort(ys.reverse()));
console.log("----- Q12 -----");
console.log('%s', permutations(3, iota(1, 3)));
console.log('%s', permutations(4, iota(1, 4)));
console.log("----- Q13 -----");
console.log('%s', combinations(3, iota(1, 5)));
console.log('%s', combinations(4, iota(1, 5)));
console.log("----- Q14 -----");
console.log('%s', powerSet(iota(1, 3)));
console.log('%s', powerSet(iota(1, 4)));
console.log("----- Q15 -----");
console.log('%s', sieve(100));
console.log('%s', sieve(500));
console.log('%s', sieve(1000));

●実行結果

$ node playlist.js
----- Q01 -----
true
false
false
----- Q02 -----
(1 2 3 4 5 6 7 8)
(1 2 3 4 5 6 7)
(1)
()
----- Q03 -----
((1 2) (3 4) (5 6) (7 8))
((1 2 3) (4 5 6) (7 8 9))
((1 2 3 4) (5 6 7 8))
((1 2 3 4 5) (6 7 8 9))
----- Q04 -----
(3 4 5)
(1 2 3 4 5 6 7 8)
----- Q05 -----
((1 11) (2 12) (3 13) (4 14) (5 15))
((1 11) (2 12) (3 13) (4 14) (5 15))
((1 11) (2 12) (3 13) (4 14) (5 15))
----- Q06 -----
[ (1 2 3 4 5), (11 12 13 14 15) ]
----- Q07 -----
(foo 10)
(oops 40)
false
----- Q08 -----
[ (2 4 6 8), (1 3 5 7) ]
[ (1 3 5 7 9), (2 4 6 8) ]
----- Q09 -----
(0 1 2 3 4 5 6 7 8 9)
(1 2 3 4 5 6 7 8 9)
(1 2 3 4 5 6 7 8 9)
----- Q10 -----
(1 2 3 4 5 6 7 8 9 10)
(1 2 3 4 5 6 7 8 9 10)
(1 1 3 3 5 5 7 7 9 9)
----- Q11 -----
(0 1 2 3 4 5 6 7 8 9)
(1 2 3 4 5 6 7 8 9)
(1 2 3 4 5 6 7 8 9)
----- Q12 -----
((1 2 3) (1 3 2) (2 1 3) (2 3 1) (3 1 2) (3 2 1))
((1 2 3 4) (1 2 4 3) (1 3 2 4) (1 3 4 2) (1 4 2 3) (1 4 3 2) (2 1 3 4) (2 1 4 3)
 (2 3 1 4) (2 3 4 1) (2 4 1 3) (2 4 3 1) (3 1 2 4) (3 1 4 2) (3 2 1 4) (3 2 4 1)
 (3 4 1 2) (3 4 2 1) (4 1 2 3) (4 1 3 2) (4 2 1 3) (4 2 3 1) (4 3 1 2) (4 3 2 1)
)
----- Q13 -----
((1 2 3) (1 2 4) (1 2 5) (1 3 4) (1 3 5) (1 4 5) (2 3 4) (2 3 5) (2 4 5) (3 4 5)
)
((1 2 3 4) (1 2 3 5) (1 2 4 5) (1 3 4 5) (2 3 4 5))
----- Q14 -----
(() (3) (2) (2 3) (1) (1 3) (1 2) (1 2 3))
(() (4) (3) (3 4) (2) (2 4) (2 3) (2 3 4) (1) (1 4) (1 3) (1 3 4) (1 2) (1 2 4)
(1 2 3) (1 2 3 4))
----- Q15 -----
(2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97)
(2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103
107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211
223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331
337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449
457 461 463 467 479 487 491 499)
(2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103
107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211
223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331
337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449
457 461 463 467 479 487 491 499 503 509 521 523 541 547 557 563 569 571 577 587
593 599 601 607 613 617 619 631 641 643 647 653 659 661 673 677 683 691 701 709
719 727 733 739 743 751 757 761 769 773 787 797 809 811 821 823 827 829 839 853
857 859 863 877 881 883 887 907 911 919 929 937 941 947 953 967 971 977 983 991
997)

モジュール

プログラムを作っていると、以前作った関数と同じ処理が必要になる場合があります。いちばんてっとり早い方法はソースファイルからその関数をコピーすることですが、賢明な方法とはいえません。このような場合、自分で作成した関数をライブラリとしてまとめておくと便利です。

ライブラリの作成で問題になるのが「名前の衝突」です。複数のライブラリを使うときに、同じ名前の関数や変数が存在すると、そのライブラリは正常に動作しないでしょう。この問題は「モジュール (module)」を使うと解決することができます。JavaScript は ES2015 からモジュールが導入されました。Node.js は ES2015 だけではなく、CommonJS 形式や AMD 形式などのモジュールをサポートしています。

今回はモジュール [ES2015] の基本的な機能について簡単に説明します。

●export

一般に、JavaScript は一つのファイルで一つのモジュールを定義します。モジュールファイルで変数、関数、クラスなどの定義に exprot を付けると、それらの名前が外部に公開されます。または、ファイルの最後でまとめて export することもできます。簡単な例を示しましょう。

リスト : モジュールの例 (module_foo.js)

export let foo = 1234;
export const bar = 1.2345;
export function baz() { return "baz"; }
リスト : モジュールの例 (module_foo1.js)

let foo = 1234;
const bar = 1.2345;
function baz() { return "baz"; }

export { foo, bar, baz }

module_foo.js には変数 foo, 定数 bar, 関数 baz が定義されています。let, const, function の前に export を付けると、変数名、定数名、関数名が外部に公開されます。ファイルの最後でまとめて export することもできます。module_foo1.js のように、export { ... } の中で公開する名前を書くだけです。

まとめて export する場合、次の構文で公開する名前に別名を付けることができます。

export { name as alais, ... }

name に alais という別名つけて、それを公開します。この場合、name でアクセスすることはできません、

●import

モジュールを読み込むには import 文を使います。

import {name1, name2, ...} from module_file_path

name はモジュールファイルからインポートする名前で、from のあとにモジュールファイルのパスを指定します。たとえば、カレントディレクトリに module_foo.js があり、それをインポートしてみましょう。

リスト : import の使用例 (foo.js)

import { foo, bar, baz } from "./module_foo.js";

console.log(foo);
console.log(bar);
console.log(baz());

import { ... } で module_foo.js がインポートする名前 foo, bar, baz を指定します。これで foo.js の中で foo, bar, baz を使用することができます。それでは実行してみましょう。

$ node foo.js
1234
1.2345
baz

公開されている名前をすべてインポートしたい場合は次の構文を使います。

import * as Module from module_file_path

公開された名前はオブジェクト Module にまとめてインポートされます。

たとえば、module_foo.js をインポート売る場合は次のようになります。

リスト : import の使用例 (foo1.js)

import * as Foo from "./module_foo.js"

console.log(Foo.foo);
console.log(Foo.bar);
console.log(Foo.baz());

インポートされる foo, bar, baz は Foo.foo, Foo.bar, Foo.baz でアクセスすることができます。実際に試してみましょう。

$ node foo1.js
1234
1.2345
baz

export と同様に、import は as を使って名前に別名を付けることができます。

import {name as alias, ...} ...

●連結リストのモジュール化

それでは簡単な例題として、「JavaScript のオブジェクト指向」で作成した連結リスト (linkedlist.js) をモジュールに改造してみましょう。プログラムの修正は簡単です。次のリストを見てください。

リスト : 連結リスト (./modules/linkedlist.js)

// List, FixedList の定義は同じ
... 略 ...

// 簡単なテストは削除

// export を追加
export { List, FixedList }
リスト : 連結リストのテスト (test_linkedlist.js)

// import を追加
import { List, FixedList } from "./modules/linkedlist.js";

// 簡単なテスト
var xs = new List()
console.log(xs.isEmpty());
for (let x = 0; x < 10; x++) xs.add(x, x);
console.log(xs.isEmpty());
console.log('%s', xs)
console.log(xs.nth(0));
console.log(xs.nth(9));
console.log(xs.nth(10));
xs.forEach(console.log);
var s = "";
for (let x of xs) s += x + " ";
console.log(s);
xs.delete(0);
console.log('%s', xs);
xs.delete(8);
console.log('%s', xs);
xs.delete(4);
console.log('%s', xs);
for (let i = 0; i < 10; i++) console.log(xs.find(x => x == i));

var a = new FixedList(4);
for (let i = 0; i < 5; i++) console.log(a.add(0, i));
console.log('%s', a);
for (let i = 0; i < 5; i++) console.log(a.delete(0));
console.log('%s', a);

カレントディレクトリにサブディレクトリ modules を作成し、そこに linkedlist.js を格納します。linkedlist.js はテスト部分を削除して、ファイルの最後に export を追加します。公開するのはクラス List と FixedList です。テスト部分はファイル test_linkedlist.js に移します。そして、先頭に import 文を追加します。インポートするのは List と FixedList で、from の後ろにパス ./modules/linkedlist.js を指定します。これで連結リストを使うことができます。

実際に試してみましょう。

$ node test_linkedlist.js
true
false
List(0,1,2,3,4,5,6,7,8,9)

... 略 ...

0
null
List()

実行結果は同じなので省略します。同様に、二分木 tree.js もモジュール化することができます。

●default export

今まで説明した export は named export (名前付きエクスポート) といいます。モジュールにはもう一つ default export という方法があります。これはモジュールのデフォルト機能を表すもので、モジュールの中で一つだけ定義することができます。定義方法は簡単です。次のリストを見てください。

リスト : default export (module_foo2.js)

let foo = 1234;
const bar = 1.2345;
function baz() { return "baz"; }

export default baz;
export { foo, bar }

module_foo2.js は関数 baz を default export します。export default のあとに公開する名前 baz を一つだけ記述します。これで baz を default export することができます。あとの foo, bar は今までのように export で公開することができます。

default export された名前を import するのは簡単です。次のリストを見てください。

リスト : default export の inport (foo2.js)

import baz from "./module_foo2.js";
import { foo, bar } from "./module_foo2.js";

console.log(foo);
console.log(bar);
console.log(baz());
$ node foo2.js
1234
1.2345
baz

default export された名前を import する場合、import の次に名前を一つ記述するだけです。defualt export はモジュールで一つしかないので、これだけで default export を import することがわかるわけです。このとき、名前は default export とは異なる名前でもかまいません。import oops とすれば、oops() で関数を呼び出すことができます。

簡単な例として、「JavaScript のオブジェクト指向」で作成した二分木 (tree.js) をモジュールに改造してみましょう。tree.js で公開するのはクラス Tree だけなので、default export を使うと簡単です。次のリストを見てください。

リスト : 二分木 (./modules/tree.js)

// Tree の定義は同じ
... 略 ...

// 簡単なテストは削除

// default export を追加
export default Tree;

最後の export で default Tree と記述することで、Tree を default export することができます。

import も簡単です。次のリストを見てください。

リスト : 二分木のテスト (test_tree.js)

import Tree from "./modules/tree.js";

// 簡単なテスト
var a = new Tree();
console.log('%s', a);
for (let x of [5,6,4,7,3,8,2,9,1,0]) {
  a.add(x);
}
console.log('%s', a);
for (let x = -1; x <= 10; x++) {
  console.log(a.contains(x));
}
for (let x = -1; x <= 10; x++) {
  a.delete(x);
    console.log('%s', a);
}
$ node test_tree.js
Tree()
Tree(0,1,2,3,4,5,6,7,8,9)

  ... 略 ...

Tree()
Tree()

import Tree from ... とすることで、tree.js で default export された Tree を名前 Tree で参照することができます。実行結果は同じなので省略させていただきます。

●ブラウザでの使用方法

ブラウザでモジュールを使用した JavaScript ファイルを読み込む場合、type="module" を指定します。次のリストを見てください。

リスト : モジュールの読み込み

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>お気楽 JavaScript プログラミング超入門</title>
</head>
<body>
  <script type="module" src="foo.js"></script>
<h1>M.Hiroi's Home Page</h1>
</body>
</html>

foo.js はモジュール module_foo.js を import するので、 <script type="module" src="foo.js"></script> のように type で "module" を指定します。HTML ファイルの設定はこれだけです。

ただし、ES2015 モジュールの場合、ブラウザがクライアントのファイルシステムから直接モジュールを読み込むことはできません。エラーが発生します。モジュールを読み込むには、サーバーを経由する必要があります。そこで、拙作のページ「お気楽 Node,js 超入門: Node.js の基礎知識」で作成した簡易サーバーを使ってみましょう。

//
// mysever.js : 静的コンテンツ用の簡単なサーバー
//
//               Copyright (C) 2017-2025 Makoto Hiroi
//
const http = require('http'),
      fs = require('fs'),
      url = require('url'),
      path = require('path'),
      mimeType = {
        '.html': 'text/html',
        '.txt': 'text/plain',
        '.css': 'text/css',
        '.js': 'application/javascript',
        '.gif': 'image/gif',
        '.jpg': 'image/jpeg',
        '.png': 'image/png'
      },
      server = http.createServer();

// ファイルの送信
function sendFile(filename, res) {
  fs.readFile('.' + filename, (err, data) => {
    if (err) {
      res.writeHead(404, {'Content-Type': 'text/plain'});
      res.write('not found');
    } else {
      const ext = path.extname(filename);
      res.writeHead(200, {'Content-Type': mimeType[ext]});
      res.write(data);
    }
    res.end();
  });
}

server.on('request', (req, res) => {
  const u = url.parse(req.url);
  if (u.pathname[u.pathname.length - 1] == '/') {
    sendFile(u.pathname + 'index.html', res);
  } else {
    sendFile(u.pathname, res);
  }
});
server.listen(1337, 'localhost');
console.log("server listening...");

mysever.js, foo.js, module_foo.js, index.html を同じディレクトリに配置します。そして、次のコマンドで簡易サーバーを立ち上げます。

$ node mysever.js
server listening...

サーバーを終了するときは CTRL-C を入力してください。ブラウザで http://localhost:1337/ にアクセスすると、M.Hiroi's Home Page が表示されます。そして、JavaScript コンソールを開くと以下の内容が表示されているはずです。

1234
1.2345
baz

簡易サーバーではありますが、簡単なモジュールであれば、ブラウザでも動作確認することができます。

●dynamic import

ES2020 からモジュールを動的に読み込むことができるようになりました。これを「dynamic import (ダイナミックインポート)」といいます。dynamic import は import() で行います。関数のように見えますが、関数ではないので注意してください。

import(module_path) => promise

import() の引数には、モジュールのパス名を文字列で渡します。返り値はプロミスです。モジュールをロードしたあと、プロミスの値はモジュールが export した名前を格納したオブジェクトになります。つまり、公開されている名前をすべてインポートする次の構文と同じです。

import * as Module from module_file_path

dynamic import では次のようになります。

import(module_path).then(Module => ...)

どちらの場合も公開された名前はオブジェクト Module にまとめてインポートされます。

簡単な例を示しましょう。Nod.js の REPL でモジュール module.foo.js を dyanamic import します。

> import('./module_foo.js').then(Module => console.log(Module))
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 103,
  [Symbol(trigger_async_id_symbol)]: 93
}
> [Module: null prototype] {
  bar: 1.2345,
  baz: [Function: baz],
  foo: 1234
}

REPL 上で import 文は使用できませんが、dynamic import ならばモジュールをロードすることができます。

dynamic import は await と一緒に使うと便利です。ES2020 以降、モジュールのトップレベルでは async が無くても await を使用することができるようになりました。これを「Top-level await」といいます。Node.js の REPL でも await を使用することができます。

> const a = await import('./module_foo.js')
undefined
> a
[Module: null prototype] {
  bar: 1.2345,
  baz: [Function: baz],
  foo: 1234
}
> a.bar
1.2345
> a.foo
1234
> a.baz()
'baz'

> const {foo, bar, baz}  = await import('./module_foo.js')
undefined
> foo
1234
> bar
1.2345
> baz()
'baz'

このように、「分割代入」することもできます。default export の場合、default という名前で export されます。分割代入で default に別名を付ければ簡単に使うことができます。

> const m = await import('./module_foo2.js')
undefined
> m
[Module: null prototype] {
  bar: 1.2345,
  default: [Function: baz],
  foo: 1234
}

> const {default: baz} = await import('./module_foo2.js')
undefined
> baz()
'baz'

例外処理

一般に、「例外 (exception)」はエラー処理で使われる機能です。「例外=エラー処理」と考えてもらってもかまいません。最近は例外処理をサポートしているプログラミング言語が多くなりました。もちろん、JavaScript にも例外処理があります。なお、エラーが発生したことを「例外が発生した」とか「例外が送出された」という場合もあります。本ページでもエラーのことを例外と記述することにします。

●例外の捕捉

通常、例外が発生すると JavaScript はプログラムの実行を中止しますが、致命的な例外でなければプログラムの実行を継続する、または特別な処理を行わせたい場合もあるでしょう。このような場合、例外処理がとても役に立ちます。

JavaScript の例外処理は try, catch, throw を使います。try に続くブロック { } 内で例外が送出された場合、そのあとに続く catch でその例外を捕捉することができます。例外を送出するには throw を使います。

try 文の構文を下図に示します。

try {
  処理A;
}
catch (引数) {
  処理B;
}
finally {
  処理C;
}

図 : 例外処理

try 文は、そのあとに定義されている処理 A を実行します。処理 A が正常に終了した場合は try 文も終了します。もしも、処理 A で例外が発生した場合、処理 A の実行は中断されて catch 節を実行します。このとき、catch 節の引数にはエラーを表すオブジェクトが渡されます。

finally 節は try 文で例外が発生したかどうかには関係なく、必ず try 文の最後に実行されます。catch 節と finally 節は、どちらかが定義されていればもう一方の節を省略することができます。つまり、catch 節だけ、または finally 節だけ定義することができます。finally 節はあとで説明します。

●例外の送出

例外は throw で送出することができます。

throw object;

throw が実行されるとプログラムの実行を直ちに中断して、例外を受け止める catch 節を探索します。見つけた場合はそこに制御を移します。このとき、throw で指定したオブジェクトが catch 節の引数に渡されます。catch 節が見つからない場合、プログラムの実行は中断され、Node.js であれば画面にエラーが表示されます。

なお、throw はどんなデータでも例外として送出することができますが、それは非推奨です。基本的には例外を表すクラスのオブジェクト (Error オブジェクト) を送出することが推奨されています。Error オブジェクトは次の式で生成します。

new Error(error_message);

Error は一般的なエラーを表すクラスです。Error 以外の主なクラスを以下に示します。

簡単な例を示しましょう。

< function foo() { throw new Error("oops!"); }
undefined
> function bar() { try { foo(); } catch (e) { console.log(e.name, e.message); }}
undefined
> bar()
Error oops!
undefined
> function bar1() { try { foo(); } catch (e) { console.log(e); }}
undefined
> bar1()
Error: oops!
    at foo (REPL1:1:24)
    at bar1 (REPL18:1:25)
    at REPL19:1:1
    ... 略 ...

undefined

関数 foo で例外を送出し、関数 bar で例外を捕捉します。JavaScript の例外は、try 文の中で呼び出した関数の中で例外が送出されても、関数の呼び出し履歴 (コールスタック) を遡って catch 節を探索し、該当する例外を捕捉することができます。

Error オブジェクトの名前 (クラス名) はプロパティ name に、メッセージはプロパティ message に格納されています。そのほかに、スタックトレースなどデバッグに有用な情報がプロパティ stack にセットされています。なお、スタックトレースの書式は標準化されていないようです。処理系に依存するので注意してください。

●例外の定義

ユーザーが独自のエラークラスを定義するときはクラス Error を継承するといいでしょう。次の例を見てください。

> class FooError extends Error {}
undefined

> try { throw new FooError("oops!"); } catch (e) { console.log(e); }
FooError: oops!
    at REPL11:1:13
    ... 略 ...

FooError は Error を継承しているので、プロパティやメソッドを定義しなくても動作します。独自の処理を行う場合は、プロパティやメソッドを定義する必要がありますが、これは本ページの範囲を超えるので説明を割愛いたします。詳細は JavaScript のマニュアルをお読みください。

node.js の場合、エラー名とメッセージは正しく表示されますが、実際の name プロパティの値は違っています。次の例を見てください。

> try { throw new FooError("oops!"); } catch (e) { console.log(e.name, e.message); }
Error oops!
undefined

name プロパティの値は Error のままです。この場合、FooError のコンストラクタで name の値を書き換えると上手くいきます。実際に試してみましょう。

> class FooError extends Error {
... constructor(...args) {
... super(...args);
... this.name = 'FooError';
... }
... }
undefined
> try { throw new FooError("oops!"); } catch (e) { console.log(e.name, e.message); }
FooError oops!
undefined
> try { throw new FooError("oops!"); } catch (e) { console.log(e); }
FooError: oops!
    at REPL31:1:13
    ... 略 ...

これで正しいエラー名を name にセットすることができます。

●大域脱出

例外処理を使うと、評価中の関数からほかの関数へ制御を移す「大域脱出 (global exit)」を実現することができます。

簡単な例を示しましょう。

> function bar1() { console.log("call bar1"); }
undefined
> function bar2() { console.log("call bar2"); throw new Error("Global Exit"); }
undefined
> function bar3() { console.log("call bar3"); }
undefined
> function foo() { bar1(); bar2(); bar3(); }
undefined
> function main() {
... try { foo(); } catch (e) { console.log(e.message); }
... }
undefined

> main()
call bar1
call bar2
Global Exit
undefined

実行の様子を下図に示します。

 ┌───────┐
 │try { ... }   │←─┐
 │catch { ... } │    │
 └───────┘    │
        ↓             │
 ┌──────┐      │
 │   foo()    │──┐│
 └──────┘    ││
       ↓↑          ↓│
 ┌──────┐  ┌ bar2() ──────┐ 
 │  bar1()    │  │throw new Error(...)│
 └──────┘  └──────────┘

            図 : 大域脱出

通常の関数呼び出しは、呼び出し元の関数に制御が戻ります。ところが bar2 で throw が実行されると、呼び出し元の関数 foo を飛び越えて、制御が try 文の catch 節に移るのです。このように、例外処理を使って関数を飛び越えて制御を移すことができます。

なお、大域脱出はとても強力な機能ですが、多用すると処理の流れがわからなくなる、いわゆる「スパゲッティプログラム」になってしまいます。例外処理はあくまでもエラーを処理するために使ったほうがよいでしょう。

●finally 節

ところで、プログラムの途中で例外が送出されると、残りのプログラムは実行されません。このため、必要な処理が行われない場合があります。たとえば、ファイルの入出力処理の場合、最初にファイルをオープンし最後でファイルをクローズしなければいけません。ファイルを関数 open でオープンして関数 close でクローズする場合、例外で処理が中断されるとファイルをクローズすることができません。

このような場合、try 文に finally 節を定義することで解決できます。finally 節は try 節で例外が発生したかどうかにかかわらず、try 文の実行が終了するときに必ず実行されます。catch 節がない場合、finally 節を実行したあとで同じ例外を再送出します。

簡単な例を示しましょう。大域脱出で作成した foo を呼び出す関数 baz を作ります。

> function baz() { try { foo(); } finally { console.log("clean up!"); }}
undefined
> function main1() { try { baz(); } catch (e) { console.log(e.message); }}
undefined
> main1()
call bar1
call bar2
clean up!
Global Exit
undefined

関数 bar2() で送出された例外は baz() の finally 節で捕捉されて console.log("clean up!") が実行されます。その後、例外が再送出されて、関数 main1() の catch 節に捕捉されて Global Exit と表示されます。

●async / await の例外処理

プロミスで計算が失敗 (reject) した場合、メソッド catch() でその値を求めることができました。async / await を使用すると、reject した時点で throw() と同様に例外が送出され、その例外は try .. catch で補足することができます。

簡単な例を示しましょう。

> Promise.reject(10)
Promise {
  <rejected> 10,
  [Symbol(async_id_symbol)]: 40,
  [Symbol(trigger_async_id_symbol)]: 6
}
> Uncaught 10

> try { Promise.reject(10); } catch(e) { console.log(e); }
Promise {
  <rejected> 10,
  [Symbol(async_id_symbol)]: 113,
  [Symbol(trigger_async_id_symbol)]: 6
}
> Uncaught 10

> Promise.reject(10).catch(e => console.log(e))
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 224,
  [Symbol(trigger_async_id_symbol)]: 223
}
> 10

> try { await Promise.reject(10); } catch(e) { console.log(e); }
10
undefined

Promise.reject(10) だけの場合、エラーを捕捉する catch() が無いので、エラーは捕捉されません。このエラーは try .. catch でも補足することはできません。次の例のように、catch() をつなげると、エラーを捕捉することができます。また、await を付けて実行すると、reject を実行した時点で例外が送出され、それを try .. catch で補足することができます。catch 節の引数 e には reject() の引数が渡されます。


改訂 2025 年 1 月 8 日
追加 2025 年 1 月 11, 29 日, 2 月 23 日