Hatena::ブログ(Diary)

TechTalkManiacs このページをアンテナに追加 RSSフィード

2010-02-02

JavaScript基本概念最速マスター

 プログラミング言語の文法をまとめた最速基礎文法マスターが流行っていますが、それだけだと物足りないので少し視点を変えてJavaScriptという言語の基礎となっている概念について簡単にまとめてみようと思います。(基礎文法についてはこちらを参照してください)

 (20010/2/4 記述ミス Typoなどを修正しました)

JavaScriptの基本概念

 JavaScriptの基本となる概念は次の二つです。

  1. 連鎖指向
  2. 全てがオブジェクト

 連鎖指向はプロトタイプチェーンやクロージャ、全てがオブジェクトであるという性質は連想配列やプリミティブ型などの性質に関わってきます。

連鎖指向

 JavaScriptでは変数オブジェクト、メソッドなどのリソースの利用において鎖のようにリソースを定義や宣言できるポイントが連なり、一番近くの宣言や定義に基づいてリソースの内容が決定される、という仕組みが採用されています。

 その中でも有名なのはプロトタイプチェーンでしょう。

プロトタイプチェーン

 よく知られているようにJavaScriptオブジェクトプロトタイプベースであり、クラス定義は存在せず代わりにコンストラクタとしての関数定義のみが存在し、継承とは特殊なプロパティであるprototype継承元のインスタンスを代入することで行われます。もちろん、継承元が何らかのクラス継承している場合にはそれのprototypeにもまた継承元のインスタンスが代入されています。

 このようなprototypeが鎖のように連なっている特性から、JavaScript継承関係はプロトタイプチェーンと呼ばれています。

 プロパティやメソッドといったオブジェクトリソースアクセスする場合、最も近い定義であるそのオブジェクトの定義からプロトタイプチェーンをとたどり一番最初に見つけた定義によって解決されます。

 ここで大事なのは、その時点での最新の状態が反映されるということです。そのため、プロトタイプチェーンは次のような性質を持ちます。

  1. 継承元の定義が変更されていたら、その変更が反映される
  2. 新しくプロトタイプチェーン上のより近い位置に定義が追加されていたら、以降はそこの定義が反映される
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では以下の二つのルールに基づいて決定されます。

  1. 何らかのオブジェクトプロパティとして呼び出された場合はそのオブジェクト
  2. 関数オブジェクトのみで呼び出された場合は画面全体を現す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
}

favrilfavril 2010/02/04 03:23 わかりやすい説明ありがとうございました。勉強になりました。
細かいところですが、いくつかタイポが気になったので、以下に。

[高階関数]のコードで、foo.test(); する前に、
foo.test = hoge.test; か foo.test = test; してあげないと、
「foo.test is not a function」になります。

[クラスもオブジェクト]のコードの、
インデントがずれているのが気になりました。

for in のコードの、「testとhogeが混じる」というコメントは、
「testとfugaが混じる」が正しいです。

最後のコードのコメントで、「要素数は2なのに」とありますが、
loop[3]="hoge"; をした時点で、
loopは[undefined, undefined, undefined, "hoge"]という配列になるので、
要素数(loop.lengthの値)は4です。
(for in で回せば2回しかまわりませんが。)

あと、groval は global な気が。。。

kagigotonetkagigotonet 2010/02/04 12:15  ありがとうごさいました。ご指摘のあった部分を修正しました。
 配列の要素数は結構奇妙な動作しているのに今回はじめて気がつきました。

 今後ともよろしくお願いします。

nanto_vinanto_vi 2010/02/05 05:25 「ネームスペースチェーン」というあまりなじみのない語を使われていますが(私は初めて聞きました)、「基本概念」を説明するならきちんと定義された語か、またはある程度一般に意味の定まった語(この場合なら「スコープチェーン」等)を使ったほうがいいと思います。

プロトタイプチェーンでは「そのオブジェクト」、ネームスペースチェーンでは「関数の名前空間」をそれぞれ「終点」としていますが、終点から始点へとたどっていくこと、及び始点から遠いものを「近い」と表現することは直感的でない気がします。また、プロトタイプチェーン上で「そのオブジェクト」に近いものを「前にある」といい、スコープチェーンの先頭から識別子の解決が行われるとするES 3の記述とも反すると思います。

「基本概念」を抑えるのなら、JavaScriptにコンストラクタは存在しても(クラスベースオブジェクト指向における)「クラス」は存在しないこと(及び関数をコンストラクタとして使えること)を強調したほうがいいかと思います。

プリミティブ値がオブジェクトでない(数値、文字列値、真偽値はオブジェクトのように扱えますが)ので、「純粋なオブジェクト指向言語」とは言えないと思います。

「高階関数」の例で、higher-order(callback);とあるのはhigher_order(callback);の誤記でしょう。

「クラスもオブジェクト」以降のはてな記法が乱れています。

個人的に、「連想配列と配列は本質的に同じ」での「異常をきたし」「正常に動いてしまう」「おかしくなる」という表現に違和感を感じます。ES 3からすればどれも正常な動作なので、(コードを書いた人の)「期待に反する」「期待通り」といった表現にされてはいかがでしょうか。

kagigotonetkagigotonet 2010/02/05 14:08 nanto_vi様>

 ありがとうございます。ご指摘あったところをできる限り修正しました
 スコープチェインについては知らなかったので本当に助かりました。

通りすがりの名無し通りすがりの名無し 2012/02/09 10:07 非常にわかりやすくて感心しました。
別サイトでは一から基本であるとかオブジェクトの説明やらで要点がたらたらと分散していましたがここは必要なことが具体的な例で示してあり、助かります

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証