JavaScript基本概念最速マスター
プログラミング言語の文法をまとめた最速基礎文法マスターが流行っていますが、それだけだと物足りないので少し視点を変えてJavaScriptという言語の基礎となっている概念について簡単にまとめてみようと思います。(基礎文法についてはこちらを参照してください)
(20010/2/4 記述ミス Typoなどを修正しました)
JavaScriptの基本概念
JavaScriptの基本となる概念は次の二つです。
- 連鎖指向
- 全てがオブジェクト
連鎖指向はプロトタイプチェーンやクロージャ、全てがオブジェクトであるという性質は連想配列やプリミティブ型などの性質に関わってきます。
連鎖指向
JavaScriptでは変数、オブジェクト、メソッドなどのリソースの利用において鎖のようにリソースを定義や宣言できるポイントが連なり、一番近くの宣言や定義に基づいてリソースの内容が決定される、という仕組みが採用されています。
その中でも有名なのはプロトタイプチェーンでしょう。
プロトタイプチェーン
よく知られているようにJavaScriptのオブジェクトはプロトタイプベースであり、クラス定義は存在せず代わりにコンストラクタとしての関数定義のみが存在し、継承とは特殊なプロパティであるprototypeに継承元のインスタンスを代入することで行われます。もちろん、継承元が何らかのクラスを継承している場合にはそれのprototypeにもまた継承元のインスタンスが代入されています。
このようなprototypeが鎖のように連なっている特性から、JavaScriptの継承関係はプロトタイプチェーンと呼ばれています。
プロパティやメソッドといったオブジェクトのリソースにアクセスする場合、最も近い定義であるそのオブジェクトの定義からプロトタイプチェーンをとたどり一番最初に見つけた定義によって解決されます。
ここで大事なのは、その時点での最新の状態が反映されるということです。そのため、プロトタイプチェーンは次のような性質を持ちます。
- 継承元の定義が変更されていたら、その変更が反映される
- 新しくプロトタイプチェーン上のより近い位置に定義が追加されていたら、以降はそこの定義が反映される
//クラス定義 var Dog=function(){}; Dog.prototype.status=function(){ alert("寝ています"); } //Dogを単純に継承するクラス var Jp_Dog=function(){}; Jp_Dog.prototype=new Dog(); //Jp_Dogを単純に継承するクラス var Shiba=function(){}; Shiba.prototype=new Jp_Dog(); //インスタンスを生成 Pochi=new Jp_Dog(); Tarou=new Shiba(); //一番近いstatusの定義はDog Pochi.status();//寝ています Tarou.status();//寝ています //Dogでの定義を変更すると、全ての継承先が変更される Dog.prototype.status=function(){ alert("起きています"); }; Pochi.status();//起きています Tarou.status();//起きています //Shibaでstatusを定義 Shiba.prototype.status=function(){ alert("走っています"); }; //この後にDogの定義を変更しても //一番近い定義の位置が変わっているので //ShibaのインスタンスであるTarouには影響してない Dog.prototype.status=function(){ alert("遊んでいます"); }; Pochi.status();//遊んでいます Tarou.status();//走っています //TarouにRubyでいう特異メソッド(そのインスタンスだけのメソッド)として //statusを設定しても同じ Tarou.status=function(){ alert("ほえています"); }; Dog.prototype.status=function(){ alert("匂いをかいでいます"); }; //一番近い定義はTarou自身なので //Shibaの変更も反映されなくなる Shiba.prototype.status=function(){ alert("走っています"); }; Pochi.status();//匂いをかいでいます Tarou.status();//ほえています
ネームスペースチェーン
JavaScriptでは名前空間も連鎖します。関数定義の中で入れ子状に繰り返し関数を定義できるため、プロトタイプチェーンと同様に名前空間はグローバルな名前空間から始まって終端となる関数の名前空間へと連なることになります。いわばネームスペースチェーンですね。(追記 スコープチェーンという呼び方があるそうです)
関数内部の名前空間から変数へアクセスした場合の解決もまたプロトタイプチェーンと同じであり、もっとも近い名前空間から遠いものへとネームスペースチェーンを辿り、その中で最初に宣言されている変数の最新の状態が使用されます。これがJavaScriptにおけるクロージャやレキシカル変数と呼ばれているものの正体です。
var test="grobal"; //引数も変数宣言の一種。 function Sample(arg){ //関数定義の中で関数を定義する functon inner_a(){ //グローバル→Smaple→inner_aと名前空間が連鎖している //名前空間の一番端で変数testとargを宣言し、定義する var test="inner_a"; var arg="inner_a"; alert(test);//"inner_a" alert(arg);//"inner_a" }; //関数定義の中で関数を定義する function inner_b(){ //グローバル→Sample→inner_bと名前空間が連鎖している //inner_bとsampleではtestは宣言されていないので、グローバルな名前空間の宣言が使われる alert(test);//grobal }; function inner_c(){ //グローバル→Sample→inner_cと名前空間が連鎖している //inner_cではargは宣言されていないので、一番近くの宣言であるSampleでの定義が使われる。 alert(arg);//argの値。この場合は"Closure"; } inner_a(); inner_b(); inner_c(); } Sample("Closure");
ただしプロトタイプチェーンと違い、宣言であるという性格上ネームスペースチェーンではチェーンの上での最も近いポイントの位置を変更できません。変数宣言したタイミングに関わらずその名前空間で宣言したものとして扱われ、その変数にアクセスした時点で値が代入されていない場合はundefinedになります。
//グローバルな名前空間で定義 var test="grobal"; function Sample(){ //関数定義の中で関数を定義する functon inner_a(){ //グローバル→Smaple→inner_aと名前空間が連鎖しているが、この場合解決に使われるのはSampleの名前空間 alert(test); }; inner_a();//この時点ではtestは宣言されているものの未定義という扱いになり、undefinedと表示される //Sampleの名前空間におけるtestそのものの宣言と定義はここで行われている var test="Sample"; inner_a();//Sample } Sample();
全てがオブジェクト
大まかな分類ではJavaScriptは関数定義のある純粋なオブジェクト指向言語にあたりJavaScriptは関数型言語の一部を取り込んだオブジェクト指向言語にあたり、少数の例外を除けば関数も含めた全てがオブジェクトです。
そのため、Pythonのような同じタイプの言語と共通した次のような特徴を持ちます。
高階関数
関数はオブジェクトですので、関数を引数や戻り値とする高階関数が作れます。jQueryを使ったことがある人ならばプラグインの初期値としてコールバック関数を与えた経験があると思いますが、これが高階関数です。
また、JavaScript特有の書き方として、関数オブジェクトがクラス定義の基礎となります
//関数オブジェクトの定義 var callback=function(){ alert("ok"); } var higher_order=function(callback){ //引数として渡された関数を呼び出す callback(); }; higher_order(callback);
高階関数が扱える言語では呼び出し元となるオブジェクト自身を表す特殊変数this(またはself)の扱いが問題になりますが、JavaScriptでは以下の二つのルールに基づいて決定されます。
- 何らかのオブジェクトのプロパティとして呼び出された場合はそのオブジェクト
- 関数オブジェクトのみで呼び出された場合は画面全体を現すwindowオブジェクト
ただし、あらかじめ定義されているapplyやcallというメソッドを使えばthisの内容を変更できます。
window.fuga="grobal"; //関数オブジェクトの定義 var hoge={}; hoge.test=function(){ alert(this.fuga); } hoge.fuga="fuga"; var foo={}; foo.fuga="bar"; //関数オブジェクトを別の変数に var test=hoge.test; //オブジェクトのプロパティとして関数オブジェクトを代入 foo.test=hoge.test; hoge.test();//fuga test();//grobal foo.test();//bar //thisを別のものに foo.test.apply(hoge)//fuga
(サンプルコードがバグっていたのを修正)
クラスもオブジェクト
クラス自身もオブジェクト(正しくは関数オブジェクト)ですので、別の変数に代入したり引数や戻り値とすることができます。
var higher_order=function(){ //クラスを定義する var callback=function(init){ this.init=init; }; callback.prototype.test=function(){ alert(this.init); }; return callback; }; var test_class=higher_order(); var target=new test_class("ok"); target.test()//ok
また、JavaScript独自の特徴として次のようなものがあります。
連想配列と配列は本質的に同じ
連想配列と配列がオブジェクトなのは他の純粋なオブジェクト指向言語と同じなのですが、JavaScriptでは実はどちらもデータをオブジェクトのプロパティとして表現しています。つまり配列とは数値をキーとしたプロパティをもちかつ配列としてのメソッドを備えた連想配列の一種であり、配列や連想配列の各要素にアクセスするための[](ブラケット)とはオブジェクトのプロパティにアクセスする記法の一つに過ぎません。
この特性が大きく影響するのがfor構文でのループです。現在標準と言えるバージョンのJavaScriptではいわゆるforeach文が無いのですが、for構文を利用したループを使おうとした場合、プロパティをスキャンするfor in構文では配列が拡張された場合に、通常のfor文では追加されるべきでない位置にプロパティが追加された場合に動作に異常をきたしてしまいます思わぬ動作をしてしまいますので注意が必要です。
//通常Arrayクラスは、for in 構文ではスキャンされないDontEnumプロパティのみを持っている var loop=['a', 'b', 'c']; //そのままならば正常に動いてしまう for (var i in loop) { alert(i); } //Arrayクラスまたはloopに適当なプロパティを追加するとおかしくなる Array.prototype.test = function() {}; loop.fuga="hoge"; for (var i in loop) { alert(i); //testとfugaが混じる }
配列に本来は設定されるべきでない位置に設定された場合
//空の配列を作る var loop=[]; //こんなことができてしまう loop[-4]="hoge"; //要素数を表すプロパティlengthの値だけfor文でループ //しかし要素数は0になるので動かない for (var i = 0; i < loop.length; i ++) { alert(loop[i]);//undefined }