1. JavaScript における重要な概念
久しぶりに JavaScrip を書こうと思ったら、ほとんど頭の中から抜けている。 (+_+)
this とか prototype って何だっけ?というレベル。てゆうか考えてみたら、その辺読んだけど何かごちゃごちゃしていて頭に入らなったので面倒くさくなって言語仕様読むのやめたんだった。 ^^; シンプルなもの以外理解も記憶もできない。
ところで、以前に JavaScript のコードを書くときに参考にした本は、
JavaScript に関する本は、これしかまともに読んだことがない。
特に以下の部分が、JavaScript を使う上で参考になった。
- 3.9 グローバル領域の利用を減らす, p28
- 4.10 クロージャ, p43
- 5.2 オブジェクト指定子, p57
- 5.4 関数型, p59
上記の内容を思い出すために、例を考えながら、復習することに。
以下のコードは、 Aptana で書いて、 Firebug 上で実行した。
2. クロージャの意味
クロージャとは、レキシカルな環境において、束縛された自由変数を含む関数
まずはクロージャから。 Closure – Wikipedia によると、
… a closure is a first-class function with free variables that are bound in the lexical environment. Such a function is said to be "closed over" its free variable.
上記の First-class function とは、First-class function - Wikipedia によると、言語が以下の機能をサポートしていることを意味する。
- constructing new functions during the execution of a program,
- storing them in data structures,
- passing them as arguments to other functions,
- returning them as the values of other functions.
JavaScript は上記の要件を満たしているので、関数は first-class function 。この関数がレキシカルに束縛される自由変数を伴なっているものがクロージャ。
「レキシカル」 とは、Scope (programming) - Wikipedia によると、
With lexical scope, a name always refers to its (more or less) local lexical environment.
実行時のことを考えなくても書かれているコードを見れば、名前が何を指し示しているのかわかる。
「自由変数」 とは、Free variables and bound variables - Wikipedia によると、
… a free variable is a variable referred to in a function that is not a local variable or an argument of that function.
関数において、ローカル変数でも引数でも変数。
こういった関数がどのような状況で生じるかと言えば、先ほどの Closure – Wikipedia に戻り、
In some languages, a closure may occur when a function is defined within another function, and the inner function refers to local variables of the outer function.
関数の中で関数を定義し、内側の関数が外側の関数のローカル変数を参照する。
(cf. Ruby におけるクロージャの例 : Proc オブジェクトを返すメソッド )
関数を返す関数
例えば、「初期値」 と 「増分」 を持つ 「カウンター」 をモデル化するのにクロージャを利用してみる。次のことを念頭に置いて関数を定義。
- 関数の引数は 「カウンター」 の 「初期値」 と 「増分」。
- 返り値は、関数を呼出すごとに値を増分だけインクリメントする関数。
- 最初に与えた初期値を保持する変数を、インクリメントするたびに現在の値に更新する。
オブジェクト指向からの類推で言えば、
- 関数の引数がプライベートなインスタンス変数
- 返される関数がメソッド
var counter = function(val, step){ return function(){ val += step; return val; }; };
これを実行すると、
var c1 = counter(0, 1); console.log(c1()); // 1 console.log(c1()); // 2 console.log(c1()); // 3 var c2 = counter(100, 10) console.log(c2()); // 110 console.log(c2()); // 120 console.log(c2()); // 130
3. オブジェクト指定子とは
上記の関数における引数は、必要となる値を直接渡していた。 しかし、JavaScript: The Good Parts の 「5.2 オブジェクト指定子」 (p57) にはこのデメリットについて、次のように述べられている。
コンストラクタが受け取るパラメータの数が非常に多くなってしまうことは、よくあることだ。しかしそうなると、引数の順番を覚えるのがとても大変になってしまい、トラブルの原因にもなりやすい。
これに従い、オブジェクトを関数の引数として与えるように変更する。
var counter = function(spec){ return function(){ spec.val += spec.step; return spec.val; }; };
カウンターを生成する部分を変更。
var c1 = counter({val:0, step:1}); var c2 = counter({val:100, spec:10});
生成するときに、与える変数の順番を気にしなくていいところが便利。また、与える変数を増やす場合も、関数側で定義する引数を変更する手間が省ける。
4. オブジェクトを返す関数
上記では関数を返す関数を定義した。今度はオブジェクトを返す関数を定義してみる。ただし、上記と同様にクロージャが生成されるような構成にする。
関数を返す関数はメソッドが一つのオブジェクト。オブジェクトを返す関数は複数のメソッドがオブジェクトに詰め込まれているというイメージ。
例えば、「名前」 をフィールドに持つ 「人」 をモデル化したオブジェクトを返す関数を定義してみる。ここで生成される 「人」 オブジェクトは、メソッドとして 「名前」 を取得する関数と設定する関数を持つとする。
var person = function(spec) { return { getName : function(){ return spec.name; } , setName : function(n){ spec.name = n; } }; };
これを使い、
var tarou = person({name: "Tarou"}); console.log(tarou.getName()); // Tarou tarou.setName("太郎"); console.log(tarou.getName()); // 太郎
アクセス制御
重要な点は、以下のように person 関数によって生成されたオブジェクトの内部的なプロパティにアクセスできないということ。 (cf. JavaScript: The Good Parts, p59)
console.log(tarou.name); // undefined
これに対して new を用いて関数を呼出した場合は、クラスにおけるプライベート変数のような用い方ができない。
var Person = function(name){ this.name = name; }; var tarou = new Person("Tarou"); console.log(tarou.name); // プロパティに直接アクセス tarou.name = "太郎"; // プロパティを直接変更 console.log(tarou.name);
this が指すものは、呼出されたメソッドをプロパティとして持つオブジェクトであり、そのプロパティは外部からアクセス可能。
オブジェクトを返す関数 と クラス指向の言語におけるコンストラクタ
ところで、典型的なクラス指向の言語では、クラスを雛形とし、 new 演算子によりクラスに定義されたコンストラクタが呼出されオブジェクトが生成される。これに対して、上記で定義した関数 person はオブジェクトを返す普通の関数だった。
この点を考えると、クラス指向の言語でオブジェクトを生成する方法は、関数 person と同じくオブジェクトを生成するための手段であり、オブジェクトを生成することを意味的に明確にするための方便であることがわかる。
その意味で Douglas Crockford が勧めている「関数型」 (JavaScript: The Good Parts, p59) と呼んでいる 「クロージャを使ってオブジェクト生成する方法」 は、関数という手段のみが使われているシンプルな方法。わざわざ new 演算子を導入する必要がない。
JavaScript で new 演算子と併用する this や prototype チェーンを使う前に、関数だけで書ける事柄は余計なものを使わずに書く方が頭の中がスッキリして良さげ。
プライベートメソッド
先ほどの person 関数は返すオブジェクトの中にメソッドが定義されていた。JavaScript: The Good Parts (p.61) ではこれを、
- 関数の定義
- 関数をオブジェクトのプロパティとして設定
の 2 段階に分けて書くことが推奨されている。
var person = function(spec) { var that = {}; var getName = function(){ return spec.name; }; that.getName = getName; var setName = function(n){ spec.name = n; }; that.setName = setName; // これを削除 return that; };
これにより、setName 関数を that のプロパティに設定しなければ、setName はプライベートメソッドになる。
5. 継承
次にオブジェクト指向の拡張に相当するものを書いてみる。(cf. JavaScript: The Good Parts , p60)
その前に準備として、「人」 オブジェクトを以下のように変更。
- 年齢をフィールドに持つ
- 年齢を問い合わせても答えない
ただし、「人」 を継承したオブジェクトは年齢を答えるものとする。また、継承したオブジェクトから利用できるメソッドを 「人」 オブジェクトは持つものとし、これを defaultGetAge 関数とするなら、
var person = function(spec, my){ my = my || {}; var that = {}; var getName = function(){ return spec.name; }; that.getName = getName; var defaultGetAge = function(){ return "私の年齢は" + spec.age + "です"; }; my.defaultGetAge = defaultGetAge; return that; };
上記 my の使われ方は、person 関数を拡張した関数を見てからの方が理解しやすい。少なくとも、that のプロパティに defaultGetAge 関数が設定されてないので、「人」 オブジェクトは年齢に関して答えることができないことはわかる。
「人」を継承した 「羽の生えた人」 と 「ヒレのある人」 を想定する。
/** * 羽の生えた人 */ var personWithWings = function(spec, my){ my = my || {}; var that = person(spec, my); var fly = function(){ return "飛んでるぅ~"; }; that.fly = fly; var getAge = function(){ return my.defaultGetAge(); } that.getAge = getAge; return that; } /** * ヒレのある人 */ var personWithFin = function(spec, my){ my = my || {}; var that = person(spec, my); var swim = function(){ return "泳いでるぅ~"; }; that.swim = swim; var getAge = function(){ return my.defaultGetAge(); }; that.getAge = getAge; return that; }
拡張した関数から、拡張元の関数に変数 my を渡し、そこへ未公開の関数を詰め込んでもらうイメージ。
これを使ってみる。
var hanako = person({ name: "Hanako", age: 15 }); var jiro = personWithWings({ name: "Jiro", age: 15 }); var tarou = personWithFin({ name: "Tarou", age: 21 }); console.log(hanako.getName()); // Hanako console.log(jiro.getAge()); // 私の年齢は15です console.log(jiro.fly()); // 飛んでるぅ~ console.log(tarou.getAge()); // 私の年齢は21です console.log(tarou.swim()); // 泳いでるぅ~
「Javascript から見る Ruby のイテレータ – Enumerable」 ヘつづく。
2コメント:
var c2 = counter(100, 1)
は
var c2 = counter(100, 10)
の間違いですか?
訂正しました。ありがとうございます。
コメントを投稿