ページ

2010-12-12

JavaScriptの勉強:A re-introduction to JavaScript

これはいいですね。一貫して記述されています。まずはこれをじっくり読むこととしましょう。


基本データ型と制御構造については、既存の、あるいは他の言語とほぼ同じといっていいでしょう。全くの初心者以外はそのあたりは読み飛ばしで充分です。他の言語と大きく違ってくるのは、オブジェクトの扱いです。ということで、この文書でも「オブジェクト」以降をじっくり読むことにします。

オブジェクト

JSでは基本型以外はすべてオブジェクトとして扱われるそうです。そして、オブジェクトは名前付きの連想配列になっていると。マシン性能の向上によって連想配列の処理が高速化されたことで、連想配列ベースでもそれなりの性能が得られるようになったからでしょうね。昔の貧弱なマシンでは連想配列は便利だけど実用性に乏しいと考えられていましたから。他言語のハッシュ系と比較していますが、連想配列が本来のデータ構造で、その効率的な実装としてハッシュマップが採用されている、というのが正しい言い方でしょう。機能としての連想配列はハッシュ以外でも実装できますから。連想配列はキーバリューペアの集合として構成されます。JSではキーが文字列、バリューは任意のもの(基本型、あるいはオブジェクト)です。バリューとしてオブジェクトを持つことができるので、任意の複雑なデータ構造(あるいはとんでもないデータ構造)が構成できるようになっています。

オブジェクトは「new」演算子にとって、あるいはオブジェクトリテラル表現({}でくくります)によって、(動的に)生成されます。リテラル表現は初期のJSにはなかったそうですが。他のスクリプト言語でもリテラル表現を持つものと持たないものがありますが、リテラル表現が無いとコードがすごく読み難くなります。結構重要なポイントでしょう。今のperlはどうですかね。昔のperlにはリテラル表現が無かったので、そのためだけにPython使ったことがありましたが。

var o = new Object();  new 演算子で生成
var o = {} ;                  オブジェクトリテラルで生成

オブジェクトのプロパティなる言葉が唐突に(説明なしで)出現しています。まあ、これぐらいは誰でも判るはずなのでいいのかも知れませんが、文書の組み立てとして引っ掛かる点です。実態としてはオブジェクト=名前付き連想配列で、プロパティ=連想配列の要素、ですね。プロパティへの参照方法、"."参照と、インデックス参照”[]"がでてきます。プロパティと言われると定義していないのに、いきなり参照可能なところが気持ち悪いのですが、連想配列であれば、そのあたりは勝手に使用できるので納得できます。

  o.name = "Name";     "."演算子による参照
  var n = o.name;

  o["name"] = "Name";   インデックス参照
  var n = o["name"];

ただ、こういう定義なしで勝手に使える機能というのは便利なようでありながら、大規模コードを書く場合にはエラーの発生源になります。このためコンベンショナルな言語では、あえて、このような機能は使えないようにしています。例えば上でキー文字列を name、namae と間違えたとしましょう。コンベンショナルな言語では言語処理系によって間違いが検出されますが、連想配列ではどちらも正しいので、そのまま動作してしまいます(が結果は変になります)。大規模コードでこのような間違いを探すのはとても大変です(perlでこの手のミスに何度泣かされたことか)。

配列

連想配列ベースのくせに専用の「配列」があるのですね。少々気持ち悪いのですが。配列も(連想配列ベースの)オブジェクトであるなら色々ないたずらができそうですが、どうなるのでしょう。例えば a.length に変な値を入れて見るとか、変なインデックス(数字以外)で参照して見るとか。試してみました。a.length 勝手に変更できてしまいます。小さい値にすると配列の後ろのデータが失われます。大きな値にした場合にはその時には特に影響ないようですが、後で、例えば a.push("c") すると、勝手に変更した length のところに設定されます。勝手なインデックスを指定した場合には、オブジェクト(連想配列)にはきちんと記録されますが、配列を評価した場合には本来の配列要素しか表示されません。まあ、いろいろと気持ちの悪い動作になりますね。

注意:
JS Console にJSコードをペーストして実行させると(JSコンソールがマルチライン入力できないので)、本来の行単位とは微妙に異なった順序で実行されてしまうようです。例えば、以下の文をペーストして実行させると、ログ出力(イタリック部分)では配列は最終的な状態で示されます。

var a = ["A","B"];
console.log(a.length, a);
a.push("C");
console.log(a.length, a);
a.push("D");
console.log(a.length, a);
2 ["A", "B", "C", "D"]
3 ["A", "B", "C", "D"]
4 ["A", "B", "C", "D"]

しかしこれを分割して実行させると、次のように正しい結果になります。

var a = ["A","B"];
console.log(a.length, a);
2 ["A", "B"]
a.push("C");
console.log(a.length, a);
3 ["A", "B", "C"]
a.push("D");
console.log(a.length, a);
4 ["A", "B", "C", "D"]

JS Console での行入力評価のメカニズムの問題だと思いますが、気持ち悪いですし、何をやっているのか判らなくなりますので、注意が必要でしょう。

関数

単純に関数だけを取り上げた場合には普通の言語と変わりません。無名関数もいまやどこでも使われる技術になってきていますので、珍しいものでもないでしょう。多少違いがあるのは、パラメタについてのチェックが全く行われない点と、関数呼び出しに関する情報が関数内部からアクセス可能になっている点ぐらいでしょうか。呼び出し情報は「arguments」という変数(と書かれていますがオブジェクトと呼びたいですね)に格納されていて、呼び出された関数そのもの(無名関数の再帰呼出で必須)とパラメタリスト(パラメタ配列)が含まれています。色々なシチュエーションで使えます。

カスタムオブジェクト

このあたり、スクリプト言語派の悪い癖、こんなこともできるんだぜい!、が出ていますね。カスタムオブジェクトの構成方法、使い方とは関係のないことで異様に盛り上がっています。スクリプト屋ってどうしてこういうことに凝りたがるのでしょうね。というわけでここは無視。自前で調べていったところをまとめておきます。

new と this による結合

上のオブジェクトの生成方法のところで、new によって生成する方法と、オブジェクトリテラルで生成する方法とが説明されていました。非常に重要なくせにそこではスルーされていたのですが、newによってオブジェクトを生成する場合には、その対象(newの右側)は関数でなければなりません。実際サンプルでも

var obj = new Object();

と、(判りにくいですが)Object関数(ちゃんと()が付いています)に対して適用しています。普通の言語では new の右はクラス名ですが、JSではこれが関数(名前付き)になります。つまり他の言語でのクラス定義に相当することをJSでは関数で行ないます。すごく気持ち悪いですが。

var obj = new Func();

newによって新しいオブジェクトが生成されます。初期状態で生成されたオブジェクトは空(厳密には Object のクローン)になっています。新たに生成されたオブジェクトは、関数内では this によって参照されます。関数内で this 経由で新規に生成されたオブジェクトの連想配列にデータや関数を追加していくことができます。thisを使わない限り生成されたオブジェクトは Object() のクローンに過ぎないので、Objectの機能しか使えません。

new の対象となる関数内で this による新規生成されるオブジェクトとのバインディングが行われる場合、そのような関数をコンストラクタ関数と呼びます。実際には空で作成されるオブジェクトへの初期設定を行なう関数になっているわけですね。new の対象になる関数に this による参照(バインディング)が存在していなければ、生成されるオブジェクトは空のままです。構文的には同じでも無意味な文になります。

つまり、

var obj = new Func()

は、概念的には以下の処理と同じことをやっていると考えることができます(後述のように他のこともやるようですが)。

var obj = new Object();
Func(this = obj)

このように考えると、個人的にですが、JSの関数、オブジェクトの関係に、やっと納得することができました。オブジェクトの初期化を行なうので、new の右には関数が必要、という発想なのでしょう。

Prototype

コンストラクタ関数ですが、いつも空の Object クローンから組み立てていくのはナンセンスです。普通のオブジェクト指向言語でいうところの継承メカニズムも必要になります。それを支えるものが prototype オブジェクトです(単なる連想配列と理解した方が判りやすいですね)。"prototype"連想配列は生成元のコンストラクタ関数に結合されているイメージで、その結果、同じコンストラクタ関数から生成(new)されたオブジェクトは同じ(コンストラクタ関数に結合された)prototype連想配列を共有します。つまり、コンストラクタ関数の prototype 連想配列に記載されたキーバリューペアは、そのコンストラクタから生成されたすべてのオブジェクトで共通して使用することができる、ということです。

"protptype"連想配列に対しては、特殊なルックアップが行なわれ、this. による参照が(prototypeを明示しなくても)及びます。オブジェクトインスタンスに目的のキー名が見つからなかった場合には、ルックアップはコンストラクタ関数の prototype 連想配列に対して行なわれ、さらにコンストラクタ関数の元になった更に上位のコンストラクタ関数に、最終的には Object コンストラクタにまで及びます。そのため、特に定義していなくてもObjectコンストラクタで定義されている toString メソッドが利用可能になるわけです。

オブジェクト(クラス)の継承は prototype 連想配列のルックアップの連鎖によって行なわれます。問題というか便利というか、prototype 連想配列のマップはプログラムから変更できてしまいます。このため都合に合わせて上位クラスの振る舞いを変えてしmじゃうことができるのですが、気を付けないと思わぬ副作用を生むことになります。まあ、直接の親以外は変にいじらないのが正解でしょう。スクリプト派の意見ではこれは非常に便利な機能、ということですが、 strictly typed 派としては危険極まりない機能、といえます。

気になる点:
解説に出ているコードでは、コンストラクタ関数の外部で prototype に関数を設定(定義)しています。ですが、これは非常に気持の悪いコードです(クラス定義は同じところで行なわれるべきです)。やり方としては、コンストラクタの prototype にメソッド定義があるかどうかを調べ、なければ追加する、という方法が考えられますが(下記のコード例)、これとてあまり綺麗とはいえません。どのような方法がJS的に「正しい」方法なのか知りたいところです。

function PersonName(pname, fname) {
  this.familyName = fname ;
  this.personalName = pname ;
  if (! PersonName.prototype.getWesternName) {
     PersonName.prototype.getWesternName = function() {
       return (this.personalName + " " + this.familyName) ;
     }
     PersonName.prototype.getEasternName = function() {
       return (this.familyName + " " + this.personalName) ;
     }
  }
}

内部関数

関数名がひどい(もっとましな例が必要)ですが、意味的には判ります。関数内に更に関数を定義した場合、内部の関数は上位関数の変数にアクセスできる(ので複数の内部関数で来データを共有することができる)のですが、外部からはその変数にも内部関数にもアクセスできない、という隠蔽メカニズムが働きます。

しかし、本当に大規模プログラムを書くのであれば、ここで例としてあげられたメカニズムだけでは充分ではありません。オブジェクト内の変数、関数に対する隠蔽メカニズムが必要です。でないと、上位関数から戻った時点で内部変数は失われてしまいます。オブジェクトが生存している間維持されながら、外部からはアクセス出来ない内部変数、内部関数がサポートされなければなりません。今までに出てきた方法では、オブジェクトのインスタンス変数は this によって(新規生成される)オブジェクトに結び付けられます。その結果、それらの変数、関数は外部からアクセス可能になってしまいます。

このあたりは、このページからリンクされていた「世界で最も誤解されたプログラミング言語」の更にその先にリンクされていた(英語の)解説ページ、Private Members in JavaScriptClassical Inheritance in JavaScriptで実現する手法が説明されています。可能なのはわかりますが、使いやすいとは言いがたい代物です。なお、これらの手法では後述のクロージャを利用していますので、クロージャの解説を理解してから読むべきです。

クロージャ(closure)

クロージャも使える言語が増えてきています。ここでは実際のコードでクロージャの実例を示しています。

function makeAdder(a) {
    return function(b) {
        return a + b;
    }
}
x = makeAdder(5);
y = makeAdder(20);
x(6)
11
y(7)
27

関数 makeAdderは、無名関数を返す(従って返り値は加算関数)のですが、そこで、外側の関数の変数 a への参照が含まれています。xが生成された時にはその値は5、yが生成された時にはその値は 20、で生成された関数にはその時の値が保持されているわけです。このような、実行時の環境(この場合には外側の関数の変数aの値)を取り込んで生成された関数をクロージャと呼びます。

JSでは関数が実行されるときには、その関数が参照する外部データのコピーを保持するオブジェクト 'scope' が関数とセットで生成されます。上の例では x が生成されたときには値5の変数aを持ったスコープが、yが生成されたときには値20の変数aを持ったスコープが生成されています。スコープは生成された関数への参照(x、y)が存在する間保持され、参照が失われるとガベージとなって(いつか)回収されます。


function Counter(init) {
  var ival = init ;
  function privdec() {
    ival = (ival-1) > 0 ? (ival-1) : 0 ;
    return ival ;
  }
  function privinc() {
    ival += 1 ;
    return ival ;
  }
  function privshow() {
    return ival ;
  }
  this.inc = function() { return privinc() } ;
  this.dec = function() { return privdec() } ;
  this.show = function() { return privshow() } ;
}
var c = new Counter(2);
undefined
c.show()
2
c.inc()
3
c.inc()
4
c.dec()
3
c.ival
undefined

上のコードではコンストラクタ関数の内部変数 ival を内部関数 privinc,privdec,privshow から参照させることで、生成されたオブジェクト c のクロージャに含めていて、結果としてオブジェクトのプライベート変数(最後の行が示すように外部からはアクセス不可)を実現しています。

メモリリーク

クロージャの副作用としてメモリリークが発生しやすくなります。function外部への参照が含まていると、それらの参照先を取り込んだクロージャが生成され、目に見えない形で格納先の変数、オブジェクトに結び付けられます。クロージャは格納先の変数、オブジェクトが無効化されたなら(参照が無くなったなら)ガベージコレクタによって回収されます。が、明記されないだけあって、参照が残ってしまうこともありますし、相互に参照し合う循環参照が起きると、そのようなオブジェクトは(プロセスが終了しない限り)永久に残ってしまいます。このページでは以下のような、クロージャによる循環参照の発生例が載っていました。いかにもありがちなコードです。

function addHandler() {
    var el = document.getElementById('el');
    el.onclick = function() {
        this.style.backgroundColor = 'red';
    }
}

内側の無名関数は、外側の addHanlder関数の el 内部変数に格納されるため、クロージャが生成され、el.onclick に結び付けられます。一方生成されたクロージャには el.onclick が含まれています。このため、変数 el と無名関数のクロージャは相互に参照し合う関係になり、結果、これらのオブジェクト、クロージャで使用されたメモリは回収されなくなります。

この問題の解決策ですが、このページで解説されている方法はふたう。ひとつはこの手のGC依存言語で良く使われる方法で不要な参照を切る、というものです。

function addHandler() {
    var el = document.getElementById('el');
    el.onclick = function() {
        this.style.backgroundColor = 'red';
    }
    el = null;
}

最後のnull代入によって変数elから無名関数への参照が切られ、循環参照が解決されます(参照だけでelが示していた実体、DOM自体、は残っていますが)。もうひとつこの文書で解説されている方法は実に巧妙な手法で、関数呼び出し(とそれによって生成されるクロージャ)を、参照されない他のクロージャの中で行なう、というものです。

function addHandler() {
    var clickHandler = function() {
        this.style.backgroundColor = 'red';
    }
    (function() {
        var el = document.getElementById('el');
        el.onclick = clickHandler;
    })();
}

確かに、内側の無名関数呼び出しによって生成されるクロージャはどこからも参照されていないので、不要になれば勝手にGCされるので、ある意味実に綺麗な解決策なのですが、普通のコード慣れの感覚ではまず思いつきません。クロージャを使い慣れるとこういう発想ができるようになるものなのでしょうかね。

補足:メモリインスペクタ
昔から、プログラムの実行に伴うメモリリークへの対策となると、まあ注意深いコーディングは別として、最終的にはメモリインスペクタでメモリの使用状況をチェックする、が使われてきました。まあ、インスペクタがどの程度メモリの状況を追跡できるかはインスペクタによって随分と差があり、割付解放状況を記録するだけのものから、完全に追跡してレポートを生成するものまで色々ですが。

特に最近のGC依存の実行システムはGC自体がメモリの使用状況を追跡するので、できのいいメモリインスペクタが用意されている(あるいは別途入手可能になっている)ものです。JavaScriptの場合はどうなっているのか、と気になって調べてみると、JSの実行環境、多くの場合はWEBブラウザ、とセットになって用意されていますね。Chromeの場合には、デベロッパーツールのProfileでメモリ使用状況のスナップショットが取れるようになっています。オブジェクトの種類ごとにメモリ使用状況がまとめられて表示されますので無論クロージャの項目もあります)、普通に追跡する程度であればこれで充分な情報を得ることができるでしょう。

追記 2010/12/13

循環参照によるメモリリークは普通プロセス終了まで解消されません。この点を考えると Chrome のタブ毎にプロセスを割り付ける方法はものすごく画期的なのかも知れません。タブクローズだけで(他に影響なく)メモリリークが解消できることになりますからね。

0 件のコメント:

コメントを投稿