読者です 読者をやめる 読者になる 読者になる

M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

『開眼!JavaScript』第2章〜第4章のエッセンス

book JavaScript

『開眼!JavaScript』の説明は明らかに冗長。ではこころみに、同書について、その第2〜4章のエッセンスだけをまとめてみよう。

開眼!  JavaScript ―言語仕様から学ぶJavaScriptの本質

開眼! JavaScript ―言語仕様から学ぶJavaScriptの本質

第2章 オブジェクトのプロパティとprototypeチェーンについて説明

同書ではこれらの説明に12ページ分の紙幅を要している。

オブジェクトはすべての値をプロパティとして格納できる...言うまでもなく。(§ 2.1)

このセクションの存在理由がわからない。「オブジェクトはすべての値をプロパティとして格納」できないほうがびっくりである。

プロパティが参照するオブジェクトのプロパティが参照する…はい、いくらでも可能です。(§ 2.2)

このセクションの存在理由がわからない(その2)。

  • プロパティはオブジェクトを参照できる。
  • プロパティはプリミティブも参照できる。
  • そしてプリミティブもnullやundefinedでないかぎり、オンデマンドにラッパーオブジェクトが生成/廃棄される。

よって、プロパティ参照は連鎖的に記述できる(o1.p1.p1_1.p1_1_1...)。以上。

繰り返しになるけれど、こんなことをいちいち説明する理由はない(それも2ページの紙幅を使ってまで…。ついでに言うとFunctionオブジェクトの例はあきらかにまちがいで、これはプロパティによる参照の例になっていない。プロパティとローカル変数を混同してはならない)。

プロパティにアクセスするための2つの方法(§ 2.3)

さっくりまとめよう。

  • プロパティにアクセスする方法にはドット記法とブラケット記法の2つがある。
  • ブラケット記法では文字列を参照する変数を使用したプロパティ・アクセスが可能。
  • ブラケット記法では予約語や(ドット記法では)構文規則に抵触してしまう名称のプロパティでもアクセスが可能。
var o1 = {foo: 1, bar: 2, "class": 3, "123": 4};
var s1 = "foo";

alert(o1.foo); // => "1".
alert(o1["foo"]); // => "1".

//alert(o1.s1); // => "undefined".
//alert(o1.class); // 実行するとエラー。
//alert(o1.123); // こちらもエラー。

alert(o1[s1]); // => "1".
alert(o1["class"]); // => "3".
alert(o1["123"]); // => "4".
alert(o1[123]); // => "4".

delete演算子によるプロパティ削除(§ 2.4)

さっくりまとめよう(その2)。

  • delete演算子はオブジェクトのプロパティを削除する唯一の手段である。
  • プロパティにundefinedやnullを設定するのとは異なる。文字通り削除する。
  • プロトタイプ・チェーン(後述)を遡って削除することはない。
  • 配列要素に適用した場合、当該配列オブジェクトのlengthプロパティは変化しない。
var o1 = {foo: 1, bar: 2, baz: 3};

delete o1.bar;
alert("bar" in o1); // => "false".

o1.baz = undefined;
alert("baz" in o1); // => "true" !

delete o1.toString
alert("toString" in o1); // => "true".

var a1 = ["foo", "bar"];

a1.push("baz");
alert(a1.length); // => "3".

delete a1[2];
alert(a1.length); // => "3" !


alert(a1[2]); // => "undefined".
alert(1 in a1); // => "true".
alert(2 in a1); // => "false".

プロパティの検索(§ 2.5)

さあ、やってきました。プロトタイプ・チェーンのお話です。

  • あるオブジェクトに対してプロパティ名が指定されたとき、まっ先に検索されるのは当該オブジェクトそのものである。
  • 当該オブジェクトそのものが持つプロパティのなかに見つからなかった場合、次に検索されるのはそのオブジェクトのコンストラクタ関数のprototypeプロパティが参照するオブジェクトである。
  • ここでも見つからなかった場合、次に検索されるのは前述のprototypeオブジェクトのコンストラクタ関数のprototypeプロパティが参照するオブジェクトである。
  • 以降、繰り返し。このprototypeチェーンの終着点となるのはObject.prototypeである。
  • ひとたび名前の合致するプロパティが見つかった場合は検索はそこでストップして、そのプロパティが参照するオブジェクトが返される。より上流に同名のプロパティが存在するかも知れないが、それはもはや関知されない。
  • しかしより下流にあるプロパティがdelete演算子で削除されると、より上流にあるプロパティが検索対象になる。
  • いずれにしてもprototypeチェーン上のいかなる場所にもその名前のプロパティが存在しない場合、エラーとなる。
var a1 = ["foo", "bar"];
a1.baz = "baz";

alert(1 in a1); // => "true".
alert("baz" in a1); // => "true".
alert(1 in Array); // => "false".
alert("baz" in Array); // => "false".
alert(1 in Array.prototype); // => "false".
alert("baz" in Array.prototype); // => "false".
alert(a1.hasOwnProperty("baz")); // => "true".

alert("toLocaleString" in a1); // => "true".
alert(a1.hasOwnProperty("toLocaleString")); // => "false".
alert("toLocaleString" in Array.prototype); // => "true".
alert(Array.prototype.hasOwnProperty("toLocaleString")); // => "true".
alert("toLocaleString" in Object.prototype); // => "true".
alert(Object.prototype.hasOwnProperty("toLocaleString")); // => "true".

alert(a1.toLocaleString()); // => "foo, bar"
alert(Array.prototype.toLocaleString.apply(a1)); // => "foo, bar"
alert(Object.prototype.toLocaleString.apply(a1)); // => "foo, bar"
alert(Array.prototype.toLocaleString.apply({foo: 1, bar: 2})); // => "".(空文字列)
alert(Object.prototype.toLocaleString.apply({"foo": 1, "bar": 2})); // => "[object Object]".

ところで上記のコード例のとおり、Array()オブジェクトのprototypeオブジェクトには独自に実装されたtoLocaleStringが「それ自身のプロパティ」として存在している。

Object.prototype.toLocaleStringは実際のところObject()オブジェクトもArray()オブジェクトも処理できるが、Array.prototype.toLocaleStringはそうではない(Object()を与えられると空文字列を返す)。

そこで以下のようにArray.prototypeそれ自身のプロパティとしてのtoLocaleStringを削除すると、シャドウ化されていたObject.prototype.toLocaleStringが検索されるようになる。

var a2 = ["foo", "bar"];
var o1 = {foo: 1, bar: 2};

alert(a2.toLocaleString()); // => "foo,bar".
alert(a2.toLocaleString.apply(o1)); // => "".(空文字列)

delete Array.prototype.toLocaleString;
alert(a2.toLocaleString()); // => "foo,bar"
alert(a2.toLocaleString.apply(o1));  // => "[object Object]".

try{
    delete Object.prototype.toLocaleString;
    alert(a2.toLocaleString());
}catch(e){
    alert(e.message); // => "Object [object Array] has no method 'toLocaleString'"
}

Object.prototype.toLocaleStringをdelete演算子で削除すると、もはやArray()オブジェクトに対してtoLocaleStringメソッドをコールすることはできなくなる。

hasOwnProperty()メソッドとin演算子とfor-inループ(§ 2.6〜2.8)

メソッドも演算子もすでに登場しているから説明はいるまい。注意すべきはfor-inループ。

  • for-inループでは「列挙可能」でないプロパティ(propertyIsEnumerable()がfalseを返す)は処理対象とならない。
  • これはin演算子の挙動とは異なっている。

好学の士に向けて、余談をいろいろ。(§ 2.9〜2.10)

リファレンス的なはなしなのでここでは省略。

第3章 Object()について懇切丁寧に(くどくど)説明

ここでうれしいお知らせ。第3章はまるまる読み飛ばせるよ!(§ 3.1〜3.6)

百歩譲って、§ 3.4 のメソッド・リファレンス(hasOwnProperty、isPrototypeOf、propertyIsEnumerable、エトセトラ)はちょっと参考なる、といったところ。

この章は2章に統合されていてしかるべき内容なのだけど、そこに6ページを費やしている。

第4章 Function()についてまたしても懇切丁寧に(...)説明

同書ではここで16ページ分の紙幅を要している。しかし例によって、前後の章ですでにあつかったトピックを再掲したり、その場で説明すれば良いことを後出しにしたりと、無駄が多い。面倒なので章ごとがばっとまとめてしまおう。

関数はJavaScriptにおいておそらくprototypeチェーンとならぶ重要トピックである。しかし本書で解説されていることを要約すれば以下の点を理解していればそれでよい(後述の通り第4章には第6章〜8章の内容、とくに第7章のスコープとクロージャについての話題も統合されていてしかるべきである)。

  • 関数(Function()オブジェクト)を定義する方法は4つある(§ 4.1、4.2、4.13、4.20)。
    1. Functionコンストラクタ関数(new Function(argName1, argName2, ..., funcBody))を使用する方法。ふつう使わない。引数名も関数本体も文字列として渡す。本体はeval()で評価される。このため関数定義のエンクロージングスコープを参照できない。つまりクロージャとなり得ない(§ 4.1〜4.2)。
    2. function文( function( ... ){ ... } )で定義する方法。あるスコープでfunction文で定義された関数は、スコープの他のすべての手続きが行われる前に評価される(「巻き上げ」と呼ばれる現象)。つまりコード上より後方で宣言された関数を、より前方にある手続きで使用できる。なおあるスコープでfunction文で定義された関数は、スコープの外からは参照できない。function文はクロージャや高階関数のような概念を知らないひとが多く使う傾向にある。
    3. 匿名function式( function( ... ){ ... }; )で定義する方法。手続きを表す文ではなく、値を返す式として使用する構文。
    4. 名前付きfunction式( function functionName( ... ){ ... }; )で定義する方法。3つ目と異なるのは定義された関数に名前が付いており、自分自身を再帰呼出するときなどに使用できることである。この名前は仮にエンクロージングスコープで同じ名前が使用されていてもこれをシャドウ化する。
  • 関数を式で定義した場合には匿名であるかどうかに関わらず、()を使って即時実行できる( (funcDefinition)(arg1, arg2, ...); ) (§ 4.16)。
  • 関数の定義場所と実行時のnew演算子の有無によりthisをめぐって以下の6ケースがある。
    1. オブジェクトのプロパティに設定された関数で、new演算子なしに実行(メソッドとして実行)……thisはオブジェクトを指す。
    2. オブジェクトのプロパティに設定された関数で、new演算子ありで実行……関数はコンストラクタ関数として機能し、thisはObject.prototypeからプロパティを継承する新しいオブジェクトを指す。明示的にreturn式が実行されない限りこのthisが呼出元に返される。
    3. グローバルスコープで定義された関数で、new演算子なしに実行……thisはwindowを指す。
    4. グローバルスコープで定義された関数で、new演算子ありで実行……関数はコンストラクタ関数として機能。2つ目と同じ。
    5. ローカルスコープで定義された関数で、new演算子なしに実行……thisはwindowを指す。エンクロージングスコープを形成する関数がメソッドであり、そのthisがメソッドの所属するオブジェクトを指していようとも、そのスコープで定義された関数のthisはwindowを指す。
    6. ローカルスコープで定義された関数で、new演算子ありで実行……関数はコンストラクタ関数として機能。2つ目と同じ。
  • JavaScriptの関数はかならず値を返す。return式が実行されずに関数の処理が終了した場合はundefinedが返される(§ 4.5)。
  • JavaScriptの関数は高階関数である。つまり変数に格納したり、プロパティに設定したり、別の関数の引数として渡したり、別の関数の結果値として返すことができる(§ 4.6、4.19)。
  • JavaScriptの関数は宣言時に仮引数を指定できるが、指定した実引数がこの数より多くても少なくてもエラーとはならない。実引数が不足した場合は、余分な仮引数にはundefinedが設定されるだけである(§ 4.7)。
  • 関数のスコープ内ではargumentsという名前で、関数に渡された引数を格納した配列のようなオブジェクトにアクセスできる。
    • おどろくべきことにこの配列様のオブジェクトへの変更は仮引数に格納されている内容に即座に反映される(これは『開眼!JavaScript』が紹介しているJavaScriptの動作のうちで群を抜いて奇妙なものである。言ってみれば第4章はこの事項を紹介するためにあるのではないか?)(§ 4.8)。
    • 『開眼!JavaScript』ではでこのargumentsが「Function()オブジェクトのプロパティ」として紹介されているが(§ 4.4)、実際上はthis同様実行スコープ内で使用できる予約変数として考えた方がよい。argumentsは関数実行スコープ外から参照する場合──これはシングルスレッド実行されるというJavaScriptの性質上、「関数実行時以外に参照する場合」と言い換え可能である──、nullが設定されているからである。
    • argumentsオブジェクトにはcalleeというプロパティがあり、これは実行中の関数(Function()オブジェクト)を指すため、再帰実行のために使用可能だが、非推奨。再帰を行う場合は名前付きfunction式などで関数定義を行って、明確に名前参照するべき(§4.9 の訳注)。
  • 関数のメソッド(Function()オブジェクトのメソッド)であるapplyとcallはともに、任意のオブジェクトをthisに設定した状態で関数を実行する方法を提供する(§ 4.14)。

まとめ

さて、『開眼!JavaScript』でおよそ34ページの紙幅を費やして解説されていることを、私なりに要約(そして一部情報を追記)すると、以上のようになった。

第4章に関して言うと、本来であれば同書の第7章「スコープとクロージャ」で別途言及されている内容が、この章に統合されていてしかるべきだろう。関数の式による定義や匿名関数、thisやargumentsまで説明しておいて、スコープやクロージャに言及しないのには理解に苦しむところがある。

実際のところ第6章〜8章の3つはいずれもすでに第4章までで述べてきたことの繰り返しか、第4章でも触れることはできたのにそれを先送りにした四方山話である。これと同じ関係が第1章と第9章〜第15章の間にもいえる。

この記事では『開眼!JavaScript』について否定的な言及ばかりしてきたけれど、同書にはJavaScript言語の特徴について多くの有益な情報や示唆が含まれている。そして本書がサイ本とは比較にならないほどコンパクトなものであることも事実である。問題はその解説とサンプルコードの示し方に見られる冗長さである(単にある章の解説が冗長であるというのにとどまらず、ほとんどすべてが既述の内容の繰り返しであるような章を後方に置くというレベルでも冗長である)。

したがって、私としてはこの本を初学の方にはおすすめしない。サイ本をおすすめする。しかしそうではあっても、次のような人にはやはり『開眼!JavaScript』は有益である。つまり、JavaScriptの仕様については理解したいけれど、サイ本のような分厚い本は読みたくない、『開眼!JavaScript』のような厚みの本であればガマンできる、その内容が本来4分の1くらい(あるいはそれ以下)の厚みで済むにしても。そういう人には。

JavaScript 第6版

JavaScript 第6版