主にJScript(Internet Explorer 5)を使っていて知ったことの覚え書きです。 間違ったことを書いていたらゴメンよー。
Perlにはmapやgrepがあって大変便利。Arrayオブジェクトでちょっと試してみる。
Array.prototype.map = function (func) { var x, ary = new Array(), len = this.length; if (typeof func == "function") { for (var i = 0; i < len; i++) { ary[i] = func(this[i]); } } else { for (var i = 0; i < len; i++) { x = this[i]; ary[i] = eval(func); } } return ary; }; Array.prototype.grep = function (func) { var x, ary = new Array(), len = this.length; if (typeof func == "function") { for (var i = 0; i < len; i++) { func(this[i]) && ary.push(this[i]); } } else if (typeof func == "object" && func.constructor == RegExp) { for (var i = 0; i < len; i++) { func.test(this[i]) && ary.push(this[i]); } } else { for (var i = 0; i < len; i++) { x = this[i]; eval(func) && ary.push(x); } } return ary; };
実行してみると次のようになる。 ただし、Perlのmap、grepにおける変数 $_ に相当するものは変数 x にしてみた。
[0, 1, 2, 3, 4].map("x + 1");
// 要素にそれぞれ1を加える。
// → [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5].map(Math.sqrt);
// 要素の平方根を求める。
// → [1, 1.4142135623730951, 1.7320508075688772, 2, 2.23606797749979]
["Win3.1", "WinNT", "Win95", "Win98", "Win2000"].grep(/^Win9./);
// 要素から正規表現に合致するものを選ぶ。
// → ["Win95", "Win98"]
var men = [
{ name:"John", age:20 },
{ name:"William", age:22 },
{ name:"James", age:19 },
{ name:"Robert", age:28 },
{ name:"David", age:25 }
];
men.grep("x.age >= 25").map("x.name");
// オブジェクト配列から条件に合致するものを選ぶ。
// → ["Robert", "David"]
[1, 2, 3, 4, 5, 6].grep(function () { var idx = 1; return function () { return idx++ & 1 == 1 }}());
// 奇数番目の要素を抜き出す。
// → [1, 3, 5]
また、累積和を得るメソッドは次のように書ける。
Array.prototype.cumsum = function () {
var sum = 0;
return this.map(function (x) { return sum += x });
};
実行してみると次のようになる。
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].cumsum();
// → [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
たとえば、あるオブジェクトのメソッドを再帰で実行したいとする。 しかも直接再帰するのではなく、setTimeoutを挟んで呼び出したい。 何も考えずに書いてみると次のような感じになるだろう。
function MyObject() { ...; } function MyObject_myMethod() { ...; setTimeout("this.myMethod()", 100); ...; } MyObject.prototype.myMethod = MyObject_myMethod;
が、言うまでもなくこれではちゃんと動かない。 setTimeout()でスクリプトが実行されるときにはthisに何が入っているか予想できないからだ。 thisが運良く期待したオブジェクトを指しているかもしれないが、 たとえそうなったとしてもそれはタマタマだ。
で、どうするかだが、 これを解決するにはthisが指しているオブジェクトをどこかに保存しておかなければならない。 そして保存しておいたオブジェクトに対してメソッドを実行すればとりあえず動作するはずである。 たとえば、グローバル変数に値を保存しておけば良いわけだが、 それだと複数のオブジェクトが同時にこういう動作をするときどうするかというような問題もあるので、 ここでは別の方法としてクロージャを使った方法を提案したい(クロージャをよく知らない人はいろいろ調べてください)。
結論から言うと上の例は次のように書きなおせばよい (setTimeout()がfunctionを引き数に取れるのはJavaScript1.2以降であるので注意してください)。
setTimeout(function (obj) { return function () { obj.myMethod() } }(this), 100);
ちょっとややこしいが、引き数のうち、外側のfunctionはクロージャを作って返す関数であり、 直後に(this)がついているので、setTimeout()が評価される前にthisを引き数として実行される。 そうして作られたクロージャがsetTimeout()に渡される。 そして、内側のfunctionはそのクロージャの定義部分である。 この内側のfunctionは、評価されるとき、すなわちsetTimeout()によって実行されるとき, objという識別名が指すオブジェクトを自らが知っていて、その情報を元に実行されるわけである。 ここではクロージャを返す関数をインラインに書いているが、 もちろん別の場所で定義してそれを呼び出すようにしても構わない。 その場合インナー関数にしておくのが分かりやすいだろうと思う。
このクロージャを使った方法は、実はthisキーワードに限らなくて、 ローカルスコープを持つ変数が示すオブジェクトや値に対して使うことができる。 グローバル変数に保存したり、値を文字列に変換したりしなくても、 クロージャをsetTimeout()に渡すことで正しい環境のもとで式を評価し、 期待通りの動作をさせることができる。
クロージャを作って返す関数を使わなくても大丈夫そうなので追記しておく。 以下のようなコードのほうが分かりやすい。
MyObject.prototype.myMethod = function (arg) { var that = this; ...; setTimeout(function () { that.myMethod(arg) }, 100); ...; }
要点はインナー関数がそのままでもクロージャを作ってるというところ。 thisキーワードは実行コンテキストと結びついてしまっているので thisの指すオブジェクトをローカル変数へ入れて使う。
JavaScriptにおいてオブジェクトのプロパティは常に丸見えの状態である。 あるオブジェクトの状態や機能はプロパティへの値の出し入れで決まってしまう というのがJavaScriptの特徴であることからもわかるように、 プロパティを直接操作してしまうこと自体はJavaScriptにおいては特に問題の無 いごく普通の行為である。
しかし、オブジェクト指向におけるカプセル化のことを考えると、 プロパティを直接操作することに対する多少の心地の悪さは否めない。 手軽なスクリプト言語であるから手軽さ優先で、 そのようなことは気にするべきことでもないのかもしれない。 だが将来のJavaScriptがpublicやprivateといったキーワードを用意しているところを見ると、その程度は別にしても、 オブジェクトに対するアクセス制御はより厳密な方向へシフトすると思われる。 ゆえに今の段階でスクリプト言語と言えどもカプセル化を意識しておくことは全 くの無駄にはならないだろう。
そこで、プロパティを操作するためのメソッド(アクセサ)を用意することを考えてみたい。 カプセル化の真似事をして少しばかりの安全性を期待してみるのである (…嘘つきました。安全性はちっとも向上しません。自己満足度は向上します)。
プロパティに対する操作は通常、値の参照(読み出し)と値の設定(書き込み)であるから、 一つのプロパティに対して読み込みを行うメソッドと書き込みを行うメソッドを一つづつ用意すれば事足りる。 だがここでは煩雑さを抑え、名前に対する工夫をもう一歩すすめて、 一つのプロパティに対して読み書き両用のメソッドを一つ用意する形にしてみた。 次のコードがそのプログラムである。 プロパティへのアクセサの名前が例えば`prop'であるとすると、 実際のプロパティの名前は、アクセサ名の先頭にアンダースコア(_)を付加した`_prop'となるようにしてある。 アンダースコアを付けたのは、そのプロパティが外から直接操作すべきでないプロパティであるということを意識した目印のつもりである。
function MyObject(init) { this._prop = init; } function MyObject_prop(val) { if (typeof val != "undefined") { this._prop = val; } return this._prop; } MyObject.prototype.prop = MyObject_prop; // 使用例。メソッドに対する引き数の有無で参照と設定の機能を区別する。 var myObject = new MyObject("initial value"); myObject.prop(); // 値の参照 myObject.prop("new value"); // 値の設定
ところで、一つ二つのプロパティであるならばそうでもないが、 アクセサを用意すべきプロパティが増えてくるとこれが結構面倒くさいと感ずる。 似たような(少しずつ違う)メソッドをいくつも用意しなければならないからだ。
少し前置きが長かった。というわけで、書くのが面倒くさいアクセサをプログラムで生成することを考えてみる。 単純な繰り返しの作業をプログラムで行うのは自然であるし、 なによりも手間を惜しむというのはコンピュータを使う上で大事な姿勢である。 しかも幸いなことにJavaScriptは実行時のオブジェクトの状態が全てであるので、 ダイナミックにメソッドを生成するのは造作も無い。
ここで参考とすべくオブジェクト指向言語のRubyを見てみる。 Rubyにはアクセサを定義するためのメソッドがはじめから用意されている。 Moduleクラスにあるattr、attr_reader、attr_writer、attr_accessorという四つのメソッドがそれである(であろう…あまり詳しくないので間違ってたらゴメン。詳しくはRubyのドキュメントを参照のこと)。 今はアクセサとして読み書き両用のメソッドを考えているのでattr_readerとattr_writerに相当するものは必要ない。 残ったattr、attr_accessorの二つのメソッドに対応するメソッドをFunctionオブジェクトに追加してみよう。
次のコードが実際に書いてみたコードであるが、 実質10行にも満たないプログラムでアクセサを生成できることがわかった。 propメソッドで指定した名前のアクセサを生成し、 prop_accessorメソッドを使うと一度にいくつものアクセサを生成できる。 アクセサの名前と実際のプロパティの名前の関係には注意しておくことが必要であるが、 無名関数を使うため名前空間を汚さないというメリットもある。 単純なアクセサではないアクセサを使いたいときにもオーバーライドするのは簡単である。
// nameで指定された名前のアクセサを用意する。 // 実際のプロパティ名はアクセサの名前の先頭にアンダースコアを足したもの。 function Function_prop(name) { this.prototype[name] = new Function("val", "if (typeof val != 'undefined') { this._" + name + " = val } return this._" + name); } // 引き数で指定された名前(複数可)のアクセサを用意する。 function Function_prop_accessor() { for (var n = 0; n < arguments.length; n++) { this.prop(arguments[n]); } } Function.prototype.prop = Function_prop; Function.prototype.prop_accessor = Function_prop_accessor;
次のように使う。
function MyObject2(init1, init2, init3) { this.prop1(init1); this.prop2(init2); this.prop3(init3); } MyObject2.prop_accessor('prop1', 'prop2', 'prop3');
上のコードの最後の一行は次のようなコードとほぼ等価である。
function MyObject2_prop1(val) { if (typeof val != 'undefined') { this._prop1 = val; } return this._prop1; } function MyObject2_prop2(val) { if (typeof val != 'undefined') { this._prop2 = val; } return this._prop2; } function MyObject2_prop3(val) { if (typeof val != 'undefined') { this._prop3 = val; } return this._prop3; } MyObject2.prototype.prop1 = MyObject2_prop1; MyObject2.prototype.prop2 = MyObject2_prop2; MyObject2.prototype.prop3 = MyObject2_prop3;
スクリプトごときにあれやこれやと細かいことを考えるほうが無駄ではないかとも思われる向きもあろうが、 同じアホならピイヒャララということで、さあ、どうだ。(謎)
コンストラクタ関数を使ってユーザ定義オブジェクトを作るような場合には toStringメソッドも再定義しておきましょう。デバッグなどに便利です。
JavaScriptのオブジェクトは通常Objectオブジェクトから派生していますので、 初めからtoStringメソッドを呼び出すことができます。 これはユーザ定義オブジェクトであっても同様です。
このtoStringメソッドは少し特別な扱いがされていて、 オブジェクトを参照するときに文字列であることを期待されるような場合に 自動的にこのメソッドが呼ばれることがあります。
たとえば、documentオブジェクトのwrite/writelnメソッドや windowオブジェクトのalertメソッドなどの引き数としてオブジェクトを渡した場合、 あるいは、文字列とオブジェクトを連結演算するような場合には そのオブジェクトに対してtoStringメソッドが自動的にコールされます。
var obj = new Date(); document.write(obj); // document.write(obj.toString()); と同じ window.alert(obj); // window.alert(obj.toString()); と同じ var str = "obj is " + obj; // var str = "obj is " + obj.toString(); と同じ
これは自分で定義したユーザ定義オブジェクトでも同様なのですが、 toStringメソッドはデフォルトでは簡単な内容の情報しか返さないので、 toStringメソッドをオーバーライドして必要な情報を返すように再定義しておきましょう。 そうすればオブジェクトの状態をインスペクトするのがかなり楽になります。
function Name(first, last, nickname) { this.first = first; this.last = last; this.nickname = nickname; } function Name_setNickname(nickname) { this.nickname = nickname; } function Name_toString() { return this.first + ((this.nickname != null) ? ' "' + this.nickname + '"' : "") + " " + this.last; } Name.prototype.setNickname = Name_setNickname; Name.prototype.toString = Name_toString; var name = new Name("Hikaru", "Utada"); window.alert(name); name.setNickname("Hikki"); window.alert(name);
結果:
Hikaru Utada Hikaru "Hikki" Utada
Objectコンストラクタのprototypeにcloneメソッドを作ってみる作戦。 オブジェクトを含むようなオブジェクトも再帰的にコピーしますが、 単純なデータコンテナとしてのオブジェクト以外はコピーできません。
function Object_clone() { var obj; var constructor = this.constructor; var obj = (constructor == Boolean || constructor == Date || constructor == Number || constructor == String) ? new constructor(this.valueOf()) : new constructor(); for (x in this) { var value = this[x]; obj[x] = (value != null && typeof value == "object") ? value.clone() : value; } return obj; } Object.prototype.clone = Object_clone;
次のように使う。
var src = [999, "string", [100, 101, 102]]; var dest = src.clone(); document.write(dest);結果:
999,string,100,101,102
オブジェクトのコピーを作るという時点で邪道だったりして。 (を、IEが落ちた。このコードには何か危険があるのかな・・・。(-_-;))
(そうしたい場面を思いつかないが) Functionオブジェクトのクローンを作りたいとき、次のようにしてみるのはどうだろう? ちょっとヤバそうな感じだが、実際のところヤバいかもしれない(笑)。
var cloneFunc = (new Function("return (" + func + ")"))();
Client-Side JavaScript Referenceによると、 JavaScript1.2からfunctionステートメントをネストして使えるということなので、 ここで静的スコープときたらやっぱクロージャでしょ。(←ホントは全然わかってない(^_^;))
<script language="JavaScript1.2"> var n = 999; function make_closure() { var n = 100; return function () { return n++; }; } document.writeln(typeof(make_closure()) + "<BR>"); var c1 = make_closure(); var c2 = make_closure(); document.writeln(c1()); document.writeln(c1()); document.writeln(c2()); document.writeln(c2()); document.writeln(c1()); document.writeln(c2()); document.writeln("<BR>" + n); </script>
結果:
function 100 101 100 101 102 102 999
あらやだ♪ make_closure()の中の変数nが別々にカウントアップされてることに注目してください。 これが本当にクロージャなのかどうか、私はしりません。 が、しかし動作はクロージャっぽいじゃないの?
帰ってくるのがclosureとかでなくてfunctionっちゅーのがやや格好悪いけれども、 それが幸いして、クロージャを評価する際は格好つける(カッコを付ける)だけで良さそうです。:-)
ジオシティーズのホストwww.geocities.co.jpとtools.geocities.co.jpでcookieを共用するには、 domainとpathを指定してcookieを利用しなければなりません。 次のような感じになるでしょう。 ただし、pathを指定するとブラウザによっては問題があるらしいので、注意する必要はあります。
document.cookie = "name=data;domain=.geocities.co.jp;path=/SiliconValley-PaloAlto/5042/";
共用して何に使うのかは知ったこっちゃありません。(^_^;)
ところで、ちゃんと調べてないのでアレですが、domainを指定するのは実はメリットがあります。
Netscapeのcookieは、サーバもしくはドメイン当たり20個で、 それ以上になると古いものから消されていくという制限があるというのは知られています。 この「サーバもしくはドメイン当たり」というのがクセモノなんです。
たとえば、ジオシティーズではdomain属性を指定しないとdomain=www.geocities.co.jpが指定されたのと同じになります。 ところがdomain=.geocities.co.jpなどと指定するとこれとは区別されます。 つまりdomain=www.geocities.co.jpのcookieが20個、それとは別にdomain=.geocities.co.jpのcookieが20個使えるのですね。 たぶん。
それがどうしたと言われればそこまでですが、 うまくすれば自分の発行したcookieを長らえさせることが出来るかもしれません。
(削除)
IE5だと小数点数や負数、結構大きな値でもソレっぽく変換してくれるが、 NC4.6では 0 ~ 231-1の整数以外はまともな変換ができませんでした。
var func = new Function("a", "b", "return a + b;"); func(1, 2);
var func = function test(a, b) { return a + b; }; func(1, 2);
var func = function (a, b) { return a + b; } func(1, 2);
document.onclick = function () { alert('Hello, how are you?'); };
function outer() { function localName() { ... }; var localVariable = function () { ... }; }
function (a, b) { return a + b; }(1, 2); var str = function(){return 'Hello, ';}() + function(){return 'world!';}();
ウィンドウ下部のステータスバーに表示されるデフォルトメッセージを設定したりする。
window.defaultStatus = "Your message is here.";
statusプロパティとはちょっと違うようです。 下の各行にマウスポインタを当てたり外したりして確認してみてください。
window.status = "status 1"; window.status = "status 2"; window.defaultStatus = "defaultStatus";
IE5で、マウスのドラッグによる文字列選択をできないようにイケズしてみる。 ちなみにこの項目全体はイケズしてある。ためしにズリズリしてみたまえ。 でもCTRL+Aされたりすると弱い。
<body onselectstart="return false;">
そうだ!うちの掲示板のペーストボタンを強制するためにイケズしてみよう(嘘)。
JScriptで日本語を含んだような文字列を1文字単位に分割したいときは次のようにすればどうでしょう。
var ary = "geoジオgeo".split('');
ちなみにネスケのJavaScriptで同じことをすると日本語文字もバラバラになっちゃうようです。
WSHを使ってInternet Explorerに指定のページを読み込ませたいことがあります。 ところがInternet Explorer ApplicationのオブジェクトのNavigateメソッドは、 ページを読み込み終わるまで待ってくれません。 そこで読み込み完了を監視するコードを用意しないといけないのですが、 以下の関数はそこそこ動作する関数です。 `url'は開きたいページのURL、`wait'は最大待ち時間(ミリ秒単位)、 `IE' はInternetExplorer.Applicationオブジェクトです。 読み込みに成功するとtrue、時間切れになるとfalseを返します。
function loadPage(url, wait) { IE.Navigate(url); //WScript.Echo(url); var startTime = (new Date()).getTime(); var limit = startTime + wait; while ((new Date()).getTime() <= limit) { if (IE.Busy) continue; var doc = IE.Document; // IE.Documentが検証中に変化するかもしれないので・・・ if (doc.readyState == 'complete' && doc.URL == url) return true; WScript.Sleep(200); // WSH2.0 } return false; }
(注)リダイレクトされるとか、 あるいはローカルファイルなどの指定などでURLが変化するような場合はこのままでは上手く動作しません。
条件分岐コメント(Conditional Comments)はブラウザの種類やバージョンによる分岐を可能にするための方法の一つである。 この機能はInternet Explorer 5.0以降のブラウザに備わっているので、 とくにIE5.0以降とそれ以外のブラウザをサックリと区別したいときに使える。 スクリプトによる分岐ではないこともミソだ。
条件分岐コメントは大きく二つの種類がある。ダウンレベルブラウザから見えない条件分岐コメントと、 ダウンレベルブラウザから見える条件分岐コメントである。 ここでいう「ダウンレベルブラウザ」とはIE5.0以降のブラウザ以外のブラウザのことである。 それに対してIE5.0以降のブラウザを「アップレベルブラウザ」と呼ぶ。
種類 | 形式 | 説明 |
---|---|---|
ダウンレベルから見えない | <!--[if 式]> HTMLコード <![endif]--> | 全体がHTMLコメントのように解釈されるため、ダウンレベルブラウザは常に無視する。 |
ダウンレベルから見える | <![if 式]> HTMLコード <![endif]> | <!~>の部分が無視されるため、ダウンレベルブラウザはHTMLコードの部分を常に表示する。 |
いずれの形式の場合も 式 がtrueであれば、アップレベルブラウザは HTMLコード の部分を解釈して表示する。 ダウンレベルブラウザの場合は、式 とは無関係にその挙動が決まっている。
式 の構文は次の通り。(なんか間違ってるかも…(^_^;))
式 ::= 項 | 式 '|' 項 項 ::= 値 | 項 '&' 値 値 ::= 'true' | 'false' | オペレータ 値 | '(' 式 ')' | 比較 オペレータ ::= '!' 比較 ::= フィーチャ | フィーチャ バージョン | ('lt' | 'lte' | 'gt' | 'gte') フィーチャ バージョン フィーチャ ::= 'IE' バージョン ::= 整数 | 浮動小数点数
「式」の定義中の'|'はいわゆるOR演算子、「項」の定義中の'&'はいわゆるAND演算子である。 また「オペレータ」の'!'はNOT演算子で、後に続くオペランドのブール値を否定する。
「比較」の定義中の'lt'、'lte'、'gt'、'gte'はそれぞれ、「より小さい」、「より小さいか等しい」、 「より大きい」、「より大きいか等しい」を表す比較演算子である。 二つのオペランドを取り、前のオペランドが後のオペランドと比べてどうなのかを記述する。 例えば、前のオペランドが後のオペランドより小さければlt演算はtrueとなる。 一致を調べるには演算子を使わず「フィーチャ」と「バージョン」を並べて書くだけである。
「フィーチャ」は現在実行中のブラウザの属性を表すものであろう。 が、いまのところ'IE'しか無い。 現状では、この'IE'が単独で使用されたとき常にtrue値として解釈され、 「バージョン」との比較においては現在実行中のブラウザのバージョンに応じた値として解釈されるようである。
「バージョン」はブラウザのバージョン番号を表す数値である。 「バージョン」が整数で指定されたとき、 ブラウザのバージョンの比較はメジャーバージョンだけがその対象となる。 マイナーバージョンまでを比較するには、整数に小数点とさらに4桁の数字を付け足す。 この4桁の数字というのがクセモノでなんだかよくわからない。 Version Vectorとか言うらしいが詳細は MSDN を参照のこと。 IE5.0には5.0000のほかに5.0002というのがあるとか書いてある。
試していないので正しいかどうか不明だがm(_'_)m、思いつくままパターンを示す。
<!--[if IE]> IE5.0以降のブラウザは表示するんじゃないか? <![endif]--> <!--[if IE 5]> IE5.0とIE5.5は表示するんじゃないか? <![endif]--> <!--[if gte IE 6]> IE6以降は表示するんじゃないか? <![endif]--> <!--[if lt IE 5.5000]> IE5.0は表示するんじゃないか? <![endif]--> <!--[if (lt IE 5.5000) | (IE 6)]> IE5.0とIE6は表示するんじゃないか? <![endif]--> <!--[if false]> 表示されないんじゃないか? <![endif]--> <!--[if ! IE]> ひょっとしてやっぱり表示されないんじゃないか? <![endif]--> <![if IE]> ダウンレベルブラウザとIE5.0以降は表示。要するにとにかく表示するんじゃないか? <![endif]> <![if ! IE]> IE5.0以降以外のブラウザは表示するんじゃないか? <![endif]> <![if ! gte IE 6]> IE6以降以外のブラウザは表示するんじゃないか? <![endif]>
Stringオブジェクトのsplit()メソッドは、IEとネスケとでは少し動作が違う。
IE5.0 | IE5.5 | NC4.5 | NC4.78 | NS6 | |
---|---|---|---|---|---|
"a,b,,c".split(",").join("+") | a+b++c | ||||
"a,b,,c".split(/,/).join("+") | a+b+c | a+b++c | |||
"a,b,,c".split(",", 2).join("+") | a+b++c | a+b | |||
"a,b,,c".split(/,/, 2).join("+") | a+b+c | a+b |
IE = Internet Explorer / NC = Netscape Communicator / NS6 = Netscape 6
document.expandoをfalseにセットすると、expandoプロパティの機構を停止させることができる。 expandoとは、オブジェクトに自由にプロパティを付け加えることのできる機能のことらしい。
window.clipboardData.setData('text', 'Your string is here.'); var str = window.clipboardData.getData('text');
ただしインターネットオプションのセキュリティの設定で 「スクリプトによる貼り付け処理の許可」が無効になっていれば使えないらしい。
右クリックで出るコンテキストメニューを表示しないようにする。
<body oncontextmenu='return false;'>
スクリプトで設定するときは次のような感じでどうですか?
document.oncontextmenu = new Function('return false;');
Last modified: 2002-03-01T21:40:27+09:00