多元配列を一元配列に変換 (JavaScript)2005年10月08日 01時43分

なんだかよくわからないことになったのでメモしておく。きっかけはある要素ノード中のすべてのノード (テキストノードも含む) を集めようと思ったこと。childNodes では子要素の内容が取得できないし、getElementsByTagName("*") ではテキストノードが取得できない。今までの自分だったらこうしていただろう。

function getDescendants(node, previousNodes)
{
  if (!previousNodes) previousNodes = [];
  var children = node.childNodes;
  for (var i = 0, length = children.length; i < length; i++) {
    var child = children[i];
    previousNodes.push(child);
    if (child.hasChildNodes())
      getDescendants(child, previousNodes);
  }
  return previousNodes;
}

var bodyDescendants = getDescendants(document.body);
// <body><p>Hello, world!!</p></body> に対して
// [[object HTMLParagraphElement], [object Text]] を返す。

しかし、おりしも「実践 JavaScript リファクタリング」(最速インターフェース研究会) を読んだ直後。なにかもっとスマート、というよりむしろトリッキーなことをしてみたくなる。そこで思い浮かんだのが Array#concat 、引数に配列を指定するとそれを展開して元の配列に付け加えた新たな配列を生成してくれるというもの。これと Array#map を組み合わせれば何とかなるかもと思って試行錯誤。結果できたのが以下のコード。

function getDescendantsOrSelf(node)
{
  return (node.hasChildNodes())
         ? Array.prototype.concat.apply([node],
             Array.map(node.childNodes, arguments.callee))
         : [node];
}

var bodyDescendants = getDescendantsOrSelf(document.body).slice(1);

この場合、結果の配列に自分自身も含まれてしまうので最初の要素を削除。正直書き上げた直後は自分でもどうしてうまくいくのかよくわからなかった。

で、これを応用すれば多元配列を一元配列に変換することもできるなと思った次第。どういうときに使えるのかは思い浮かばないけど。

Array.prototype.flatten = function () {
  var element = (arguments.length == 0) ? this : arguments[0];
  return (element instanceof Array)
         ? Array.prototype.concat.apply([],
             Array.map(element, arguments.callee))
         : element;
}

[0, 1, [[]], [2, 3], [4, [[5], [], 6, [7, 8], 9]]].flatten()
// => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

ふと見たら prototype.js でも Array#flatten が定義されている。1.4.0 pre 7 から追加されたようだ。というよりもともと Ruby には flatten メソッドがあったのか。全然知らなかった。Ruby.js でも定義されていたのに見落としていた。もっとしっかり見ないとな……。(それにしても flatten という名前にして本当によかった。危うく toOneDimArray とかセンスのなさ全開の名前をつけるところだった。)

Array#map 及び Array.mapJavaScript 1.6 (Firefox 1.5) から使えるメソッド。ここでは以下のように定義した。

if (!Array.map) {
  if (!Array.prototype.map) {
    Array.prototype.map = function (callback, thisObject) {
      var length = this.length;
      var result = new Array(length);
      for (var i = 0; i < length; i++)
        result[i] = callback.call(thisObject, this[i], i, this);
      return result;
    };
  }
  Array.map = function (array, callback, thisObject) {
    return Array.prototype.map.call(array, callback, thisObject);
  };
}

ちなみに以下のコードは Firefox では動くが IE ではエラーになる。IE では DOM ノードは JavaScript のオブジェクトではないかららしい。

Array.prototype.slice.call(document.documentElement.childNodes, 1);
// IE => 「エラー: JScript オブジェクトを指定してください。」

alert(document.documentElement.childNodes instanceof Object);
// Firefox => true
// IE      => false

DrawingCanvas.js 0.03 公開2005年10月12日 03時10分

高橋登史朗さんDrawingCanvas.js を使ったグラフ描画のデモを公開してくださっています。すばらしいですね。これぞまさしく Ajax という感じです。

しかし、Pentium M 1.3 GHz 、メモリ 256 MB という私の環境では Firefox 1.0 (CSS Positioning バックエンド) での表示がフリーズしたかと思うほど遅かったです。そこで処理を少し改善した DrawingCanvas.js 0.03 (ソース表示) を公開しました。本当は 0.02 公開のときにやろうかと思っていたのですが面倒くさくなってやめたという (^^;

それから、描画部分にスタイルなどを指定する際は、DrawingCanvas#container ではなく DrawingCanvas#parent (コンストラクタの第 1 引数に指定した要素) に対して指定してください。DrawingCanvas#container は private (というよりは protected か) にすべきだったかも。

それにしても DrawingCanvas 自体は非同期通信も何も使っておらず、はてなブックマークでつけられているタグを見てどこが「Ajax」なのだろうと思っていたのですが、こうやって Ajax アプリケーションに組み込まれるとまあ Ajax でもいいかという気分になってきます。いうなれば Advanced JavaScript Applications which are Cross-browser のほうでしょうか (どうでもいいけど私はつい Ajax を「アジャックス」と読んでしまいます)。以下のようなスクリプトもあることですし Advanced かどうかは疑問ですが。(しかしこれを最初から知っていれば DrawingCanvas なんて作っていなかったかも。)

DrawingCanvas.js 0.04 (ソース表示) 公開。0.03 で DrawingCanvas#container にスタイルが指定されてもいいよう、内部的なスタイル指定を冗長にしていたのですが、処理速度の低下を招くことがわかったので元に戻しました。

Opera 8 の SVG のバグ2005年10月16日 00時22分

Opera で SVG を触ってみてつまずいた点など。どうやら Opera は HTML 文書中で SVG を扱おうとするとトラブルになることが多いようだ。

Opera 9.0 Preview 1 では以下のバグはすべて修正されていた。すばらしい。

  1. svg 要素の背景が透過されない。
  2. SVG 要素を文書に追加 / 文書から削除しただけでは描画に反映されない。
    • SVG 要素に属性をセットするなどする必要がある。
  3. HTMLDocument#createElementNS() で生成された SVG 要素の nodeName プロパティが空文字列を返す。
    • Document#createElementNS() で生成された SVG 要素は正しい値を返す。
  4. HTMLDocument#createElementNS() で動的に生成できる SVG 要素の数に制限がある。
    • 一定数を超えるとそれ以降は createElementNS() で SVG の名前空間 URI を指定していても HTML 要素として生成される。
    • この制限は Opera を起動している限り続く。あるページで一定数の SVG 要素を生成したら、ほかのページではもう SVG 要素を生成できない。
    • Windows XP SP2 、Opera 8.50 では 240 回 SVG 要素を生成できた。
    • Document#createElementNS() には制限はない模様。

はてなに参加2005年10月24日 13時16分

はてなにユーザー登録をした。目的ははてなブックマーク。とはいってもソーシャルな何かを求めているわけではなく、単に気になったページを自分のためにメモするオンラインブックマークとして使おうと思っている。

今はブラウザのブックマーク中に「Memos」というフォルダを作ってそこに気になったページを放り込んでいるのだが、数が増えてきて参照するにも整理するにも面倒くさいのでタグによる分類を試してみようと思った次第。

ダイアリーはキーワードリンクが文中に含まれるのがうっとうしいし、何よりここがあるので使う予定なし。その他のサービスは気が向いたら利用しようと思う。アンテナは便利そうかも。

ちなみに登録しようとしたら、ともだち紹介者に30ポイントプレゼントとあったので知人に紹介してもらって登録した。紹介してくれた M さんには改めて感謝する。

JavaScript の new 演算子の意味2005年10月24日 16時25分

JavaScript における new 演算子の動作は大まかにいって以下のとおりである。(new F() とした場合。)

  1. 新しいオブジェクトを作る。
  2. 1 で作ったオブジェクトの [[Prototype]] 内部プロパティ (__proto__ プロパティ) に F.prototype の値を設定する。
    • F.prototype の値がオブジェクトでないのなら代わりに Object.prototype の値を設定する。
  3. F を呼び出す。このとき this の値は 1 で作ったオブジェクトとし、引数には new 演算子とともに使われた引数をそのまま用いる。
  4. 3 の返り値がオブジェクトならそれを返す。そうでなければ 1 で作ったオブジェクトを返す。

ここで「オブジェクトである」というのはプリミティブ値 (文字列、数値、真偽値、undefinednull) ではないということだ。new String("string")new Number(123)new Boolean(true) はオブジェクトだが "string"123true はオブジェクトではない。

コードであらわすと以下のようになる。

Function.prototype.construct = function () {
  var newInstance = new Object();
  if (this.prototype instanceof Object)
    newInstance.__proto__ = this.prototype;

  var returnValue = this.apply(newInstance, arguments);
  if (returnValue instanceof Object)
    return returnValue;
  return newInstance;
};

var o = F.construct(arg); // == new F(arg)

new F() としたとき、実際に F が呼び出される前に新たなオブジェクトが作られるが、そのオブジェクトの生成にあたっては F.prototype の値が参考にされるのみで F 自体は直接関わってはいない。だからこそ一時的なコンストラクタを使って継承を実現ということもできる。

そして、それと同様に考えれば、「prototype.js を用いた OOP で、後方参照できるクラスを宣言する」(Noncommutative Field) で触れられていた配列として渡された引数を用いて new するということは以下のように実現できる。

Function.prototype.applyNew = function (args) {
  var Constructor = function () {};
  Constructor.prototype = this.prototype;
  var instance = new Constructor();
  var result = this.apply(instance, args);
  return (result instanceof Object) ? result : instance;
};

var o = F.applyNew([param1, param2]); // == new F(param1, param2)

余談になるが、F の返り値がオブジェクトでないとき、new F() の値には F の返り値ではなく新たに作られたオブジェクトが使用されるというのは改めて仕様書を読むまで気づかなかった。これまで以下のように書いていたのはまったくの見当違いだったのか。(というかろくにテストしていないのがばればれ。)

function F(arg)
{
  if (!arg) return null;

  ...
}

var o = new F(someArg);

参考