HTML 要素にクラスを付与したり削除したりするのに便利なユーティリティメソッド (JavaScript)
HTML5 における HTMLElement#classList
HTML5 では、HTML DOM の HTMLElement インターフェイスに classList というプロパティ が追加される予定です。 このプロパティは W3C DOM 4 の DOMTokenList インターフェイス を実装したオブジェクトを参照しており、このリストに値を追加したり、このリストから値を削除すると、要素のクラス属性に反映されます。
つまり、HTMLElement#classList を使うことで、要素にクラスを追加したり削除したりすることが簡単にできます。 詳細は MDN あたりが分かりやすいと思います。
しかしながら、現在のところ HTMLElement#classList が使える環境は多くはありません。 Firefox 7 や Opera 11.51 では使えることを確認しました (Google Chrome でも使えるようです) が、Safari 5 や IE 10 では実装されていません。
書評: 『ハイパフォーマンス JavaScript』 Nicholas C. Zakas 著, 水野 貴明 訳
- 作者: Nicholas C. Zakas,水野貴明
- 出版社/メーカー: オライリージャパン
- 発売日: 2011/03/20
- メディア: 大型本
- 購入: 9人 クリック: 1,176回
- この商品を含むブログ (33件) を見る
『ハイパフォーマンス JavaScript』 を読み終わりました。 簡単に内容についてと感想を書いておきます。
続きを読むJavaScript の正規表現マッチング
JavaScript で正規表現 (RegExp オブジェクト) を使ってマッチングする方法。 しょっちゅう忘れてしまううえに ECMAScript 仕様に適合した方法があまり web 上に書かれていないようなので軽くメモしておきます。
RegExp.prototype.exec( string )
メソッド
正規表現のマッチングを試して、マッチした文字列全体とキャプチャリングした文字列を取得するためのメソッドが RegExp.prototype.exec( string )
メソッドです。
このメソッドは、マッチングが成功した場合は Array
オブジェクトを返します。 その Array
オブジェクトの第 0 要素はマッチした部分文字列全体です。 正規表現にキャプチャリングが含まれていた場合は、Array
オブジェクトの第 1 要素以降に、順番にキャプチャリングされた文字列が格納されます。 さらに、この Array
オブジェクトには index
プロパティと input
プロパティが定義され、それぞれの値は、正規表現が文字列にマッチした開始位置とマッチングの対象になった文字列全体です。
また、RegExp
オブジェクトの global フラグが true
であるならば、マッチした部分の最後の文字の次の文字の位置が、RegExp
オブジェクトの lastIndex
プロパティに保持されます。 同じ RegExp
オブジェクトを使って再び exec
メソッドを実行すると、そのときのマッチングは lastIndex
プロパティの値から再開されます。 よって、while
ループでマッチしなくなるまで繰り返す、ということが可能です。
なお、マッチしなかった場合には exec
メソッドは null
を返します。
例
// global フラグを true にした RegExp オブジェクト var re = /<(\d+),(\d+)>/g; var m; // マッチする部分がある限り繰り返す while( m = ( re.exec( "ab<32,43>cddd<faa>da<342,2>bcaaeabc" ) ) ) { // マッチした部分の開始位置 print( m.index ); // マッチした部分文字列全体 (Ruby などでは $& で参照できる) print( m.shift() ); // (第 0 要素だけを取り出している) // キャプチャされた部分 (Ruby などでは $1, $2, ... で参照できる) print( m.join( ", " ) ); // (元々の第 0 要素は既に取り出したので, 残りはキャプチャされた部分のみ) }
JavaScript でも、バージョンによっては RegExp['$&']
としてマッチした部分文字列全体を参照したり RegExp.$1
, RegExp.$2
, ... としてキャプチャされた部分を参照できたりしましたが、非推奨となっており、使わないようにした方がいいでしょう。 (ちなみに RegExp.$&
は基本的に構文エラーになってしまうので、RegExp['$&']
と書く必要がある。)
参考文献
- ECMAScript - Documentation (ECMAScript の仕様書)
- RegExp - MDN Docs
- 取説 正規表現 (nanto_vi さん)
ECMAScript 5 の strict mode でグローバルオブジェクトを得るひとつの方法
ECMAScript 5th edition (JavaScript 系統のコア部分をまとめた言語みたいなもの) には strict mode というものがあり、strict mode では関数コード (Function Code) 内の this
キーワードの参照先オブジェクトが、strict mode でない場合と異なる可能性があります。 例えば、関数式を即実行する Immediate Function Pattern (((function () { /* ... */ })();
という形式のもの。 特に正式名称があるわけではないと思いますが、『JavaScriptパターン ―優れたアプリケーションのための作法』 でこの名前が使われていたのでこの名前で呼んでいます)) と呼ばれる手法を使う際、strict mode でなければ関数コード内で this
キーワードがグローバルオブジェクトを参照しますが、strict mode では未定義値 (undefined
) になります。
詳しくは下記参照。
その対策として、id:think49 さんのところでは以下のように new Function()
を利用する方法が紹介されています。
関数コードでは
new Function()
を利用します。"use strict"; (function () { console.log(new Function('return this')()); // [object Window] console.log(Function('return this')()); // [object Window] })();
グローバルコード (Global Code) からグローバルオブジェクトを渡す方法
別の方法としては、Immediate Function の引数としてグローバルオブジェクトを渡してやる方法もあります。 グローバルコードでは this
キーワードはグローバルオブジェクトを参照するため、グローバルコードにおける Immediate Function に対しては、引数としてグローバルオブジェクトを渡してやることができます。
"use strict"; (function ( globalObj ) { // 引数 globalObj としてグローバルオブジェクトへの参照を受け取る print( globalObj ); // [object Window] // strict mode: Immediate Function 内の this キーワードは undefined print( this ); // undefined })( this ); // グローバルコードにおける this はグローバルオブジェクト
どこででも使えるという意味では new Function()
を使う手法の方が便利ではあるのかなーと思いますが。
new を不当に貶める陰謀と JavaScript におけるクラスの継承構造の話
私は陰謀論者じゃないですし JavaScript の new
演算子が大好きなわけでも大嫌いなわけでもないです。 念のため。 本記事は Hiraku さんが書かれた下記記事への言及です。
new
演算子は使うな!?
「new
を封印するべき4つの理由」 でも new
がいかに糞であるかが書かれていますし、その記事からも言及があるように Crockford さんが書かれた書籍 『JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス』 でも new
演算子は Bad Parts に分類されています。
new
演算子が忌避される理由はいろいろあるみたいですが、Hiraku さんの記事では
new
を書き忘れてもコンストラクタ関数が実行されて意図しない動作になる- 継承を考慮する必要がある
- 何をやっているのかわかりにくい
- 読みづらい
という点が指摘されています。
常に new
演算子を使うことが最良の選択だとは思っていませんが、ことさら new
演算子を貶める言説も好きじゃないのでちょっと反論しておこうかな、というのがこの記事の主題です。
各継承方法のオブジェクトの関係図
はじめに、議論の土台として、純粋なプロトタイプ継承と、クラスの継承構造を持ち込んだ場合の継承方法について、オブジェクトの関係図を示します。
純粋なプロトタイプ継承
まずは JavaScript の純粋なプロトタイプ継承の形を見てみましょう。
ECMA-262 (5th edition) を満たしている JavaScript 処理系なら、Object.create
メソッドで簡単にプロトタイプ継承が可能です。 Hiraku さんの記事で定義されている object
関数は、この Object.create
メソッドと同じようなものです。
var baseObj = { name: "ベースオブジェクト", printName: function(){ print( this.name ) } }; // Object.create メソッドでプロトタイプ継承する var subObj = Object.create( baseObj, { name: { value: "サブオブジェクト" } } ); // 使ってみる subObj.printName() // サブオブジェクト
このような純粋なプロトタイプ継承による継承構造を図で表すと次のようになります。 非常に単純ですね。
new
演算子とクラスの継承構造
次に、プロトタイプ継承と new
演算子によるオブジェクト生成の機構を利用して、クラスの継承構造を JavaScript で実現してみましょう。 詳しい実現方法は 「JavaScript におけるクラスの作成と継承」 の記事で述べましたので、ここでは詳細には立ち入りません。 基本的な思想としては、インスタンス (に相当するオブジェクト) で全てのインスタンス変数を保持し、プロトタイプ継承の先祖のオブジェクトでインスタンスメソッドを定義する、という形式です。 コンストラクタ関数オブジェクトがクラスそのものを表しています。
少しややこしいですが、インスタンス生成には new
演算子、クラスの継承構造の生成には関数定義、という風に明確に分離しています。 また、クラスを表現するオブジェクト (コンストラクタ) とインスタンスメソッドを定義するオブジェクトが分かれているため、コンストラクタのプロパティによってクラスメソッドやクラス変数を実現できます。
Hiraku さんの記事におけるクラスの継承構造
最後に、Hiraku さんの記事に書かれている方法でクラスの継承構造を実現した場合のオブジェクトの関係を図に示します。
JavaScript における純粋なプロトタイプ継承の継承構造に、無理やりクラスとインスタンスという概念を持ち込んでいるように私は思います。 以下のような問題点があるように思います。
- クラスとインスタンスの区別が明確でない : クラスの継承もインスタンスの生成も完全に同じ機構で実現している。 同じプロトタイプ継承によって実現しているとしても、クラスとインスタンスという概念を持ち込むのであれば、区別するようにしたほうが良い
- クラスメソッドとインスタンスメソッドのを区別が明確でない : クラスを表すオブジェクトとインスタンスメソッドを定義するオブジェクトが同一なので、クラスメソッドとインスタンスメソッドを区別できない
- 明示的なコンストラクタ呼び出しが必要 :
new
演算子によるオブジェクト生成の場合は暗黙的にコンストラクタ関数が呼び出されましたが、この方法では明示的にコンストラクタの呼び出しが必要です
4 つの new
批判に対する反論
それでは 「new
を封印するべき4つの理由」 に対して反論してみます。
new
を書き忘れてもコンストラクタが実行されてしまう問題
var Person = function Person( name ) { this.name = name; }; Person.prototypte.sayHello = function sayHello() { print( "Hello, I'm " + this.name + "." ); }; // new を忘れて実行 var tarou = Person( tarou );
確かに、new
を書き忘れてもコンストラクタ関数が実行されてしまいます。 それは認めましょう。 ですが、これの何が問題なんでしょうか? new
を書き忘れることってありますか? 書き忘れたとして、それが発見困難なバグに繋がりますか?
私の感覚では、new
の書き忘れは引数の個数不足での関数呼び出しと同じレベルの問題であると思います。 引数の個数や型チェックが必要なら関数内でチェックをするように、チェックする必要があるならコンストラクタ関数内で this
が何を指しているのかチェックすればいいだけです。
new
の書き忘れで時間を浪費するようなプログラマが居るならば、その人はプログラマに向いていないので転職を勧めてあげましょう。
継承を考慮する必要があるという問題
『この書き方は、object関数をインライン展開したのと同義です…。それならいっそのこと、常にobject関数を使うようにした方が楽だと思いませんか?』 とありますが、クラスの継承を行う関数を定義すればいいだけのことです。 例えば 「JavaScript におけるクラスの作成と継承」 の記事では、クラスの継承のための extend
という関数を定義しました。
何をやっているのかわかりにくいという問題
『JavaScript自体はプロトタイプベースで作られているくせに、newを使うとクラスベースみたいな書き方をする必要があります。』 って、new
メソッドはプロトタイプ継承を応用してクラスの継承構造を JavaScript に持ち込むためのものなので当然でしょう。 クラスの継承構造を持ち込むのであれば、インスタンス化に new
演算子を使うことは他の言語との類似性から言ってもわかりやすい と思います。 もちろんクラスの継承構造を使わないのに new
演算子を使うことは混乱の元になると思いますが。
読みづらいという問題
個人的にはオブジェクトリテラルの中に関数式を書くのは好きじゃないのでどっちかというと
var Animal = { name: "動物" , breathe: function(){alert("すーはー")} , sayName: function(){alert(this.name)} };
よりも
var Animal = function Animal( name ) { this.name = name; }; Animal.prototype.breath = function breath() { print( "すーはー" ); }; Animal.prototypte.sayName = function sayName() { print( this.name ); };
の書き方が好きなんですが。 まあでも多くの人は前者の書き方の方が好みなんだろうなー、とは思います。
ていうかこの name
ってなんなんでしょう。。 クラス変数?
そもそも new
演算子のどの部分を批難しているのか?
というわけで 4 つの問題点に対する反論 (になってるかどうかは微妙ですが) を書いてきましたが、そもそも new
を封印する目的がいまいちよくわかりません。
- 本当に、ただ純粋に
new
演算子を使わないようにすべき、という主張? - JavaScript にクラスの継承構造を持ち込むな、という主張?
本当に、ただ純粋に new
演算子を使わないようにすべき、という主張は JavaScript に限らず Java などでもあります。 new
演算子によるインスタンス化は柔軟性に欠ける、というものです。 インスタンス化のための API としてユーザーにはファクトリーメソッドを提供し、new
演算子は表から見えないようにするべきであるという主張であり、『Effective Java 第2版 (The Java Series)』 などに書かれています。
しかし、今回の場合は単純に new
演算子を表から隠すだけでなく継承構造も変えていますし、単純に new
演算子を表から隠せ、という主張ではないように思います。
それでは、JavaScript にクラスの継承構造を持ち込むな、という主張なのかというとそれも違います。 Hiraku さんの記事では 「クラス(に相当するオブジェクト)を作る」 という表現が出てきていますし、クラスの継承構造を実現したいように思えます。
「クラスの継承構造が不要な場面ではわざわざクラスの継承構造なんか作らずに、JavaScript の基本的な継承であるプロトタイプ継承を使いましょう!」 という主張なら納得するのですが、Hiraku さんの記事では結局のところ何が目的なのかよくわからなくなっており、「俺俺オブジェクト指向な気がする」 と言われても仕方ないかなー、と思います。
new
演算子を使うとわかりやすい? わかりにくい? (追記)
ちょっと重要なことなので追記。
本記事の上のほうでも 「クラスの継承構造を持ち込むのであれば、インスタンス化に new
演算子を使うことは他の言語との類似性から言ってもわかりやすい」 と述べましたし、id:teramako さんも 「僕はnew演算子は好きです。書き忘れが発生してorzすることはあるけど。何故好きかというと、コードを読んでいてインスタンスを作成していることが明確に分かるから」 と仰っています。 一方で、Hiraku さんは 「JavaScript自体はプロトタイプベースで作られているくせに、newを使うとクラスベースみたいな書き方をする必要があります。中でやっていることと、外側から見えるインターフェースが違いすぎる」 という立場です。
もちろん、どちらが間違っている、ということはありません。 とはいえどちらかの立場をとらなければいけないのであれば、「誰にとってわかりやすくすべきか」 という視点を取り入れるべきでしょう。
クラスという概念を取り入れる以上、クラスの設計者とクラスのユーザーという 2 人の視点で考える必要があります。 これはしばしば同一人物でありますが、同一人物ではない可能性もあります。 そして、クラスというものは クラスのユーザーにとってわかりやすい ものであるべきだと思います。 クラスの設計・実装者は当然 JavaScript に詳しいはずですので、プロトタイプ継承がどうとか、そういう点は問題なく認識しているはずです。 一方、クラスの使用者はクラスを提供されているだけなので、「クラスに見えるけどプロトタイプ継承で実装されている」 という点は認識していないかもしれません。 また、そのように認識させる必要もありません。 なぜなら 「クラス」 を提供しているのですからクラスのように扱えればいいのです。
例えば、Person
というクラスを提供するとしましょう。 このクラスは人物を表すもので、人物の名前を引数にとってインスタンス化するものとしましょう。 こう聞くと、クラスのユーザーはほぼ間違いなく以下のようなコードでインスタンス化できるものと期待するはずです。
var tarou = new Person( "田中 太郎" );
「クラスをプロトタイプ継承したオブジェクトを作って、自分で初期化関数を呼んでください」 などと言われるとどうでしょう?
var tarou = Object.create( Person ); tarou.init( "田中 太郎" );
という感じでしょうか。 お世辞にも良いインターフェイスだとは言えません。 ではオブジェクトの生成と初期化を行う関数を Person オブジェクトに追加してみますか?
// ファクトリーメソッド Person.newInstance = function newInstance( name ) { Object.create( this ); this.init( name ); }; // インスタンス化 var tarou = Person.newInstance( "田中 太郎" );
すっきりしました。 これなら良いでしょう。 しかしこうなるともはや従来の手法でいい気がします。 クラスの実装方法に違いはありますが、上でも言ったようにクラスの設計・実装者には十分な知識があるはずなのでどちらの実装方法でもあまり問題にはなりません。 それよりも、ユーザーが標準的な使用方法でクラスを使用できるかどうか、が重要なのです。
標準的な使用方法で使えるのかどうかという点において、インスタンス化についてよりももっと大きな問題があります。 instanceof
演算子を実直に使えないという問題です。 (そもそも instanceof
演算子は使うべきではない、という主張もありますが、それはさておき。) ユーザーは、Person
クラスをインスタンス化した tarou
は、以下の式で true
になると期待するでしょう。
tarou instanceof Person; //=> true
しかし、Hiraku さんとこの方法では
tarou instanceof type( Person ); //=> true
などとしなければいけません。 大規模なフレームワークとしてクラス機構を提供するのであれば新しい書き方を導入するのもひとつの手ではありますが、単一、または少数個のクラスを提供する際にこのような書き方をクラスのユーザーに強要するのは無理というものです。 様々なクラスの提供者がそれぞれ独自に type
関数のような関数を提供したらどうでしょう? 使いにくいですよね。 これが 「俺俺オブジェクト指向」 と呼ばれてしまったひとつの理由でしょう。
プロトタイプ継承という機構を隠してしまったことの弊害
しかしながら、JavaScript が 「クラスの継承構造のような機構」 を提供し、プロトタイプ継承という機構を隠してしまったがための弊害もあります。 Hiraku さんが仰るように 「実際の挙動 (プロトタイプ継承) と見た目 (クラスの継承やインスタンス化) が異なっている」 というものです。
JavaScript で new Date()
のようにインスタンス化を行ったことがある人はたくさんいると思いますが、プロトタイプ継承という継承方法を知っている人は思っている以上に少ないのではないか、と思います。 プロトタイプ継承という機構が裏に隠れてしまっているために、(誰かが作ったクラスを使うだけなら) プロトタイプ継承について勉強する必要が無いためです。 特に JavaScript はプログラマだけでなく、web デザイナーなど本職がプログラマじゃない人にも使われていますので、ちゃんと勉強してない人も多いはずです。
そのような 「クラスを使ったことはあるけどプロトタイプ継承はよく知らない」 というような人がクラスを設計・実装しようとするとよくわからなくなって混乱してしまいます *1。 そして、「JavaScript のクラスってよくわからない」 ということになってしまうのではないかと思います。
この点はどうしようもないかなーという気がするのですが、とりあえず言えることは 「他の言語と同じようにクラスを使えるからといってろくに勉強せずクラスの設計・実装をしようとすると痛い目を見るので、まず JavaScript の勉強をしましょう」 ということですね。
私の主張
最後に JavaScript におけるオブジェクトに関して私の主張をちょこっとしておきます。
new
演算子がしばしば攻撃の対象になるのは、他の言語との類推から JavaScript においてもなんでもかんでも new
演算子を使ってオブジェクト生成するような人がいるからだと思います。 他の言語、例えば Java などではオブジェクトを生成するために基本的 *2 に new
を使います。 一方、JavaScript では new
演算子が必要となる場面はそれほど多くありません。
JavaScript は弱い動的型付け言語であり、使い捨てのオブジェクトをオブジェクトリテラルで生成することができます。 なんらかのオブジェクトを継承したい場合、クラスの継承などしなくてもプロトタイプ継承によりオブジェクトそのものを継承することができます。 特に web サイト上で動く JavaScript は使い捨てのオブジェクトを使うことが多く、クラスの継承構造が必要になることはあまり多くないでしょう。
一方で、クラスの継承構造を利用すると便利な場面もあります。 同じメソッドを持ち、異なるデータを保持する多くのオブジェクトを使いたい場面では、クラスの継承構造が役に立ちます。 そういうところでは、クラスの継承構造をつくり、new
演算子 ((new
じゃなくてファクトリーメソッドなどを使うべき、という議論もありますが、それはまた別のお話ということで。)) を使ってインスタンス化する、ということをすれば良いでしょう。
使いどころを考えて、使うべきところで使うべき方法をとるというのが重要です。
ECMAScript のレキシカル環境 〜catch 節のスコープのお話〜
Twitter で ECMAScript (JavaScript) の catch
節のスコープについての話をみかけた ので、ちょっと調べてみた。
catch( err )
の err
のスコープは?
例外処理の機構で使用される catch( err )
ですが、この err
はどの範囲で有効なのか? 普通に考えると catch
節内だけで有効な気がするけど、ECMAScript では一般に関数ごとにスコープを持つと言うし、関数全体? まさかグローバル?
(function test() { try { throw 0; } catch( err ) { print( err ); // 0 } print( err ); // [ERROR] err is not defined })();
試したところ、どうやら catch
節内だけのスコープがある模様。
ECMA-262 (5th edition) の 10.2 節 (Lexical Environments) のところには以下のように書かれていて、catch
節にもレキシカル環境 (Lexical Environment) が結び付けられていることがわかります。
Usually a Lexical Environment is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a WithStatement, or a Catch clause of a TryStatement and a new Lexical Environment is created each time such code is evaluated.
catch
節内の VariableDeclaration は?
じゃあ catch
節内の VariableDeclaration ((var variable_name;
という形式の変数宣言)) は catch
節の外の環境には影響を与えないのか?
(function test2() { try { throw 1; } catch( err ) { var n = err; } print( n ); // 1 })();
試してみると、catch
節内の var
で宣言された変数は、catch
節の外でも有効でした。 なぜ?
レキシカル環境と実行コンテキスト
ECMA-262 (5th edition) の 10.3 節 (Execution Context) を見ると、次のように書いてあります。
When control is transferred to ECMAScript executable code, control is entering an execution context. Active execution contexts logically form a stack. The top execution context on this logical stack is the running execution context. A new execution context is created whenever control is transferred from the executable code associated with the currently running execution context to executable code that is not associated with that execution context. The newly created execution context is pushed onto the stack and becomes the running execution context.
An execution context contains whatever state is necessary to track the execution progress of its associated code. In addition, each execution context has the state components listed in Table 19.
The LexicalEnvironment and VariableEnvironment components of an execution context are always Lexical Environments. When an execution context is created its LexicalEnvironment and VariableEnvironment components initially have the same value. The value of the VariableEnvironment component never changes while the value of the LexicalEnvironment component may change during execution of code within an execution context.
Table 19 の内容は次のとおり。
Component | Purpose |
---|---|
LexicalEnvironment | Identifies the Lexical Environment used to resolve identifier references made by code within this execution context. |
VariableEnvironment | Identifies the Lexical Environment whose environment record holds bindings created by VariableStatements and FunctionDeclarations within this execution context. |
ThisBinding | The value associated with the this keyword within ECMAScript code associated with this execution context. |
適当にまとめると、『ECMAScript を実行するための実行コンテキストは LexicalEnvironment と VariableEnvironment と ThisBinding から成る。 LexicalEnvironment と VariableEnvironment は常にレキシカル環境である。 実行コンテキストが生成されるとき、最初は LexicalEnvironment と VariableEnvironment は同じ値を持つ。 VariableEnvironment の値は決して変更されないが、LexicalEnvironment の値は変更される可能性がある』 という感じでしょうか。 つまり、実行コンテキストにおけるレキシカル環境として、変更される可能性がある LexicalEnvironment と変更されない VariableEnvironment の 2 種類のレキシカル環境がある、というわけです。
実行コンテキストは関数へ入るときに生成され、catch
節に入るときには LexicalEnvironment が変更される
さて、実行コンテキストには LexicalEnvironment と VariableEnvironment の 2 種類のレキシカル環境があることがわかりましたが、それぞれどのように使われるのでしょうか。
実行コンテキストが生成されるのはいつかというと、ECMA-262 の 10.4 節 (Establishing an Execution Context) に書かれています。 適当に意訳すると 『グローバルコードや eval
関数で使われているコードを評価する際、および関数を呼び出す際に新しい実行コンテキストが作られ、そこに入る。 実行コンテキストに入る際、ThisBinding がセットされ、VariableEnvironment と初期の LexicalEnvironment が定義される。 また、宣言束縛の具現化 (10.5 節) が実行される。』 と書かれています。 宣言束縛の具現化 (Declaration Binding Instantiation) というのが、関数宣言 (FunctionDeclaration) や変数宣言 (VariableDeclaration) を環境に結びつけることを意味しています。
つまり、VariableDeclaration が環境に結び付けられるのは実行コンテキストが生成されるときであり、実行コンテキストが生成されるのは関数を呼び出すときである、ということです。 すなわち、catch
節の中に VariableDeclaration があったとしても、それが環境に結び付けられるのはその catch
節を含む関数 (またはグローバルコードか eval
コード) が呼び出されるときであって、catch
節に入るときはない、というわけです。
では catch
節に入るときはどのようなことが行われているのでしょうか? これは、12.14 節 (The try
Statement) に書かれています。
The production Catch : catch ( Identifier ) Block is evaluated as follows:
1. Let C be the parameter that has been passed to this production.
2. Let oldEnv be the running execution context’s LexicalEnvironment.
3. Let catchEnv be the result of calling NewDeclarativeEnvironment passing oldEnv as the argument.
4. Call the CreateMutableBinding concrete method of catchEnv passing the Identifier String value as the argument.
5. Call the SetMutableBinding concrete method of catchEnv passing the Identifier, C, and false as arguments. Note that the last argument is immaterial in this situation.
6. Set the running execution context’s LexicalEnvironment to catchEnv.
7. Let B be the result of evaluating Block.
8. Set the running execution context’s LexicalEnvironment to oldEnv.
9. Return B.
NOTE No matter how control leaves the Block the LexicalEnvironment is always restored to its former state.
いくつか処理を行っていますが、要は実行コンテキストの LexicalEnvironment を変更しているわけです。 その新しい LexicalEnvironment は、古い LexicalEnvironment を親環境とし、catch
節にパラメータとして渡された値を唯一の変数束縛として持っています。
識別子の名前解決に使われるのは LexicalEnvironment (10.3.1 節 Identifier Resolution 参照) なので、catch
節に渡されたパラメータは catch
節の中だけのスコープを持つわけです。 ちなみに with
文も catch
節と同じで、実行時に LexicalEnvironment を変更します。
まとめ
- グローバルコードと
eval
コードの評価時、および関数の呼び出し時に新たな実行コンテキストを生成し、そこに入る - 実行コンテキストは ThisBinding と LexicalEnvironment と VariableEnvironment から成る
- 識別子の名前解決に使われるのは LexicalEnvironment
- 実行コンテキストの生成時に、関数宣言と変数宣言は環境に結び付けられる (このとき LexicalEnvironment と VariableEnvironment は同じ値)
with
文とcatch
節は LexicalEnvironment を変更する
JavaScript におけるクラスベースの継承方法色々
JavaScript Patterns: Build Better Applications with Coding and Design Patterns
- 作者: Stoyan Stefanov
- 出版社/メーカー: O'Reilly Media
- 発売日: 2010/10/01
- メディア: ペーパーバック
- 購入: 2人 クリック: 79回
- この商品を含むブログ (5件) を見る
先日、JavaScript におけるクラスベースの継承方法に関して @think49 さんと色々議論してたんですが、クラスベースの継承方法に関して 『JavaScript Patterns』 (和訳版 『JavaScript パターン』) の中にパターン化してまとめられていましたのでここで紹介しておきます。
JavaScript におけるクラスベースの継承って?
JavaScript にはクラスというものがありませんが、コンストラクタ関数を作り、new
演算子を使ってインスタンスを作るということが可能です。
var Constructor = function Constructor() {}; // コンストラクタ var c = new Constructor(); // インスタンス化
インスタンス化の記法が他のクラスベースの言語 (特に Java など) のものに近いため、コンストラクタ (prototype
プロパティなどを含む) を擬似的なクラスだとみなすことがあります。 本記事では、継承を用いてこの擬似的なクラスのコードを再利用する方法をクラスベースの継承という *1 ことにし、それについて紹介します。 簡単な紹介に留めますので、詳細は 『JavaScript Patterns』 の 6 章 (Code Reuse Patterns) をご覧ください。 以降、"Classical Pattern" という言葉が出てきますが、これは書籍中の言葉をそのまま使っています。
なお、書籍では 「再利用」 を目的としていますので必ずしも 「継承」 と言えないパターンもありますが、「再利用」 というのも締りが悪いので本記事では 「継承」 という言葉を使わせてもらいます。
前提
親となるコンストラクタ Parent
と、それを再利用する Child
があるとします。 特に断りが無い限りは、このコードを前提に話を進めていきます。
var Parent = function Parent( name ) { this.name = name || "Adam"; }; Parent.prototype.getGreetingMessage = function getGreetingMessage() { return "Hello, I'm " + this.name + "!"; }; var Child = function Child() {};
以降、5 つのパターンを紹介します。
Classical Pattern #1 - The Default Pattern
クラスベースの継承の最も一般的なパターンです (多分)。 単純に子となるコンストラクタの prototype
プロパティに、親のインスタンスを代入するというものです。
// Child のインスタンスが Parent のインスタンスをプロトタイプ継承する Child.prototype = new Parent(); // 必要に応じてメソッドの追加などを行う Child.prototype.newMethod = function newMethod() { /* ... */ };
単純なので JavaScript 関係の書籍でもしばしば目にすることがあり、一般的に使われている手法だといえます。 しかし、この方法ではクラスを継承しているのではなく、親クラスのあるインスタンスを継承していることになります。 上の例では、本来インスタンスごとに持っているべきだと思われる name
プロパティが、Child
インスタンスごとではなく Child
インスタンスの単一のプロトタイプオブジェクトにのみ存在しているという点で問題があります。 (もちろんそれが問題でないなら良いのですが。)
Classical Pattern #2 - Rent-a-Constructor
次に紹介するのは、子のコンストラクタ内から親のコンストラクタを呼び出す、というものです。
// 前提となる Child コンストラクタをこれに置き換える var Child = function Child( name ) { // 親のコンストラクタを呼び出す Parent.apply( this, arguments ); };
このパターンは、親のコンストラクタを再利用する、というものです。 親と子の間に直接的なプロトタイプ継承の関係はありません。
Classical Pattern #1 の欠点が解消され、親クラスで定義されているプロパティが、ちゃんと子のインスタンスごとに持たされるようになっています。 その一方で、親の prototype
プロパティで定義されているメソッドは継承されない、という欠点もあります。
Classical Pattern #3 - Rent and Set Prototype
上の 2 つのパターンをあわせたものがこのパターンです。
// 前提となる Child コンストラクタをこれに置き換える var Child = function Child( name ) { // 親のコンストラクタを呼び出す Parent.apply( this, arguments ); }; // prototype プロパティに親のインスタンスの設定する Child.prototype = new Parent(); // 必要に応じてメソッドの追加などを行う Child.prototype.newMethod = function newMethod() { /* ... */ };
これまでの 2 つのパターンの短所を解消できています。 ただ、親のコンストラクタ内で定義される name
プロパティが、子のインスタンスのプロトタイプオブジェクトにも存在し、また子のインスタンス自身のプロパティとしても存在する、(すなわち、name
プロパティが 2 回継承されている) というのが気になるところではあります。
Classical Pattern #4 - Share the Prototype
これは 「継承」 とは少し違いますが、プロトタイプオブジェクトを共有するというものです。
Child.prototype = Parent.prototype
このようなコードの再利用方法が有効な場面もあるかもしれませんが、基本的には使う必要のないパターンでしょう。
Classical Pattern #5 - A Temporary Constructor
最後は、#3 のパターンをさらに改良したものです。
// #3 のパターンにおける // Child.prototype = new Parent(); // の代わりに以下の処理を行う (function () { // ローカルスコープ生成 var F = function F() {}; // 一時的なコンストラクタ F.prototype = Parent.prototype; Child.prototype = new F(); })();
#3 のパターンでは Parent.prototype
を継承するために Child.prototype = new Parent()
としていましたが、この方法だと Parent
コンストラクタを実行してしまいます。 Parent
コンストラクタを実行せずに Parent.prototype
を継承するために、一時的なコンストラクタ F
を作って対処するというのがこのパターンです。
さらに改良
他のパターンではプロトタイプオブジェクトの constructor
プロパティについてはあまり考えてきませんでしたが、実際にはそういった点も考えたほうがいいでしょう。 ついでに親となるコンストラクタを子のコンストラクタのプロパティとして持たせておけばいいかもしれません。
(function () { // ローカルスコープ生成 var F = function F() {}; // 一時的なコンストラクタ F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; // constructor プロパティの設定 Child.baseConstructor = Parent; // 親コンストラクタを表すプロパティの設定 })();
まとめ
色々なパターンを紹介しましたが、クラスベースの継承という点では #5 の方法が最良だと思います。
ただし、JavaScript でクラスベースの継承を使う必要がある場面というのはそうそうないと思います。 他の言語に近いからという理由で安易にクラスベースっぽい書き方をするのではなく、本当に必要な場合にのみ使うようにしましょう。
参考文献
本記事の内容は、次の書籍の内容を紹介するものです。 詳細は書籍をご覧ください。
- JavaScript Patterns, Stoyan Stefanov 著
- 和訳版: JavaScript パターン, Stoyan Stefanov 著, 豊福 剛 訳
*1:書籍では "Classical Inheritance" と言っています。