多元配列を一元配列に変換 (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