ラベル クロージャ の投稿を表示しています。 すべての投稿を表示
ラベル クロージャ の投稿を表示しています。 すべての投稿を表示

2010年8月23日月曜日

JavaScript のクロージャ と オブジェクト指向

1. JavaScript における重要な概念

久しぶりに JavaScrip を書こうと思ったら、ほとんど頭の中から抜けている。 (+_+)

this とか prototype って何だっけ?というレベル。てゆうか考えてみたら、その辺読んだけど何かごちゃごちゃしていて頭に入らなったので面倒くさくなって言語仕様読むのやめたんだった。 ^^; シンプルなもの以外理解も記憶もできない。

4873113911

ところで、以前に 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 オブジェクトを返すメソッド )

 

関数を返す関数

例えば、「初期値」 と 「増分」 を持つ 「カウンター」 をモデル化するのにクロージャを利用してみる。次のことを念頭に置いて関数を定義。

  • 関数の引数は 「カウンター」 の 「初期値」 と 「増分」。
  • 返り値は、関数を呼出すごとに値を増分だけインクリメントする関数。
  • 最初に与えた初期値を保持する変数を、インクリメントするたびに現在の値に更新する。

オブジェクト指向からの類推で言えば、

  1. 関数の引数がプライベートなインスタンス変数
  2. 返される関数がメソッド
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) ではこれを、

  1. 関数の定義
  2. 関数をオブジェクトのプロパティとして設定

の 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 関数が設定されてないので、「人」 オブジェクトは年齢に関して答えることができないことはわかる。

「人」を継承した 「羽の生えた人」 と 「ヒレのある人」 を想定する。

img08-23-2010[1]

/**
 * 羽の生えた人
 */
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」 ヘつづく。

 

関連記事

参考

2009年10月6日火曜日

Google 検索と同時に Google ブックマーク の検索結果を表示する Greasemonkey スクリプト

ブックマークの保存に Google ブックマーク

保存しておきたいサイトやページは全て Google ブックマーク で管理している。Firefox のブックマークは、ツールバーに頻繁に見るサイトを置いているのと、「あとで読む」用の一時的な保管場所として使っているのみ。ブックマークをツリー状に整理整頓しなくなって久しい。 ^^;

オンラインブックマークに移行して随分経過した。当初、はてブ を使うことも考えたけれど、完全にプライベート目的だったので Google ブックマークを選んだ。

 

Firefox のアドオン Gmarks で素早く入力

Google ブックマークを使うときは、Firefox のアドオン GMarks を利用。ブックマークしたくなったら、Ctrl + D で素早くブックマーク。後で検索できるように、なるべく多くタグを付けておく。分類目的のタグに加え、読んだときに気になった単語、「おもしろい」「なるほど」などという感想もタグとして付ける。場合によっては、そこから連想すること、人から教えてもらったらその人の名前、人に教えたいことなら相手の名前をタグとしてつけることも。後で検索するときに手がかりをたくさんつけておくのがポイント。

 

検索時にブックマークも同時に検索してほしい

検索をしていて、以前に見たページに辿りつけなくなることはよくある。なんとなく見たいサイトをぼやっと覚えているものの、肝心のそこへ辿り着くための検索ワードが思い出せない。 (+_+) そんなとき、Google ブックマークで再検索すると見つかることがある。Google 検索の後、ブックマーク検索するのは面倒なので、検索と同時にブックマークも検索して表示するスクリプトを書くことにした。

動作イメージは下図の通り。検索した単語に応じて、右側にブックマーク検索の結果を表示する。ただし、表示されるブックマークは、Google ブックマークで検索したときの 1 ページ目のみを表示するというシンプルなものに。

091001-003.png

 

インストール

前提 : Greasemonkey がインストールされていること。

インストールはこちらをクリック → search_extention.user.js

更新情報
  • 2011.9.10 : Google Bookmarks からの検索結果が表示されない問題を修正。
  • 2010.11.15 : Google のデザインの変更に対応。ただし、Google の検索結果の幅を固定したので、幅の狭い PC では見ずらくなるかも。
  • 2010.7.31 : Google のデザインの変更に対応。マーカーとして数値を振る。

 

Firefox のブックマークを Google ブックマークへインポート

Google ブックマークを普段使ってない場合は、Google ツールバー を使うと Firefox のブックマークをインポートすることができる。

ツールバーの機能 : ブックマーク - ツールバー ヘルプ によると、

  1. ツールバーの [ログイン] ログインボタンをクリックして、Google アカウントにログインします。
  2. [ブックマーク] ドロップダウン メニューをクリックし、(…) [Firefox ブックマークをインポートする] をクリックします。
  3. インポートしたいブックマークのチェックボックスをオンにします。
  4. [インポート] をクリックします。

ただし、ブックマークを階層化している場合、分類しているフォルダが独立したタグになるのではなく、

フォルダ名1:フォルダ名2

のように `:’ で区切られた文字列で一つのタグになるようなので注意。

 

ソースコードについて

GitHub

当初、Gist – GitHub に置こうと思っていたけれど、なぜか途中で切れてしまったので以下に置いた。

Git の使い方覚えなければ。。 (+_+)

追記 (2010.7.31) : Windows で GitHub の利用 - msysgit, TortoiseGit を使って

 

オブジェクト間の関係

大雑把に言えば、以下の図のような動作。

091005-001

 

スクリプトでは、トップレベルのオブジェクトを名前空間として利用。以下の図では便宜的に名前空間をパッケージとして描いた。

classDiagram2

 

当初、スーパークラスに相当する Abstract 内のオブジェクトは考えていなかった。Google 検索時には Google ブックマークからデータを引っぱてくるだけでよいと。しかし、スクリプトを書いている内に、Google 検索時に はてブ 、Bing 検索時に Delicious から読み込みたいなど、検索サイトとオンラインブックマークの組み合わせを変えたくなるかもしれないと思、共通になりそうな部分を抽出した。必要であればスーパークラスのメソッドをオーバーライドするだけ。しかし、他を実装していないので、実際に書いたときに色々と問題がでてくると思うけれど。。 ^^;

一応想定では、スクリプトの起動部分である最後の文を

searchResultArea({
    searchSite: Google.searchSite(),
    bookmarkService: Hatena.bookmarkService()
}).load();

または、

searchResultArea({
    searchSite: Bing.searchSite(),
    bookmarkService: Delicious.bookmarkService()
}).load();

のように切り替えられるつもり。

 

処理の流れ

大雑把な流れは以下の通り。

sequenceDiagram

 

参考書籍

スクリプトを書くのに以下を参考にした。

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス より

4873113911

  • 3.9 グローバル領域の利用を減らす, p28
  • 4.10 クロージャ, p43
  • 5.2 オブジェクト指定子, p57
  • 5.4 関数型, p59

上記の本では、new 演算子を想定している関数に対して、普通に関数呼出しをした場合、グローバル変数を上書きしていしまう可能性があるために、new を JavaScript の「悪いパーツ」と評している。(p133) その代わりとして、クロージャによるメソッドのアクセス制御と継承の方法について解説があった。これを読み、new 演算子の必要性を感じなくなったので、オブジェクトの生成に new は全く使っていない。クロージャは、はじめ取っつきにくかったけれど、慣れたらクラスと同じように見えてきた。クラスベースのオブジェクト指向のように、クラスの構文を持ち込まなくても、関数だけで表現できるのというのはシンプルでいいかも。 ^^

ところで、下記コードにおける searchSite 関数内の defaultInitArea メソッドは、引数 `my’ によって、searchSite 関数 (コンストラクタ) によって生成されたオブジェクトを継承したオブジェクトからのみアセクス可能であることを想定したが、こういう書き方で良かったのかな? JavaScritp に不慣れなので自信なし。。 (+_+)

 

関連記事