M.Hiroi's Home Page

JavaScript Programming

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


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

継承

前回は簡単な例題として連結リストという基本的なデータ構造を作成しました。今回は「継承 (inheritance : インヘリタンス) 」について説明します。

●継承とは?

クラスベースのオブジェクト指向では、「クラス (class) 」に「親子関係」を持たせることを「継承」といいます。子供のクラスは親クラスの性質を受け継ぐことができます。プログラミング言語の場合、引き継ぐ性質は定義されたインスタンス変数 (プロパティ) やメソッドなどになります。

プログラムを作る場合、今まで作ったプログラムと同じような機能が必要になることがありますが、継承を使うことでその機能を受け継ぎ、新規の機能や変更される機能だけプログラムする、いわゆる「差分プログラミング」が可能になります。

クラスを継承する場合、その元になるクラスを「スーパークラス」とか「ベースクラス」と呼びます。そして、継承したクラスを「サブクラス」と呼びます。この呼び方は言語によってまちまちで統一されていません。C++の場合は、元になるクラスを基本クラスといい、継承するクラスを派生クラスとか導出クラスといいます。

たとえば、クラス Foo1 を継承してクラス Foo2 を定義しましょう。クラス Foo1 にはメソッド bar() が定義されています。クラス Foo2 にメソッド bar() は定義されていませんが、Foo2 のオブジェクトに対して bar() を呼び出すと、スーパークラス Foo1 のメソッド bar() が実行されるのです。

メソッドの選択は次のように行われます。まず、オブジェクトが属するクラス Foo2 にメソッド bar() が定義されているか調べます。ところが、Foo2 には bar() が定義されていないので、スーパークラスである Foo1 に bar() が定義されているか調べます。ここでメソッド bar() が見つかり、それを実行するのです。このように、メソッドが見つかるまで順番にスーパークラスを調べていきますが、最上位のスーパークラスまで調べてもメソッドが見つからない場合はエラーになります。

継承したクラスのメソッドとは違う働きをさせたい場合、同名のメソッドを定義することで、そのクラスのメソッドを設定することができます。これを「オーバーライド (over ride) 」といいます。メソッドを選択する仕組みから見た場合、オーバーライドは必然の動作です。メソッドはサブクラスからスーパークラスに向かって探索されるので、スーパークラスのメソッドよリサブクラスのメソッドが先に選択されるわけです。

●プロトタイプチェーンによる継承の実現

JavaScript の場合、あるオブジェクトのプロパティやメソッドを引き継ぐことは簡単に行うことができます。その仕組みはとても簡単で、コンストラクタの prototype に継承するオブジェクトをセットするだけです。プロパティの探索はプロトタイプチェーンをたどって行われるので、プロトタイプのオブジェクトがスーパークラス的な役割をはたしていると考えることができます。

簡単な例を示しましょう。次のリストを見てください。

リスト : 継承

// 親
function Foo() {
    this.a = 10;
}
Foo.prototype.get_a = function() { return this.a; }
Foo.prototype.set_a = function(x) { this.a = x; }

// 子
function Bar() {
   this.b = 20;
}

// Foo を継承
Bar.prototype = new Foo();

// Bar 用のメソッドを追加
Bar.prototype.get_b = function() { return this.b; }
Bar.prototype.set_b = function(x) { this.b = x; }

継承のポイントは、コンストラクタ Bar の prototype に Foo のオブジェクトを生成してセットするところです。これを図に示すと次のようになります。

  Constructor
 ┌─ Foo  ─┐      ┌→┌─Object A─┐
 ├─────┤      │  ├──────┤
 │  a = 10  │      │  │get_a, set_a│
 ├─────┤      │  └──────┘
 │prototype ┼───┤
 └─────┘      │
      │             │
   new Foo()         │
      ↓             │
 ┌─Object B─┐←┐│
 ├──────┤  ││
 │     a      │  ││
 ├──────┤  ││
 │ __proto__  ┼──┘
 ├──────┤  │
 │get_b, set_b│  │
 └──────┘  │
                   │
  Constructor      │
 ┌─ Bar  ─┐────new Bar() →┌─Object C─┐
 ├─────┤    │              ├──────┤
 │  b = 20  │    │              │     b      │
 ├─────┤    │              ├──────┤
 │prototype ┼──┴───────┼ __proto__  │
 └─────┘                    └──────┘

                図 : 継承

Foo の prototype は Object A で、ここにメソッド get_a(), set_a() が格納されます。そして、new Foo() で生成した Object B を Bar の prototype にセットします。Object B のプロトタイプチェーンは Object A になります。ここで、new Bar() でオブジェクト Object C を生成すると、そのプロトタイプチェーンは Object B になります。したがって、Object C からプロトタイプチェーンをたどって、Object B に格納されているプロパティ a の値を参照したり、Object A のメソッド get_a(), set_a() を呼び出すことができます。

Bar のメソッドを定義する場合は、prototype に設定した Foo のオブジェクト Object B に追加します。リスト : 継承 では new Foo() で生成した Object B を Bar.prototype にセットし、その中にメソッド get_b(), set_b() を追加しています。もしも、空のオブジェクト { } の中でメソッドを定義して、それを Bar.prototype にセットすると、Foo のオブジェクトを継承することができなくなります。ご注意ください。

それでは実際に試してみましょう。

> a = new Foo()
Foo { a: 10 }
> b = new Bar()
Foo { b: 20 }
> a.get_a()
10
> b.get_a()
10
> b.get_b()
20

変数 a, b に Foo と Bar のオブジェクトをセットします。オブジェクトを生成するとき、Foo のオブジェクトにはプロパティ a に 10 がセットされ、Bar のオブジェクトにはプロパティ b に 20 がセットされます。a.get_a() はプロパティ a の値を返すので 10 になります。b.get_a() はオブジェクトにプロパティ a がないのでプロトタイプを調べます。プロトタイプは Foo のオブジェクトなので、プロパティ a の値は 10 に初期化されています。したがって、b.get_a() は 10 を返します。

●プロパティの更新

次に、a.set_a(100) でプロパティ a の値を更新します。

> a.set_a(100)
undefined
> a.get_a()
100
> b.get_a()
10

a.get_a() は 100 を返しますが、b.get_a() は 10 のままですね。a.set_a() は変数 a のオブジェクトにアクセスするのですから、他のオブジェクトのプロパティ a が書き換えられることはありません。

それでは、b.set_a(200) はどうなるのでしょうか。次の例を見てください。

> b.set_a(200)
undefined
> b.get_a()
200
> c = new Bar()
Foo { b: 20 }
> c.get_a()
10

メソッド set_a() の処理内容は this.a = x なので、アクセス対象となるのは変数 b のオブジェクトです。したがって、変数 b のオブジェクトにプロパティ a が作られて、そこに 200 が代入されます。プロトタイプ (Bar.prototype) のプロパティ a の値が書き換えられるわけではないことに注意してください。このあとは変数 b のオブジェクトにあるプロパティ a の値が参照されます。

実際に、新しい Bar のオブジェクトを生成してみましょう。c.get_a() は 10 を返すので、プロトタイプ (Bar.prototype) のプロパティ a の値は書き換えられていないことがわかります。逆にいえば、プロトタイプのオブジェクトのプロパティを書き換えると、それを参照しているすべてのオブジェクトでプロパティの値が変化します。次の例を見てください。

> b1 = new Bar()
Foo { b: 20 }
> b2 = new Bar()
Bar {b: 20, a: 10, get_b: function, set_b: function, get_a: function…}
> b1.get_a()
10
> b2.get_a()
10
> Bar.prototype.a = 100
100
> b1.get_a()
100
> b2.get_a()
100

このように、Bar.prototype.a の値を書き換えると、Bar で生成したオブジェクト b1, b2 のプロパティ a の値も変化します。ただし、b1.set_a(200) のようにプロパティ a の値を書き換えると、オブジェクト b1 の中にプロパティ a が作成されるため、プロトタイプのプロパティ a を参照することはできません。次の例を見てください。

> b2.set_a(1000)
undefined
> b2.get_a()
1000
> Bar.prototype.a = 500
500
> b1.get_a()
500
> b2.get_a()
1000

b2.set_a(1000) を実行すると b2 の中にプロパティ a が生成されて 1000 がセットされます。Bar.prototype.a の値を 500 に変更すると、b1.get_a() は 500 になりますが、b2.get_a() は 1000 のままです。このように、プロパティの値を更新すると、そのオブジェクトのプロパティに値が代入されるため、プロトタイプのプロパティを参照することはできなくなります。

●call() と apply()

プロトタイプのプロパティは、前回説明したように「クラス変数」的な使い方が可能ですが、インスタンス変数 (メンバ変数) として使う場合は、オブジェクトを生成するコンストラクタで初期化しておいたほうがよいでしょう。このとき、プロパティを初期化するコンストラクタを呼び出すことができると便利です。いわゆるスーパークラスのコンストラクタを呼び出すわけです。

JavaScript の場合、関数オブジェクトのメソッド call() と apply() を使うと、どんなメソッドでも呼び出すことができます。call() と apply() の第 1 引数には、操作対象となるオブジェクトを指定します。呼び出すメソッドでは、この値が this になります。グローバルな関数を呼び出す場合は null または undefined を指定します。そのあとに引数を指定します。apply() は配列の要素を引数に展開して呼び出すことができます。

たとえば Bar から Foo を呼び出す場合は次のようになります。

リスト : コンストラクタの呼び出し

function Bar() {
    Foo.call(this);
    this.b = 20;
}

Bar を呼び出すときに生成されたオブジェクト (this) を call() の第 1 引数にセットします。これで、コンストラクタ (関数オブジェクト) Foo にオブジェクトを渡して呼び出すことができます。また、サブクラスでメソッドをオーバーライドしたとき、スーパークラスのメソッドを呼び出す場合にも call() や apply() を使います。

●ES2015 の継承

ES2015 の場合、継承はキーワード extends を使って簡単に行うことができます。

class ClassName extends SuperClassName { ... }

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

リスト : 継承

class Foo {
  constructor(a, b) {
    this._a = a;
    this._b = b;
  }
  get a() { return this._a; }
  set a(x) { this._a = x; }
  get b() { return this._b; }
  set b(x) { this._b = x; }
  show(){
    console.log(this._a);
    console.log(this._b);
  }
}

class Bar extends Foo {
  constructor(a, b, c) {
    super(a, b);
    this._c = c;
  }
  get c() { return this._c; }
  set c(x) { return this._c = x; }
  show(){
    super.show()
    console.log(this._c);
  }
}
> var obj = new Foo(1, 2)
undefined
> var obj1 = new Bar(10, 20, 30)
undefined
> obj.a
1
> obj.b
2
> obj1.a
10
> obj1.b
20
> obj1.c
30
> obj.show()
1
2
undefined
> obj1.show()
10
20
30
undefined
> obj
Foo { _a: 1, _b: 2 }
> obj1
Bar { _a: 10, _b: 20, _c: 30 }

スーパークラスのメソッドを呼び出すには super を使います。

  1. super(引数, ...)
  2. super.method_name(引数, ...)

1 はサブクラスのコンストラクタでスーパークラスのコンストラクタを呼び出す場合です。サブクラスのコンストラクタで this を使用する場合は、その前に super() で必ずスーパークラスのコンストラクタを呼び出してください。2 はスーパークラスのメソッドを呼び出す場合です。なお、スタティックメソッドでも super を使用することができます。

●制限付き連結リスト

それでは簡単な例題として、連結リストを継承して、格納する要素数を制限する連結リストを作ってみましょう。クラス名は FixedList としました。プログラムは次のようになります。

リスト : 制限付き連結リスト

class FixedList extends List {
    // コンストラクタ
    constructor(limit) {
        super();            // スーパークラスのコンストラクタを呼び出す
        this.limit = limit;
        this.size = 0;
    }

    // オーバーライド
    // データの挿入
    add(n, x) {
        if (this.size < this.limit) {
            let result = super.add(n, x);
            if(result != null) this.size++;
            return result;
        }
        return null;
    }

    // データの削除
    delete(n) {
        if (this.size > 0) {
            let result = super.delete(n);
            if(result != null) this.size--;
            return result;
        }
        return null;
    }
}

制限付き連結リスト (FixedList) は指定した上限までしか要素を格納できません。連結リスト (List) で要素を追加するメソッドは add() で、削除するメソッドは delete() です。この 2 つのメソッドをオーバーライドすることで、FixedList の機能を実現することができます。

FixedList のコンストラクタは、最初に super() を実行して List のコンストラクタを呼び出します。次に、使用するプロパティ limit と size を初期化します。limit は要素数の上限値を表していて、引数 limit で指定します。size は連結リストに格納されている要素数を表します。

それから、add() と delete() をオーバーライドします。add() は this.limit と this.size を比較して、this.size が this.limit よりも小さい場合はデータ x を挿入します。スーパークラスのメソッド super.add() を呼び出して、データを挿入できた場合は this.size を +1 します。

delete() の場合、this.size が 0 よりも大きいときにスーパークラスのメソッド super.delete() を呼び出します。データを削除できた場合は this.size を -1 します。これで、連結リストに格納される要素数を管理することができます。

それでは、簡単な実行例を示しましょう。

リスト : 簡単なテスト

// 連結リスト
... 略 ...

// 制限付き連結リスト
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);
$ node linkedlist.js
... 略 ...
0
1
2
3
null
List(3,2,1,0)
3
2
1
0
null
List()

このように List を継承することで、FixedList を簡単にプログラムすることができます。


●プログラムリスト1

//
// linkedlist.js : 連結リスト
//
// Copyright (c) 2017-2025 Makoto Hiroi
//
// Released under the MIT license
// https://opensource.org/license/mit/
//

// セル
class Cell {
  constructor(item, next = null) {
    this._item = item;
    this._next = next;
  }
  get item() { return this._item; }
  get next() { return this._next; }
  set item(x) { this._item = x; }
  set next(x) { this._next = x; }
}

// 連結リスト
class List {
  constructor(...args) {
    let xs = new Cell(null); // ヘッダセル
    this._top = xs;
    for (let x of args) {
      xs.next = new Cell(x, null);
      xs = xs.next;
    }
  }
  get top() { return this._top; }
  set top(x) { this._top = x; }

  // 作業用
  _nth(n) {
    let xs = this.top;
    for (let i = -1; i < n && xs != null; i++) xs = xs.next;
    return xs;
  }

  // n 番目の要素を求める
  nth(n) {
    let xs = this._nth(n);
    return xs != null ? xs.item : null;
  }

  // n 番目に x を挿入する
  add(n, x) {
    let xs = this._nth(n - 1);
    if (xs != null) {
      xs.next = new Cell(x, xs.next);
      return x;
    }
    return null;
  }

  // n 番目の要素を削除
  delete(n) {
    let xs = this._nth(n - 1);
    if (xs != null && xs.next != null) {
      let ds = xs.next;
      xs.next = ds.next;
      return ds.item;
    }
    return null;
  }

  // pred が真を返す要素を探す
  find(pred) {
    for (let x of this) {
      if (pred(x)) return x;
    }
    return false;
  }

  // 空リストか?
  isEmpty() { return this.top.next == null; }

  // 空にする
  clear() { this.top.next = null; }

  // 巡回
  forEach(func) {
    for (let xs = this.top.next; xs != null; xs = xs.next) {
      func(xs.item);
    }
  }

  // ジェネレータ
  *[Symbol.iterator]() {
    for (let xs = this.top.next; xs != null; xs = xs.next) {
      yield xs.item;
    }
  }

  // 文字列
  toString() {
    return "List(" + [...this].join(",") + ")";
  }

  // 表示
  inspect(depth) { return this.toString(); }
}

// 制限付き連結リスト
class FixedList extends List {
    // コンストラクタ
    constructor(limit) {
        super();            // スーパークラスのコンストラクタを呼び出す
        this.limit = limit;
        this.size = 0;
    }

    // オーバーライド
    // データの挿入
    add(n, x) {
        if (this.size < this.limit) {
            let result = super.add(n, x);
            if(result != null) this.size++;
            return result;
        }
        return null;
    }

    // データの削除
    delete(n) {
        if (this.size > 0) {
            let result = super.delete(n);
            if(result != null) this.size--;
            return result;
        }
        return null;
    }
}

// 簡単なテスト
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));

// 制限付き連結リスト
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);

多重継承と Mix-in

継承には「単一継承」と「多重継承」の 2 種類があります。単一継承は、ただひとつのクラスからしか機能を継承することができません。これに対し多重継承は複数のクラスを継承することができます。JavaScript の場合、プロトタイプのオブジェクトは一つしか指定できないので単一継承になります。

●多重継承の問題点

多重継承は異なる性質や機能を持つ複数のクラスを継承することができるので、とても強力な機能です。ところが問題点もあるのです。たとえば、クラス Foo にはメソッド method_a() があり、クラス Bar にはメソッド method_b() があるとしましょう。この 2 つのメソッドはまったく異なる働きをします。ここで、メソッド method_a() はインスタンス変数 (プロパティ) x を使っていて、method_b() も x を使っていると、多重継承で問題が発生します。

クラス Foo と Bar を多重継承してクラス Baz を作成した場合、クラス Baz のインスタンスには x がひとつしかありません。メソッド method_a() と method_b() はひとつしかない x を使うことになります。この場合、どちらかのメソッドは正常に動作しないでしょう。これでは多重継承する意味がありません。また、Foo と Bar に同じ名前のメソッドが存在することもあります。このように、多重継承では名前の衝突が発生する場合があるのです。

それから、多重継承にはもうひとつ問題点があります。それはクラスの階層構造が複雑になることです。単一継承の場合、クラスの階層は木構造になりますが、多重継承ではグラフになります。木構造の場合、クラスの優先順位は簡単にわかりますが、グラフになると優先順位を理解するのは難しくなります。多重継承は強力な機能ですが、使うときには十分な注意が必要です。

●Mix-in

これらの問題を回避するため、インスタンス変数 (属性) を継承するスーパークラスはひとつだけに限定して、あとのスーパークラスはメソッド (実装) だけを継承するという方法があります。この方法を Mix-in といいます。具体的には、インスタンス変数を定義せずにメソッドだけを記述したクラスを用意します。属性の継承は単一継承になりますが、実装のみを記述したクラスはいくつ継承してかまいません。ひとつのクラスに複数の実装を混ぜることから Mix-in と呼ばれています。

なお、Mix-in は特別な機能ではなく、多重継承を使いこなすための方法論にすぎません。多重継承を扱うことができるプログラミング言語であれば Mix-in を行うことが可能です。ちなみに、この Mix-in という方法を言語仕様に取り込んだのが Ruby です。

Mix-in を図に示すと次のようになります。

                A
              /
            B
 Mixin A  /  \    Mixin B
    \  /      \  /
      C          D

      図 : Mix-in

クラス C はクラス B を継承していて、そこにクラス Mixin A が Mix-in されています。クラス D もクラス B を継承していますが、Mix-in されているクラスは Mixin B となります。

多重継承の問題点は Mix-in ですべて解決できるわけではありませんが、クラスの階層構造がすっきりとしてわかりやすくなることは間違いありません。Mix-in は多重継承を使いこなす優れた方法だと思います。

●クラス式

クラス式はクラスを定義するもう一つの方法です。

var 変数名 = class [ClassName] [extends SuperClass] { ... }

クラス式はその値を変数に代入しておいて、new 変数名() でオブジェクトを生成することができます。クラス式はクラス名を省略することができます。クラス名を指定した場合、class 文と違って名前の有効範囲はそのクラスの中だけになります。

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

> var foo = class Foo {}
undefined
> foo
[class Foo]
> new foo()
Foo {}
> new Foo()
Uncaught ReferenceError: Foo is not defined

> var bar = class {}
undefined
> bar
[class bar]
> new  bar()
bar {}

class 式を使うと Mix-in も簡単に実現できます。

> var oopsMixin = base => class extends base {
... oops() { console.log("oops"); }
... }
undefined
> var piyopiyoMixin = base => class extends base {
... piyopiyo() { console.log("piyopiyo"); }
... }
undefined
> class Foo {}
[Function: Foo]
> var FooOops = oopsMixin(Foo)
undefined
> var obj = new FooOops()
undefined
> obj.oops()
oops
undefined
> var FooOopsPiyoPiyo = piyopiyoMixin(FooOops)
undefined
> var obj1 = new FooOopsPiyoPiyo()
undefined
> obj1.oops()
oops
undefined
> obj1.piyopiyo()
piyopiyo
undefined

●Enumerable

Mix-in はクラスのないプロトタイプベースのオブジェクト指向でも簡単に実現することができます。Mix-in は簡単にいえばクラスにメソッドを追加することです。JavaScript の場合、プロトタイプのオブジェクトにメソッドを追加することで Mix-in を実現することができますが、ES2015 ではクラス式を使ったほうが簡単です。

それでは Mix-in の例題として、Mix-in 用のオブジェクト Enumerable を作ってみましょう。Enumerable は配列や連結リストなどのような複数のデータを格納するオブジェクトに高階関数を Mix-in します。これは Ruby のモジュール (Mix-in 用のクラス) Enumerable を参考にしました。追加するメソッドを下表に示します。

表 : Enumerable のメソッド
名前機能
obj.member(func)func が真となる要素を返す
obj.position(func)func が真となる要素の位置を返す
obj.count(func)func が真となる要素の個数を返す
obj.mapcar(func)要素に func を適用した結果をリストに格納して返す
obj.remove_if(func)func が真となる要素を削除したリストを返す
obj.fold_left(func, init)すべての要素を func を用いて結合した結果を返す

プログラムは次のようになります。

リスト : Enumerable 

var Enumerable = base => class extends base {
    // メソッドの定義
    // 探索
    member(func) {
        for (let x of this) {
            if (func(x)) return x;
        }
        return false;
    }

    // 位置を返す
    position(func) {
        let i = 0;
        for (let x of this) {
            if (func(x)) return i;
            i++;
        }
        return -1;
    }

    // 条件を満たす要素をカウントする
    count(func) {
        let c = 0;
        for (let x of this) {
            if(func(x)) c++;
        }
        return c;
    }

    // マッピング
    mapcar(func) {
        let a = [];
        for (let x of this) {
            a.push(func(x));
        }
        return a;
    }

    // フィルター
    remove_if(func) {
        let a = [];
        for (let x of this) {
            if (!func(x)) a.push(x);
        }
        return a;
    }

    // 畳み込み
    fold_left(func, a) {
        for (let x of this) {
            a = func(a, x);
        }
        return a;
    }
}

オブジェクトの要素は for (let x of this) ... で取り出します。オブジェクトが iterable であれば、どんなオブジェクトにも Enumberable を Mix-in することができます。あとは、Enumerable のメソッドで for ... of 文を使って要素を取り出して処理していくだけです。

たとえば member() の場合、func(x) を呼び出して、結果が真であれば要素 x を返します。真となる要素が見つからない場合、member() は false を返します。mapcar() と remove_if() の場合は、結果を配列 (Array) に格納して返すことにします。

Mix-in も簡単です。List に Enumerable を Mix-in する場合は次のようにします。

var elist = Enumeralbe(List);

あとは new で elist のオブジェクトを生成すれば、そこから Enumerable のメソッドを呼び出すことができます。簡単な実行例を示しましょう。

> .load linkedlist.js
... 略 ...

> .load enumerable.js
... 略 ...

> var elist = Enumerable(List)
undefined
> var xs = new elist(1,2,3,4,5)
undefined
> xs.toString()
'List(1,2,3,4,5)'
> xs.member(x => x == 4)
4
> xs.position(x => x == 4)
3
> xs.count(x => x == 4)
1
> xs.mapcar(x => x * x)
[ 1, 4, 9, 16, 25 ]
> xs.remove_if(x => x % 2 == 0)
[ 1, 3, 5 ]
> xs.fold_left((a, x) => a + x, 0)
15

正常に動作していますね。また、既存のオブジェクト、たとえば配列 (Array) にも Enumerable を Mix-in することができます。

var eary = Enumerable(Array);

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

> var eary = Enumerable(Array)
undefined
> var a = new eary(1,2,3,4,5,6,7,8)
undefined
> a
[
  1, 2, 3, 4,
  5, 6, 7, 8
]
> a.member(x => x % 2 == 0)
2
> a.member(x => x % 2 != 0)
1
> a.position(x => x % 2 != 0)
0
> a.position(x => x % 2 == 0)
1
> a.count(x => x % 2 == 0)
4
> a.mapcar(x => x * x)
[
   1,  4,  9, 16,
  25, 36, 49, 64
]
> a.remove_if(x => x % 2 == 0)
[ 1, 3, 5, 7 ]
> a.fold_left((a, x) => a + x, 0)
36

このように、複数のクラスで共通の操作 (メソッド) を定義したい場合、Mix-in はとても役に立ちます。


プロミスとプロキシ

●コールバック関数

Web ブラウザなどの GUI アプリケーションは、ユーザーからの入力やシステムの状態変化など、あるイベントをきっかけに処理を行うイベントドリブン型のプログラムです。このようなプログラムは、一般に次のようなメインルーチンを持っています。

  1. 初期化
  2. イベントを取得する
  3. イベントの種類に応じて処理を振り分ける
  4. 2 に戻る

2 から 4 をイベントループと呼び、アプリケーションはユーザーからの入力などのイベントを待ちます。そして、3 の処理に対応する機能を「バインディング (binding)」といいます。バインディングは、ウィンドウでイベントが発生したときに、それに応じて実行するプログラムを設定します。このプログラムを「イベントハンドラ」とか「コールバック関数」と呼びます。

一般に、イベントは非同期に発生するので、それに対応するコールバック関数も非同期に実行されることになります。JavaScript はシングルスレッドのプログラミング言語なので、複数のプログラムを同時に実行することはできません。このため、Web ブラウザや Node.js などでは、イベントが発生したら対応するコールバック関数をすぐに実行するのではなく、いったんキュー (queue, 待ち行列) に登録しておいて、あとでキューからコールバック関数を取り出して順番に処理するようになっています。

また、コールバック関数はイベントだけではなく、時間がかかる処理を実行するときにも使われます。たとえば、ファイル入出力や通信などの処理を終了まで待っていると、他の処理を実行することができなくなります。終了後に行うプログラムをコールバック関数として登録しておけば、終了を待たずに他の処理を実行することができます。

簡単な例を示しましょう。関数 setTimeout() を使うと、指定した時間後に登録した関数を実行することができます。

setTimeout(callback, after [, args, ...])

引数 callback は after msec 後に実行するコールバック関数です。callback に与える引数は after の後ろに指定することができます。

それでは実際に試してみましょう。

> function foo() {
... console.log("foo start");
... setTimeout(console.log, 2000, "oops!!")
... setTimeout(console.log, 1000, "oops!")
... console.log("foo end");
... }
undefined
> foo()
foo start
foo end
undefined
> oops!
oops!!

関数 foo() の実行はすぐに終了しますが、1 秒後と 2 秒後に コールバック関数が実行されるので oops! と oops!! が表示されまます。setTimeout() は after 秒後に callback をキューに登録する処理を行います。foo() の処理を終了すると REPL に戻りますが、そこでキューの状態を監視していて、キューに callback が登録されているならば、それを順番に取り出して実行します。

setTimeout() のほかにも、setInterval() や setImmediate() があります。

setInterval(callback, msec [, args, ...])
setImmediate(callback [, args, ...])

setInterval() は callback を msec 間隔で実行します。setImmediate() は callback をすぐにキューに登録します。なお、これらの関数は Node.js ではグローバルに定義されていますが、Web ブラウザの JavaScript では window オブジェクトのメソッドになっています。名前の前に window. を付けてください。

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

リスト : setImmediate() の実行例

function foo(n) {
  if (n > 0) {
    console.log("foo" + n);
    setImmediate(foo, n - 1);
  }
}

function bar(n) {
  if (n > 0) {
    console.log("bar" + n);
    setImmediate(bar, n - 1);
  }
}

foo(10);
bar(8);
$ node test.js
foo10
bar8
foo9
bar7
foo8
bar6
foo7
bar5
foo6
bar4
foo5
bar3
foo4
bar2
foo3
bar1
foo2
foo1

関数 foo() は "foo" + 引数 を n 回、bar() は "bar" + 引数 を n 回表示します。どちらの関数も setImmediate() で foo(n - 1), bar(n - 1) をコールバック関数として登録します。最初に foo(10), bar(8) を実行すると、キューには foo(9), bar(7) が登録されます。次に、キューから foo(9), bar(7) を取り出して実行すると、キューには foo(8), bar(6) が登録されます。このように、コールバック関数をキューに登録することで、n 回の繰り返しを相互に行わせることができます。

●コールバック関数の同期処理

個々のコールバック関数は非同期に実行されるため、それを順番に実行 (同期処理) するのはけっこう面倒です。たとえば、foo(callback_a); bar(callback_b); baz(callback_c); と順番に並べても、コールバック関数が a, b, c の順番で実行されるとは限りません。次の例を見てください。

リスト : コールバック関数の同期処理 (間違い版)

function foo(func) {
  setTimeout(func, 3000);
}

function bar(func) {
  setTimeout(func, 1000);
}

function baz(func) {
  setTimeout(func, 2000);
}

function callback_a() {
  console.log("callback_a");
}

function callback_b() {
  console.log("callback_b");
}

function callback_c() {
  console.log("callback_c");
}

foo(callback_a);
bar(callback_b);
baz(callback_c);
$ node test1.js
callback_b
callback_c
callback_a

このように、コールバック関数の起動タイミングは foo(), bar(), baz() で異なるため、単純に foo(), bar(), baz() を並べただけでは、コールバック関数の同期処理を行うことはできません。

このような場合、コールバック関数を入れ子にするとうまくいきます。

リスト : コールバック関数の同期処理 (入れ子バージョン)

foo(() => {
  callback_a();
  bar(() => {
    callback_b();
    baz(() => {
      callback_c();
    });
  });
});
$ node test2.js
callback_a
callback_b
callback_c

foo() に渡すアロー関数の中で callback_a() を実行し、それが終了してから bar() を呼び出します。次に、bar() に渡すアロー関数の中で callback_b() を実行し、それが終了したら baz() を呼び出します。最後に、bar() に渡すアロー関数の中で callback_c() を実行します。これで、コールバック関数を a, b, c の順番で実行することができます。

ただし、この方法は同期させるコールバック関数の数に比例して入れ子が深くなるので、プログラムを記述するのが難しくなり、可読性も低下するという欠点があります。このような場合、ES2015 で導入された「プロミス (Promise)」を使うとプログラムをすっきり記述することができます。

●プロミス

「プロミス (promise)」は、まだ計算を完了してはいないが、いずれ計算が完了することを表すオブジェクトです。

new Promise(function) => promise_object  (pending 状態)

Promise() には関数 function を渡します。Promise() はまだ計算を完了していない promise_object を返します。これを pending 状態といいます。引数 function の仕様を示します。

function(resolve, reject) { 処理; ... }
成功ならば resolve() を呼ぶ (fulfilled 状態)
失敗ならば reject() を呼ぶ  (rejected 状態)

引数 resolve, reject は関数で、処理の中で resolve() を呼ぶと計算が完了したことになります。これを fulfilled 状態といいます。このとき、resolve() の引数に計算結果を渡します。reject() を呼ぶと計算が失敗したことになります。これを rejected 状態といいます。reject() の引数にはエラーの情報を渡します。

Promise() を実行すると、引数の function はすぐに実行されることに注意してください。実際に非同期処理を行うには、function の中で非同期処理を行う関数、たとえば setTimeout() などを実行する必要があります。それに渡すコールバック関数の中で resolve() や reject() を呼び出します。

promise_object の値を求めるにはメソッド then(), catch() を使います。

then(onFulfilled [, onRejected])
catch(onRejected)

引数 onFulfilled は fulfilled 状態のときに実行される関数、onRejected は rejected 状態のときに実行される関数です。どちらの関数も引数はひとつで、resolve() または reject() の引数がそのまま渡されます。promise_object が pending 状態の場合、then() や catch() の実行は遅延されます。

then() と catch() は promise_object を返します。コールバック関数で promise_object 以外の値を返した場合、その値をラップした promise_object が返されます。

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

> function twice(x) {
... console.log(x);
... return x * 2;
... }
undefined
> Promise.resolve(1).then(twice).then(twice).then(twice).then(twice)
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 35,
  [Symbol(trigger_async_id_symbol)]: 34
}
> 1
2
4
8

関数 twice() は引数を表示して、それを 2 倍した値を返します。Promise.resolve(x) は値が x に確定した promise_object を返します。then() は promise_object を返すので、ドット ( . ) で連結していくことができます。then(twice) を連結していくことで、値が倍々されていきます。

then() で onRejected を指定していない場合、rejected 状態の promise_object はスルーされます。次の例を見てください。

> Promise.reject(0).then(twice).then(twice).then(twice).catch(console.log)
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 49,
  [Symbol(trigger_async_id_symbol)]: 48
}
> 0
> Promise.reject(0).then(twice).then(twice).then(twice, console.log)
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 62,
  [Symbol(trigger_async_id_symbol)]: 61
}
> 0

Promise.reject() は rejected 状態の promise_object を返します。引数は reject() と同じくエラーを表す情報です。最初の例では、then() に onRejected が指定されていないので全てスルーされ、最後の catch() でエラーが捕捉されます。次の例では、最後の then() で onRejected が指定されているので、そこでエラーが捕捉されます。

コールバック関数の同期処理をプロミスを使って書き直すと次のようになります。

リスト : コールバック関数の同期処理 (プロミスバージョン)

function callback_a() {
  console.log("callback_a");
}

function callback_b() {
  console.log("callback_b");
}

function callback_c() {
  console.log("callback_c");
}

function makePromise(func, d) {
  return new Promise((resolve, reject) => {
    setTimeout(() => { func(); resolve(d); }, d);
  });
}

makePromise(callback_a, 3000)
.then(x => makePromise(callback_b, 1000))
.then(x => makePromise(callback_c, 2000));
$ node test3.js
callback_a
callback_b
callback_c

関数 makePromise() はプロミスを生成します。引数のアロー関数の中で setTimeout() を呼び出し、そのコールバック関数の中で func() と resolve() を呼び出します。これで、引数の d msec 後に関数 func を実行し、プロミスを fulfilled 状態に移行します。

あとは、makePromise() によるプロミスの生成を then() でつなげばいいわけです。then() に渡すアロー関数の引数 x にはプロミスの値が渡されます。これでコールバック関数 a, b, c を順番に実行することができます。

●Promise.all() と Promise.race()

Promise.all(iterable) は引数 iterable の要素 (プロミス) がすべて成功したとき、Promise.all() も成功します。ひとつでも失敗すると、Promise.all() も失敗します。成功した場合、プロミスの値を格納した配列が Promise.all() の値になります。

簡単な実行例を示します。

> Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]).
then(console.log)
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 32,
  [Symbol(trigger_async_id_symbol)]: 28
}
> [ 1, 2, 3 ]
> Promise.all([Promise.resolve(1), Promise.reject("oops"), Promise.resolve(3)]).
then(console.log, console.log)
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 51,
  [Symbol(trigger_async_id_symbol)]: 47
}
> oops

Promise.race(iterable) は引数 iterable の要素 (プロミス) で最初に完了したプロミスの値で成功・失敗が決まります。

> Promise.race([makePromise(callback_a, 3000), makePromise(callback_b, 1000)]).
then(console.log)
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 157,
  [Symbol(trigger_async_id_symbol)]: 154
}
> callback_b
1000
callback_a

> Promise.race([makePromise(callback_a, 3000), Promise.reject("oops")
,makePromise(callback_b, 1000)]).then(console.log, console.log)
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 182,
  [Symbol(trigger_async_id_symbol)]: 178
}
> oops
callback_b
callback_a

Promise.race() の値が決定したとき、実行中のコールバック関数はそのまま最後まで実行されます。途中で停止しないことに注意してください。

●プロキシ

「プロキシ (Proxy)」は、ターゲットとなるオブジェクトの基本操作をトラップして、その動作をカスタマイズするために使用します。

new Proxy(target, handler) => Proxy_object

引数 target は操作をトラップするオブジェクト、handler はトラップしたメソッドを含むオブジェクトです。トラップできる基本操作と、それに対応するメソッドについては、リファレンスマニュアル Proxy - JavaScript | MDN や Proxy オブジェクト (JavaScript) をお読みください。

簡単な例として、プロパティのアクセスをトラップしてみましょう。プロパティの参照はメソッド get() で、更新はメソッド set() でトラップすることができます。

get(target, prop, receiver)
set(target, prop, value, receiver)

target はターゲットのオブジェクト、prop はプロパティ名、receiver はプロキシかそれを継承するオブジェクトです。引数 receiver は省略しても動作します。set() の引数 value はプロパティに書き込む値です。

リスト : Proxy の使用例

var obj = {a: 1, b: 2};
var handler = {
  get(target, name) {
    console.log("get " + name);
    return target[name];
  },
  set(target, name, value) {
    console.log("set " + name);
    target[name] = value;
  }
}

var p = new Proxy(obj, handler);
console.log(p.a);
console.log(p.b);
p.a = 10;
p.b = 20;
console.log(obj)

変数 handler にメソッド get(), set() を格納したオブジェクトをセットします。トラップされるのは get() と set() だけで、他の基本操作はトラップされません。get() と set() はメッセージを表示したあと、本来の操作を行っています。

そして、new Proxy(obj, handler) とすると、obj の操作をトラップする Proxy オブジェクト p を生成することができます。あとは、p.a, p.b とすると、トラップした get() が、p.a = 10, p.b = 20 とすると set() が実行されます。

それでは実行してみましょう。

$ node test4.js
get a
1
get b
2
set a
set b
{ a: 10, b: 20 }

このように、Proxy オブジェクトを介して他のオブジェクトの操作をトラップすることができます。

●Reflect

なお、オブジェクトの基本操作はクラス Reflect のスタティックメソッドとして定義されています。たとえば、プロパティをアクセスする基本操作は Reflect.get() と Reflect.set() になります。簡単な例を示しましょう。

> var obj = {x: 123}
undefined
> Reflect.get(obj, "x")
123
> Reflect.set(obj, "x", 456)
true
> obj
{ x: 456 }

詳しい説明はリファレンスマニュアル Reflect - JavaScript | MDN をお読みください。

●async / await

ES2017 より async / await がサポートされました。async / await を使うと、非同期関数を同期関数のように呼び出すことができます。最初に async から説明します。

function の前に async を付けると、その関数は return の値をプロミスに包んで返します。

async function foo(args, ...) {
    ...;
    return value;
}

以降 async を付けた関数を async 関数と呼ぶことにします。たとえば、async 関数の返り値が "oops" であれば、Promise.resolve("oops") を返すことになります。また、明示的にプロミスを返してもかまいません。簡単な例を示しましょう。

> async function foo() { return "oops"; }
undefined
> var a = foo();
undefined
> a.then(console.log)
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 105,
  [Symbol(trigger_async_id_symbol)]: 82
}
> oops

> async function bar() { return Promise.resolve("oops!"); }
undefined
> var b = bar()
undefined
> b.then(console.log)
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 226,
  [Symbol(trigger_async_id_symbol)]: 201
}
> oops!

async 関数の中ではキーワード await を使うことができます。

let result = await promise;

await はプロミスが結果を返すまで、async 関数の実行を一時停止します。プロミスの実行が終了すると、await はその結果を取り出して返します。そのあと、async 関数の実行が再開されます。

簡単な例を示しましょう。コールバック関数の同期処理 (test3.js) を async / await で書き直してみましょう。

リスト : コールバック関数の同期処理 (test5.js)

function callback_a() {
  console.log("callback_a");
}

function callback_b() {
  console.log("callback_b");
}

function callback_c() {
  console.log("callback_c");
}

async function makePromise(func, d) {
  return new Promise((resolve, reject) => {
    setTimeout(() => { func(); resolve(d); }, d);
  });
}

async function test() {
    await makePromise(callback_a, 3000);
    await makePromise(callback_b, 1000);
    await makePromise(callback_c, 2000);
}
> .load test5.js
... 略 ...

> test()
Promise {
  ,
  [Symbol(async_id_symbol)]: 76,
  [Symbol(trigger_async_id_symbol)]: 6
}
> callback_a
callback_b
callback_c

>

then() でプロミスをつなぐよりも、async / await を使ったほうが直感的でわかりやすいと思います。なお、async / await で発生したエラーは例外処理 (try .. catch) で補足することができます。詳細は拙作のページ「例外処理」をお読みくださいませ。


改訂 2025 年 1 月 8 日
追加 2023 年 2 月 25 日