「イテレータ (iterator)」はコレクションの要素を順番にアクセスするための機能です。イテレータは「反復子」と訳されることがありますが、最近は訳さずにそのまま使われることが多いようです。JavaScript にはいろいろなコレクションが用意されていますが、for...in 文で取り出すことができるのはプロパティだけです。ES2015 から導入されたイテレータを使うと、コレクションの要素を順番に取り出していくことができます。
JavaScript では、要素を順番に取り出すメソッド next() を持つオブジェクトを「イテレータ」と呼びます。next() の仕様を示します。
iterator.next() => {value: 要素, done: 真偽値}
next() は引数無しで呼び出して、返り値はプロパティ value と done を持つオブジェクトです。通常、value にはコレクションの要素がセットされます。要素がない場合、done は true がセットされ、value は省略されるか undefined がセットされます。
簡単な例を示しましょう。
リスト : 配列のイテレータ function makeIter(ary) { return { idx: 0, next() { if (ary.length == this.idx) { return {value: undefined, done: true}; } else { return {value: ary[this.idx++], done: false}; } } } }
関数 makeIter() は配列 ary のイテレータを返します。処理は簡単で、next() は idx の位置にある要素を返して、idx を +1 するだけです。idx が配列の最後に到達したら、done を true にセットしたオブジェクトを返します。
それでは実行してみましょう。
> var iter = makeIter([1,2,3,4]) undefined > iter.next() { value: 1, done: false } > iter.next() { value: 2, done: false } > iter.next() { value: 3, done: false } > iter.next() { value: 4, done: false } > iter.next() { value: undefined, done: true }
イテレータを自分で作るのはけっこう面倒ですが、ES2015 で導入された「ジェネレータ (Generator)」を使うと、もっと簡単にイテレータを作ることができます。
function* name(仮引数, ...) { 処理; ... }
function の後ろに * を付けると、その関数は「ジェネレータ関数」になります。ジェネレータ関数はイテレータを返しますが、処理はすぐに実行されません。イテレータのメソッド next() を実行すると、ジェネレータ関数の実行が開始されます。
そして、ジェネレータ関数の処理中で yield 文があると、そこでジェネレータ関数の実行を中断し、その引数を呼び出し元の next() に返します。つまり、next() を実行するたびにジェネレータ関数の実行が再開され、yield 文で実行を中断して値を next() に返すわけです。
簡単な例を示しましょう。
> function* foogen() { ... console.log("foo start"); ... yield 1; ... console.log("foo 1 end"); ... yield 2; ... console.log("foo 2 end"); ... yield 3; ... console.log("foo 3 end"); ... } undefined > var foo = foogen() undefined > foo.next() foo start { value: 1, done: false } > foo.next() foo 1 end { value: 2, done: false } > foo.next() foo 2 end { value: 3, done: false } > foo.next() foo 3 end { value: undefined, done: true } > foo.next() { value: undefined, done: true }
next() を呼び出すたびに yield 文の引数を順番に取得していることがわかります。たとえば、配列のジェネレータは次のように簡単に定義することができます。
> function* makeGen(ary){ ... for (let i = 0; i < ary.length; i++) yield ary[i]; ... } undefined > var gen = makeGen([1, 2, 3, 4, 5]) undefined > gen.next() { value: 1, done: false } > gen.next() { value: 2, done: false } > gen.next() { value: 3, done: false } > gen.next() { value: 4, done: false } > gen.next() { value: 5, done: false } > gen.next() { value: undefined, done: true }
ジェネレータ関数から他のジェネレータ関数を呼び出すときは yiled* 文を使います。
> function* genbar(ary1, ary2) { ... yield* makeGen(ary1); ... yield* makeGen(ary2); ... } undefined > var bar = genbar([1, 2, 3], [4, 5]); undefined > bar.next() { value: 1, done: false } > bar.next() { value: 2, done: false } > bar.next() { value: 3, done: false } > bar.next() { value: 4, done: false } > bar.next() { value: 5, done: false } > bar.next() { value: undefined, done: true }
ES2015 では匿名のジェネレータ関数も定義することができます。
> var genfunc = function*() { yield 1; yield 2; yield 3; } undefined > var gen = genfunc() undefined > gen.next(); { value: 1, done: false } > gen.next(); { value: 2, done: false } > gen.next(); { value: 3, done: false } > gen.next(); { value: undefined, done: true }
ES2015 では、プロパティ [Symbol.iterator] を持ったオブジェクトを「iterable オブジェクト」といいます。Symbol.iterator はあらかじめグローバルに定義されたシンボルです。マニュアルでは @@iterator と表記されることがあります。@@iterator は関数で、返り値はイテレータです。配列などの基本的なコレクションは iterable オブジェクトです。
> var ary = [1, 2, 3, 4, 5] undefined > var gen = ary[Symbol.iterator]() undefined > gen.next() { value: 1, done: false } > gen.next() { value: 2, done: false } > gen.next() { value: 3, done: false } > gen.next() { value: 4, done: false } > gen.next() { value: 5, done: false } > gen.next() { value: undefined, done: true }
iterable オブジェクトは、構文 for...of を使って要素を順番に取り出すことができます。
for (変数 of iterable) { ... }
> for (let x of ary) console.log(x) 1 2 3 4 5 undefined
オブジェクトのプロパティ [Symbol.iterator] にジェネレータ関数をセットすれば、そのオブジェクトは iterable になります。
> var obj = {} undefined > obj[Symbol.iterator] = function*() { yield 1; yield 2; yield 3; } [GeneratorFunction (anonymous)] > for (let x of obj) console.log(x) 1 2 3 undefined
上記のプログラムは次のように記述することもできます。
リスト ; iterable オブジェクトの定義 var obj = { *[Symbol.iterator]() { yeild 1; yield 2; yield 3; } }
iterable オブジェクトはスプレッド演算子で展開することができます。
> [...obj] [ 1, 2, 3 ] > [...obj, 4, 5, 6] [ 1, 2, 3, 4, 5, 6 ]
ES2015 で導入された「テンプレート文字列 (Template literal)」はバッククォート ( ` ) で囲まれた文字列で、${...} で変数や式を展開することができます。これは bash や Perl の変数展開、Ruby の式展開などと同様の機能です。
簡単な例を示しましょう。
> var x = "foo" undefined > `hello ${x}!` 'hello foo!' > var a = 123 undefined > var b = 456 undefined > `a + b = ${a + b}` 'a + b = 579'
テンプレート文字列は複数行に渡って文字列を記述することができます。つまり、改行文字がそのまま含まれます。もちろん \n や \t などエスケープシーケンスも使うことができます。
> var s = `hello world ... hello, foo` undefined > console.log(s) hello world hello, foo undefined > s 'hello world\nhello, foo' > var s1 = `hello\tworld` undefined > console.log(s1) hello world undefined
タグ付きテンプレートは、テンプレート文字列を ${...} の結果とそれ以外の文字列に分けて、それをタグ名と同じ名前の関数に渡して実行します。
tag_name `...`
関数の仕様を示します。
tag_name(strings, ...values) `abc${n}def${m}ghi` => strings[0] = "abc" strings[1] = "def" strings[2] = "ghi" values[0] = ${n} の結果 values[1] = ${m} の結果
関数の返り値は文字列以外でもかまいません。簡単な実行例を示します。
> function foo_tag(strings, ...values) { ... console.log(strings); ... console.log(values); ... return "oops!"; ... } undefined > var x = 100 undefined > var y = 200 undefined > foo_tag`x + y = ${x + y}` [ 'x + y = ', '' ] [ 300 ] 'oops' > foo_tag`x + y = ${x + y}, x * y = ${x * y}` [ 'x + y = ', ', x * y = ', '' ] [ 300, 20000 ] 'oops'
引数の strings にはエスケープシーケンスなどの処理をした文字列が格納されますが、入力された生の文字列は、引数 strings のプロパティ raw から求めることができます。
> function bar_tag(strings, ...values) { ... console.log(strings.raw); ... console.log(strings); ... console.log(values); ... return "oops!"; ... } undefined > bar_tag`hello\tworld\n` [ 'hello\\tworld\\n' ] [ 'hello\tworld\n' ] [] 'oops!'
関数 String.raw`...` は ${...} を処理をした結果と生の文字列を連結して返します。
> var x = 123 undefined > var y = 456 undefined > String.raw`hello${x}\t\tworld${y}\n` 'hello123\\t\\tworld456\\n'
console.log() の第 1 引数には、C言語の関数 printf のような書式文字列を指定することができます。これを置換文字列といいます。置換文字列はそのまま文字列として扱われますが、文字列の途中にパーセント % が表れると、その後ろの文字を変換指示子として解釈し、引数に与えられたデータをその指示に従って表示します。
簡単な例を示しましょう。
> var a = [1,2,3,4,5] undefined > console.log(a) [ 1, 2, 3, 4, 5 ] undefined > console.log('%o', a) [ 1, 2, 3, 4, 5, [length]: 5 ] undefined > console.log('%O', a) [ 1, 2, 3, 4, 5 ] undefined > console.log('%s', a) [ 1, 2, 3, 4, 5 ] undefined > var b = "hello, world" undefined > console.log(b) hello, world undefined > console.log('%o', b) 'hello, world' undefined > console.log('%O', b) 'hello, world' undefined > console.log('%s', b) hello, world undefined > function foo() { console.log('foo'); } undefined > console.log(foo) [Function: foo] undefined > console.log('%o', foo) <ref *1> [Function: foo] { [length]: 0, [name]: 'foo', [arguments]: null, [caller]: null, [prototype]: { [constructor]: [Circular *1] } } undefined > console.log('%O', foo) [Function: foo] undefined > console.log('%s', foo) function foo() { console.log('foo'); } undefined
今回は JavaScript のオブジェクト指向機能について説明します。現在のオブジェクト指向はクラスでインスタンス変数やメソッドを定義する「クラスベース」が主流です。ところがプロトタイプベースにはクラスが存在しません。このため、「雛形 (プロトタイプ) 」となるオブジェクトから、新しいオブジェクトを生成する方法が必要になります。この場合、次の 2 通りの方法が考えられます。
2 の方法はクローン (clone) と呼ばれることもあります。JavaScript の場合、基本的には 1 の方法を使います。まず最初にオブジェクトの生成から説明します。
JavaScript では、new 演算子を使って空のオブジェクトを生成し、それを関数に渡してオブジェクトに必要なデータをセットします。この関数を「コンストラクタ (constructor) 」と呼びます。JavaScript の場合、オブジェクトはハッシュで実装されています。オブジェクトのキーのことを「プロパティ (property)」と呼びます。プロパティは「属性」という意味で、クラスベースのオブジェクト指向では「インスタンス変数」に相当します。
たとえば、コンストラクタを Foo とすると、new Foo() で新しいオブジェクトが生成されます。次の例を見てください。
リスト : コンストラクタ function Foo(a, b) { this.a = a; this.b = b; }
JavaScript の場合、コンストラクタの中ではキーワード this を使ってオブジェクトを参照することができます。new Foo() の場合、new で生成されたオブジェクトが this に渡されるので、関数 Foo の中では this を使ってオブジェクトのプロパティにアクセスすることができます。
プロパティのアクセス方法は [ ] だけではなく、object.name でもアクセスすることができます。this.a は this["a"] と同じ意味です。ただし、this.1 のように数値を直接指定することはできません。この場合は [ ] を使って this[1] とします。
それでは実際にオブジェクトを生成してみましょう。
> x = new Foo(10, 20) Foo {a: 10, b: 20} > x.a 10 > x.b 20 > y = new Foo(100, 200) Foo {a: 100, b: 200} > y.a 100 > y.b 200
JavaScript の場合、プロパティのアクセスを制限する機能はありません。どこからでもアクセスすることができます。
JavaScript の場合、オブジェクトのプロパティに関数オブジェクトをセットすれば、それをメソッドとして呼び出すことができます。メソッドの呼び出しは object.method() とします。このとき、メソッドの中ではキーワード this を使って呼び出したオブジェクト (object) を参照することができます。次の例を見てください。
リスト : メソッドの定義 (1) function Foo(a, b) { this.a = a; this.b = b; this.get_a = function() { return this.a; }; this.get_b = function() { return this.b; }; this.set_a = function(x) { this.a = x; }; this.set_b = function(x) { this.b = x; }; }
コンストラクタ Foo の中でメソッド get_a(), get_b(), set_a(), set_b() を定義します。匿名関数で関数オブジェクトを生成してプロパティにセットするだけなので簡単です。
それでは実際に試してみましょう。
> x = new Foo(10, 20) Foo { a: 10, b: 20, get_a: [Function (anonymous)], get_b: [Function (anonymous)], set_a: [Function (anonymous)], set_b: [Function (anonymous)] } > x.get_a() 10 > x.get_b() 20 > x.set_a(100) undefined > x.get_a() 100 > x.set_b(200) undefined > x.get_b() 200
このように、関数オブジェクトをプロパティにセットすれば、それをメソッドとして呼び出すことができます。
ところで、この方法ではオブジェクトを生成するたびに、新たな関数オブジェクトが生成されてプロパティにセットされます。同じ処理を行う関数をいくつも作るのは無駄ですね。そこで、メソッド用の関数を定義して、それをプロパティにセットすることにしましょう。次の例を見てください。
リスト : メソッドの定義 (2) function Foo(a, b) { function get_a() { return this.a; }; function get_b() { return this.b; }; function set_a(x) { this.a = x; }; function set_b(x) { this.b = x; }; this.a = a; this.b = b; this.get_a = get_a this.get_b = get_b this.set_a = set_a this.set_b = set_b }
Foo の中で局所関数 get_a(), get_b(), set_a(), set_b() を定義し、それをプロパティにセットします。これで、無駄な関数オブジェクトの生成を抑えることができます。ただし、この方法でもメソッドは個々のオブジェクトに格納されるので、メモリを余分に使うことになります。この問題は「プロトタイプチェーン」という機能を使うと解決することができます。
ES2015 からメソッドを簡単に定義するための構文が導入されました。
旧 { name: function(...) { .... }、... } 新 { name(...) { ... }, ...} { [symbol](...) { ... }, ...}
プロパティを省略して名前付きの関数を定義すると、関数名がプロパティになります。プロパティにシンボル (symbol) を使いたい場合は、角カッコを使って関数名を [symbol] と指定してください。
簡単な実行例を示しましょう。
> var foo = Symbol("foo") undefined > var obj = { foo() { console.log("string foo"); }, ... [foo]() { console.log("symbol foo"); } ... } undefined > obj.foo() string foo undefined > obj[foo]() symbol foo undefined
角カッコはシンボルだけではなく、その中でプロパティ名を計算で求めることもできます。
> var obj = { ... ["foo" + 1]() { console.log("foo" + 1); }, ... ["bar" + 10]() { console.log("bar" + 10); } ... } undefined > obj.foo1() foo1 undefined > obj.bar10() bar10 undefined
getter と setter はメソッドで、プロパティのアクセスと同じ構文で呼び出すことができます。つまり、obj.prop のときは getter が呼び出され、obj.prop = value のときは setter が呼び出されます。
getter と setter の定義は簡単で、名前の前にキーワード get, set を付けるだけです。
{ get name() {...} } { get [expr]() {...} } { set name(value) { ... } } { set [expr](value) { ... } }
簡単な例を示しましょう。
> var obj = { ... get foo() { return this._foo; }, ... set foo(val) { this._foo = val; } ... } undefined > obj.foo = 100 100 > obj.foo 100 > obj { foo: [Getter/Setter], _foo: 100 }
getter を定義する場合、getter の名前と実際のプロパティ名を同じにすると、無限の再帰呼び出しになってしまうので、値を格納するプロパティは _foo としました。
setter と getter を削除する場合は delete 文を使います。delete はオブジェクトから指定したプロパティを削除します。
> delete obj.foo true > obj { _foo: 100 }
JavaScript の場合、new 演算子でオブジェクトを生成するとき、プロトタイプとなるオブジェクトを指定することができます。もし、オブジェクトの中でプロパティが見つからない場合はプロトタイプからプロパティを探します。そのプロトタイプにも、プロトタイプとなるオブジェクトが存在する場合があります。つまり、複数のプロトタイプがつながっている場合があるのです。これをプロトタイプチェーンといいます。
ようするに、JavaScript はプロトタイプをコピーするのではなく、プロトタイプをリンクでつないでおくわけです。そして、プロトタイプチェーンをたどってプロパティを探します。また、プロトタイプチェーンを使って「継承」を実現することもできます。
コンストラクタで生成されるオブジェクトのプロトタイプは、コンストラクタ (関数オブジェクト) のプロパティ prototype で指定します。次の例を見てください。
リスト : メソッドの定義 (3) function Foo(a, b) { this.a = a; this.b = b; } Foo.prototype.get_a = function() { return this.a; }; Foo.prototype.get_b = function() { return this.b; }; Foo.prototype.set_a = function(x) { this.a = x; }; Foo.prototype.set_b = function(x) { this.b = x; };
関数はコンストラクタになる可能性があるので、関数オブジェクトにはプロパティ prototype が必ず用意され、その値は空オブジェクトで初期化されています。たとえば上記プログラムの場合、コンストラクタ Foo の prototype には空オブジェクトがセットされています。そして、演算子 new でオブジェクトを生成するとき、新しいオブジェクトのプロトタイプチェーンに Foo.prototype のオブジェクトがリンクされます。
ここで、Foo.prototype で設定するオブジェクトは、new 演算子で生成されるオブジェクトのプロトタイプを指定するものであり、Foo のプロトタイプではないことに注意してください。プロトタイプチェーンを表すプロパティ名は処理系に依存していて、Google Chrome の場合は __proto__ になります。つまり、new 演算子は生成したオブジェクトの __proto__ に、コンストラクタの prototype の値をセットするわけです。これを図に示すと次のようになります。
┌─Constructor ─┐ ┌── object ──┐ │ │ │ │ │prototype: {...}│─ new Constructor()→│__proto__: {...}│ │ │ │ │ ↑ │ └──────┼─┘ └──────┼─┘ │ │ │ │ └────────────────────┤ │ │ │ ┌── object ┼─┐ │ │ ↓ │ └───── new Constructor()→│__proto__: {...}│ │ │ └────────┘ 図 : プロトタイプチェーンの設定
したがって、Foo.prototype にセットされているオブジェクトにメソッドを追加すれば、new Foo() で生成したオブジェクトからそれらのメソッドを呼び出すことができるわけです。また Foo.prototype には、次のように { } でオブジェクトを生成してセットすることもできます。
リスト : メソッドの定義 (4) function Foo(a, b) { this.a = a; this.b = b; } Foo.prototype = { get_a() { return this.a; }, get_b() { return this.b; }, set_a(x) { this.a = x; }, set_b(x) { this.b = x; } }
{ } の中では name: value でプロパティ名とその値を設定することができます。このように、prototype のオブジェクトでメソッドを設定すると、コンストラクタで生成されたオブジェクトからそのメソッドを呼び出すことができます。メソッドを持っているオブジェクトは一つだけなので、メモリを余分に使うこともありません。
それでは簡単な例題として、点と表すオブジェクトを作ってみましょう。コンストラクタ Point で作成するオブジェクトは 2 次元座標を表し、Point3D で作成するオブジェクトは 3 次元座標を表します。それぞれ、2 点間の距離を計算するメソッド distance() を定義します。プログラムは次のようになります。
リスト : 座標を表すオブジェクト // 2 次元座標 function Point(x, y) { this.x = x; this.y = y; } // 距離を求める Point.prototype.distance = function(p) { let dx = this.x - p.x; let dy = this.y - p.y; return Math.sqrt(dx * dx + dy * dy); } // 3 次元座標 function Point3D(x, y, z) { this.x = x; this.y = y; this.z = z; } // 距離を求める Point3D.prototype.distance = function(p) { let dx = this.x - p.x; let dy = this.y - p.y; let dz = this.z - p.z; return Math.sqrt(dx * dx + dy * dy + dz * dz); }
コンストラクタ Point, Point3D は座標を受け取り、それをオブジェクトにセットします。メソッド distance() は引数 p にオブジェクトを受け取り、this と p の距離を計算します。sqrt() は平方根を求める関数で、JavaScript のグローバルオブジェクト Math に定義されています。
それでは実行してみましょう。
> p1 = new Point(0, 0) Point {x: 0, y: 0 } > p2 = new Point(10, 10) Point {x: 10, y: 10 } > p3 = new Point3D(0, 0, 0) Point3D {x: 0, y: 0, z: 0 } > p4 = new Point3D(10, 10, 10) Point3D {x: 10, y: 10, z: 10 } > p1.distance(p2) 14.142135623730951 > p3.distance(p4) 17.32050807568877
このように、ドットの左側のオブジェクトによって適切なメソッドが呼び出され、ポリモーフィズム (polymorphism) がきちんと働いていることがわかります。
クラスベースのオブジェクト指向に慣れている方ならば、prototype で指定したオブジェクトを「クラス」と考えるとわかりやすいかもしれません。たとえば、オブジェクトで共通に使用する変数が必要な場合、prototype のオブジェクトに変数を定義することで実現することができます。これはクラスベースでいうところの「クラス変数」と同じ機能です。
簡単な例を示しましょう。
> function Foo() {} undefined > Foo.prototype = {bar: 1} {bar: 1} > a = new Foo() {} > b = new Foo(); {} > a.bar 1 > b.bar 1
プロパティ bar がクラス変数になります。new Foo() でオブジェクトを生成して、変数 a, b にセットします。a.bar と b.bar は同じプロパティ bar を参照するので、同じ値 1 になります。
bar の値を書き換える場合は注意が必要です。次の例を見てください。
> Foo.prototype.bar = 10 10 > a.bar 10 > b.bar 10 > a.bar = 20 20 > a.bar 20 > b.bar 10
最初の例は prototpye のオブジェクトを指定して、プロパティ bar の値を書き換えています。この場合、a.bar と b.bar は書き換えた値 10 になります。次に、a.bar = 20 を実行します。このとき、オブジェクト a にプロパティ bar が存在しないことに注意してください。
JavaScript の場合、オブジェクトにプロパティが存在しない場合、そのオブジェクトにプロパティを登録し、そこに値を代入します。つまり、オブジェクト a にプロパティ bar が作られて、そこに 20 がセットされるのです。この場合、prototype の bar は 10 のままなので、b.bar の値は 10 になります。a.bar の値は a のプロパティ bar を参照するので 20 になります。
このため、クラス変数を使用する場合は、prototype のオブジェクトに直接アクセスするよりも、次のようにアクセサを定義した方がよいでしょう。
リスト : クラス変数のアクセスメソッド function Foo() {} Foo.prototype = { bar: 1, show() { return Foo.prototype.bar; }, update(x) { Foo.prototype.bar = x; } }
メソッド show() で Foo.prototype の bar の値を参照し、update() で bar の値を更新します。簡単な例を示しましょう。
> a = new Foo() {} > b = new Foo() {} > a.show() 1 > b.show() 1 > a.update(10) undefined > a.show() 10 > b.show() 10 > Foo.prototype.show() 10 > Foo.prototype.update(100) undefined > Foo.prototype.show() 100 > a.show() 100 > b.show() 100
show() と update() は new Foo() で生成したオブジェクトから呼び出すことができますし、Foo.prototype に格納されたオブジェクトからも呼び出すこともできます。show() と update() は、クラスベースオブジェクト指向でいうところの「クラスメソッド」という機能に相当します。
クラスメソッドを定義するとき、this の使用には十分に注意してください。Foo.prototype からメソッドを呼び出すと、this の値は Foo.prototype のオブジェクトになります。ところが、オブジェクト a から呼び出すと this の値は a になります。たとえば、update() の処理を this.bar = x; とした場合、オブジェクト a から呼び出すと a のプロパティ bar に x の値を代入することになり、Foo.prototype の bar の値を書き換えることはできません。
ES2015 で導入された「クラス (class)」は新しい機能ではなく、プロトタイプベースのオブジェクト指向を使いやすくするための「糖衣構文」です。次の例を見てください。
リスト : 今までのクラス定義 function Foo(a) { this.a = a; } Foo.prototype.get_a = function() { return this.a; } Foo.prototype.set_a = function(a) { this.a = a; }
> obj = new Foo(10) Foo { a: 10 } > obj.get_a() 10 > obj.set_a(100) undefined > obj.get_a() 100
このプログラムを ES2015 の class 文を使って書き直すと次のようになります。
リスト : class 文によるクラス定義 class Bar { constructor(x) { this._a = x; } get a() { return this._a; } set a(x) { this._a = x; } }
> obj1 = new Bar(123) Bar { _a: 123 } > obj1.a 123 > obj1.a = 456 456 > obj1.a 456
クラスを定義する場合、従来は function を使っていたので、関数定義と間違える恐れがありましたが、class 文を使うと一目瞭然でクラス定義だとわかります。コンストラクタ (constructor) は初期化を行う特別なメソッドです。複数のコンストラクタを定義することはできません。メソッド定義も ES2015 で導入されたプロパティを省略する書き方をすれば簡単です。もちろん、getter, setter も簡単に定義することができます。
簡単な例として、点を表す Point クラスを示します。
リスト : Point クラス class Point { constructor(x, y) { this._x = x; this._y = y; } get x() { return this._x; } get y() { return this._y; } distance(p) { const dx = this.x - p.x; const dy = this.y - p.y; return Math.sqrt(dx * dx + dy * dy); } toString() { return `Point(${this.x}, ${this.y})`; } }
メソッド toString() をオーバーライドすると、オブジェクトを文字列に変換するとき、そのメソッドが呼び出されます。
> var p1 = new Point(0, 0) undefined > p1 Point { _x: 0, _y: 0 } > console.log('%s', p1) Point(0, 0) undefined > var p2 = new Point(10, 10) undefined > p1.distance(p2) 14.142135623730951
プロパティやメソッドの前に static を付けると、静的プロパティ (クラス変数) や静的メソッド (クラスメソッド) になります。クラス変数はオブジェクトからアクセスすることはできません。同様に、クラスメソッドは object.method() の形式で呼び出すことはできません。class.method() の形式で呼び出してください。このとき、this の値は class になります。
簡単な例を示しましょう。
リスト : スタティックメソッド class Baz { constructor(x) { this._a = x; } get a() { return this._a; } set a(x) { this._a = x; } static baz = 1; // static { this.baz = 1; } static get_baz() { return this.baz; } static set_baz(x) { this.baz = x; } }
> var obj = new Baz(123) undefined > obj.get_baz() Uncaught TypeError: obj.get_baz is not a function > Baz.get_baz() 1 > Baz.set_baz(2) undefined > Baz.get_baz() 2 > obj Baz { _a: 123 } > Baz [class Baz] { baz: 2 }
static { ... } を「静的初期化ブロック」といい、クラスの初期化の時に評価されます。この中でクラス変数を初期化することができますし、もっと複雑な初期化処理を行わせることも可能です。
最後に簡単な例題として、「連結リスト (Linked List) 」という基本的なデータ構造を作ってみましょう。なお、今回のプログラムは拙作のページ「お気楽 Python プログラミング入門: 連結リスト」のプログラムを JavaScript で書き直したものです。内容は重複していますが、あしからずご了承ください。
連結リストはデータを一方向につなげたデータ構造です。リストを操作するプログラミング言語では Lisp が有名ですが、Lisp で扱うリストが連結リストです。下図に連結リストの構造を示します。
(1)変数 ┌─┐ ┌─┬─┐ ┌─┬─┐ ┌─┬─┐ │・┼─→│10│・┼→│20│・┼→│30│/│ /:終端(null) └─┘ └─┴─┘ └─┴─┘ └─┴─┘ (2)ヘッダセル ┌─┬─┐ ┌─┬─┐ ┌─┬─┐ ┌─┬─┐ │ │・┼─→│10│・┼→│20│・┼→│30│/│ /:終端(null) └─┴─┘ └─┴─┘ └─┴─┘ └─┴─┘ 図 : 連結リスト
連結リストはセル (cell) というデータを繋げて作ります。セルにはデータを格納する場所と、次のセルを指し示す場所から構成されます。上図でいうと、箱がひとつのセルを表していて、左側にデータを格納し、右側に次のセルへの参照を格納します。リストの終わりを示すため、最後のセルの右側には特別な値(たとえば null)を格納します。そして、図 (1) のように先頭セルへの参照を変数に格納しておけば、この変数を使って連結リストにアクセスすることができます。また、図 (2) のようにヘッダセルを用意する方法もあります。
// // 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(",") + ")"; } } // 簡単なテスト 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));
$ node linkedlist.js true false List(0,1,2,3,4,5,6,7,8,9) 0 9 null 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 List(1,2,3,4,5,6,7,8,9) List(1,2,3,4,5,6,7,8) List(1,2,3,4,6,7,8) false 1 2 3 4 false 6 7 8 false
ES2015 の機能を使った簡単な例題として「二分木 (binary tree)」を作ります。二分木の詳しい説明は拙作のページ「Algorithms with Python: 二分木, ヒープ」をお読みください。
// // tree.js : 二分木 // // Copyright (c) 2017-2025 Makoto Hiroi // // Released under the MIT license // https://opensource.org/license/mit/ // // 節 class Node { constructor(item, left = null, right = null) { this._item = item; this._left = left; this._right = right; } get item() { return this._item; } get left() { return this._left; } get right() { return this._right; } set item(x) { this._item = x; } set left(x) { this._left = x; } set right(x) { this._right = x; } // 探索 static contains(node, x) { while (node != null) { if (x == node.item) { return true; } else if (x < node.item) { node = node.left; } else { node = node.right; } } return false; } // 追加 static add(node, x) { if (node == null) { return new Node(x); } else if (x < node.item) { node.left = Node.add(node.left, x); } else if (x > node.item) { node.right = Node.add(node.right, x); } return node; } // 最小値の探索 static min(node) { while (node.left != null) node = node.left; return node.item; } // 最小値の節を削除 static deleteMin(node) { if (node.left == null) { return node.right; } else { node.left = Node.deleteMin(node.left); return node; } } // 削除 static delete(node, x) { if (node != null) { if (x == node.item) { if (node.left == null) return node.right; if (node.right == null) return node.left; node.item = Node.min(node.right); node.right = Node.deleteMin(node.right); } else if (x < node.item) { node.left = Node.delete(node.left, x); } else { node.right = Node.delete(node.right, x); } } return node; } // 巡回 static forEach(func, node) { if (node != null) { Node.forEach(func, node.left); func(node.item); Node.forEach(func, node.right); } } // ジェネレータ static *generator(node) { if (node != null) { yield* Node.generator(node.left); yield node.item; yield* Node.generator(node.right); } } } // 二分木 class Tree { constructor() { this._root = null; } get root() { return this._root; } set root(x) { this._root = x; } // 追加 add(x) { this.root = Node.add(this.root, x); } // 探索 contains(x) { return Node.contains(this.root, x); } // 削除 delete(x) { this.root = Node.delete(this.root, x); } // 空にする clear() { this.root = null; } // 空か? isEmpty() { return this.root == null; } // 巡回 forEach(func) { Node.forEach(func, this.root); } // ジェネレータ *[Symbol.iterator]() { yield* Node.generator(this.root); } // 文字列に変換 toString() { return "Tree(" + [...this].join(",") + ")"; } } // 簡単なテスト 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 tree.js Tree() Tree(0,1,2,3,4,5,6,7,8,9) false true true true true true true true true true true false Tree(0,1,2,3,4,5,6,7,8,9) Tree(1,2,3,4,5,6,7,8,9) Tree(2,3,4,5,6,7,8,9) Tree(3,4,5,6,7,8,9) Tree(4,5,6,7,8,9) Tree(5,6,7,8,9) Tree(6,7,8,9) Tree(7,8,9) Tree(8,9) Tree(9) Tree() Tree()