/ 最近 .rdf 追記 編集 設定 本棚

脳log[20080208] Arrayと Stringの故意に汎用的なメソッドたち (1) | Arrayと Stringの故意に汎用的なメソッドたち (2) | Arrayと Stringの故意に汎用的なメソッドたち (3) | Arrayと Stringの故意に汎用的なメソッドたち (4) | Arrayと Stringの故意に汎用的なメソッドたち (5) | Arrayと Stringの故意に汎用的なメソッドたち (6) | Arrayと Stringの故意に汎用的なメソッドたち (7)



2008年02月08日 (金) 寝ているすきに腕まくらをされに来ていたにゃんこ。これだから冬は(≧∀≦)

[javascript] Arrayと Stringの故意に汎用的なメソッドたち (1)

Under Translation of ECMA-262 3rd Edition を読んでいて見つけた「故意にに汎用的である」と書かれたメソッド群。Arrayと Stringのほとんどのメソッドが該当する。

Firefoxが Array.prototype.methodなどを Array.methodからも参照できるようにする(している)のも 仕様が汎用的で再利用が可能になっているからだろう。

それら汎用的なメソッドを使い回してやるために、その動作を javascriptのコードで表してみる。

 Array.prototype.concat([item1[, item2[, ...]]])

function() {
  var array = [];
  var array_index = 0;
  var item = this;
  var arg_index = 0;
  do {
    if(item instanceof Array) { // (1)
      var item_index = 0;
      while(item_index !== item.length) {
        if(item_index in item) {
          array[array_index++] = item[item_index++];
        } else {
          ++array_index; ++item_index; // (2)
        }
      }
    } else {
      array[array_index++] = item;
    }
  } while(arg_index !== arguments.length &&
          (item = arguments[arg_index++] || true)
  );
  array.length = array_length;
  return array;
}
  • thisと引数は Arrayであるかどうかをチェックされ、扱いが変わる。(1)
  • 新しい配列にコピーするときに、(Arrayである) thisや引数の疎な部分が省略されることはない。(2)
  • thisが Arrayでない場合、単に戻り配列の "0" プロパティに thisオブジェクトが入っているというだけである。(余計な引数なしで呼び出して、Arrayでなければ配列化、という使い方ができる。うれしいか?)

 Array.prototype.join(separator)

function(separator) {
  var length = this.length>>>0; // (1)-1
  if(typeof(separator) === "undefined") {
    separator = ",";
  }
  separator = ToString(separator);
  if(length === 0) {
    return "";
  }
  var result;
  var index = 0;
  var item = this[index++];
  item = (item == null) ? "" : ToString(item);
  result = item;
  while(index !== length) { // (1)-2
    item = this[index++];
    item = (item == null) ? "" : ToString(item);
    result += separator + item;
  }
  return result;
}
// argが数値の場合は厳密には違うかも。
// 以後もたびたび登場する予定。
function ToString(arg) {
  return ""+arg.valueOf();
}
  • 0 から数値化された lengthプロパティの値までを処理する。(1)
  • 戻り値は必ず文字列であり、this.length-1 個のセパレータを必ず含む。
  • thisが Arrayでない場合、例えば Array.prototype.join.call({length:10+1}, " ") で長さが 10 のスペースを得ることが期待される。

[javascript] Arrayと Stringの故意に汎用的なメソッドたち (2)

 Array.prototype.pop()

function() {
  var length = this.length>>>0;
  if(length === 0) {
    this.length = length; // (1)-1
    return; // undefined
  }
  length -= 1;
  var result = this[length];
  delete this[length];
  this.length = length; // (1)-2
  return result;
}
  • (数値化された) this.lengthを 1つデクリメントしたプロパティを削除し、その値を返す。
  • pop()を呼び出すと副作用で lengthプロパティが数値になる。(1)

 Array.prototype.push([item1[, item2[, ...]]])

function() {
  var length = this.length>>>0;
  for(var i = 0; i < arguments.length; ++i) {
    this[length++] = arguments[i];
  }
  this.length = length; // (1)
  return length;
}
  • (数値化された) this.lengthをインクリメントしながら引数をプロパティに設定していく。
  • push()を呼び出すと副作用で lengthプロパティが必ず数値になる。(1)

[javascript] Arrayと Stringの故意に汎用的なメソッドたち (3)

 Array.prototype.reverse()

function() {
  var length = this.length>>>0;
  var mid = Math.floor(length/2);
  for(var p = 0; p !== mid; ++p) {
    var q = length-p-1;
    var P = this[p], Q = this[q];
    if(!(q in this)) {
      delete[p];
    } else {
      this[p] = Q;
    }
    if(!(p in this)) {
      delete[q];
    } else {
      this[q] = P;
    }
  }
  return this;
}
  • 0 から数値化された lengthプロパティの値までを処理する。
  • reverse呼び出し前後でプロパティ(疎な要素など)が増えたり減ったりはしない。

 Array.prototype.sort(comparefn)

sort()については、this.length>>>0 が用いられることと、thisを変更して thisを返すことだけを書いておいて、i, j要素を比較する手順について。

function compare(i, j) {
  // (A) 存在しない要素を最後尾へ。
  if(!(i in this)) {
    if(!(j in this)) {
      return +0; // (1)
    } else {
      return 1;
    }
  } else if(!(j in this)) {
    return -1;
  }
  var I = this[i], J = this[j];
  // (B) undefinedな要素を後ろへ。
  if(typeof(I) === "undefined") {
    if(typeof(J) === "undefined") {
      return +0; // (1)
    } else {
      return 1;
    }
  } else if(typeof(J) === "undefined") {
    return -1;
  }

  // (C) ユーザー比較関数
  if(typeof(comparefn) !== "undefined") {
    return comparefn(I, J);
  }
  // (D) デフォルト比較方法 (文字列化して昇順)
  I = ToString(I), J = ToSTring(J);
  if(I < J) {
    return -1;
  } else if(I > J) {
    return 1;
  }
  return +0;
}
  • デフォルトの比較関数はやっぱり要素を文字列化していた。(20080207p01。数値配列を期待通りにソートすることはできない)
  • +0 と書いてみたが、スクリプトで +0 と -0 を区別することはできない。(1)

[javascript] Arrayと Stringの故意に汎用的なメソッドたち (4)

 Array.prototype.shift()

function() {
  var length = this.length>>>0;
  if(length === 0) {
    this.length = length; // (1)
    return; // undefined
  }

  var result = this[0];

  // ひとつずつ前へスライド
  for(var k = 1; k !== length; ++k) {
    if(k in this) {
      this[k-1] = this[k];
    } else {
      delete this[k-1];
    }
  }

  delete this[length-1];
  this.length = length-1;
  return result;
}
  • 0 から数値化された lengthプロパティの値までを処理する。
  • shift()を呼び出すと副作用で lengthプロパティが必ず数値になる。(1)

 Array.prototype.unshift([item1[, item2[, ...]]])

function() {
  var length = this.length>>>0;
  // 要素を引数の数だけ後ろへシフト。
  for(var k = length; k !== 0; --k) {
    var j = k-1;
    if(j in this) {
      this[j + arguments.length] = this[j];
    } else {
      delete this[j + arguments.length];
    }
  }
  // 先頭に引数をコピー
  for(var k = 0; k !== arguments.length; ++k) {
    this[k] = arguments[k];
  }

  this.length = length + arguments.length; // (1)
  return this.length;
}
  • 0 から数値化された lengthプロパティの値までを処理する。
  • unshift()を呼び出すと副作用で lengthプロパティが必ず数値になる。(1)

[javascript] Arrayと Stringの故意に汎用的なメソッドたち (5)

 Array.prototype.slice(start, end)

function(start, end) {
  var array = [];
  var array_index = 0;
  var length = this.length>>>0;
  start = ToInteger(start);
  start = (start < 0) ? Math.max(length+start, 0) : Math.min(start, length);
  end = (typeof(end) === "undefined") ? length : ToInteger(end);
  end = (end < 0) ? Math.max(length+end, 0) : Math.min(end, length);
  for(var k = start; k < end; ++k) {
    if(k in this) {
      array[array_index++] = this[k];
    } else {
      ++array_index; // (1)
    }
  }
  array.length = array_index;
  return array;
}
function ToInteger(x) {
  x = +x;
  if(isNaN(x)) {
    return +0; // (2)
  }
  if(x === 0 || !isFinite(x)) {
    return x;
  }
  // 0 に近づける。(小数点以下を切り捨てる)
  return (0 <= x) ? Math.floor(x) : Math.ceil(x);
}
  • 数値化された this.lengthと引数 start, end を元に範囲を決定し処理する。
  • startと endには負数も使用可。
  • thisの存在しない要素がコピーされることはないが、無視されるわけではない。(1)
  • +0 と書いたがスクリプトが +0 と -0 を区別することはできない。(2)

 Array.prototype.splice(start, deleteCount[, item1[, item2[, ...]]])

function(start, deleteCount) {
  var array = [];
  var length = this.length>>>0;
  start = ToInteger(start);
  start = (start < 0) ? Math.max(length+start, 0) : Math.min(start, le  deleteCount = ToInteger(deleteCount);
  deleteCount = Math.min(Math.max(deleteCount, 0), length-start);
  // 取り出す要素をコピー。(まだ削除はしない)
  for(var k = 0; k !== deleteCount; ++k) {
    var l = start+k;
    if(l in this) {
      array[k] = this[l];
    }
  }
  // (A) 要素を後ろへずらして空きを作る
  //     (後で上書きされてしまう要素までずらしてない?)
  if(deleteCount < arguments.length) {
    for(var k = length-deleteCount; k !== start; --k) {
      var l = k + deleteCount -1;
      var m = k + arguments.length -1;
      if(l in this) {
        this[m] = this[l];
      } else {
        delete this[m];
      }
    }
  // (B) 要素を前へ詰めて、空きを新要素と同じ数にする
  } else if(arguments.length < deleteCount) {
    for(var k = start; k !== length-deleteCount; ++k) {
      var l = k + arguments.length;
      var m = k + deleteCount;
      if(m in this) {
        this[l] = this[m];
      } else {
        delete this[l];
      }
    }
    // (なぜ逆順で削除する?)
    for(var k = length; k !== length-deleteCount+arguments.length; --k) {
      delete this[k-1];
    }
  }
  // 追加する要素で thisを上書き
  if(arguments.length === deleteCount) {
    for(var k = 0; k !== arguments.length; ++k) {
      this[start+k] = arguments[k];
    }
  }
  this.length = length - deleteCount + arguments.length;
  return array;
}
  • 数値化された this.lengthと引数 start を元に範囲を決定し処理する。
  • startには負数も使用可。
  • splice()を呼び出すと副作用で lengthプロパティが必ず数値になる。
  • splice()は仕事を抱え込みすぎ。

[javascript] Arrayと Stringの故意に汎用的なメソッドたち (6)

パターンがわかってきた。

 Arrayのビルトインメソッドは concat()を除いて、this.lengthを 32-bit unsigned int(UINT32)に変換したものを処理範囲の上限として利用する。(下限はもちろん 0)

  • concat()は thisや引数が Arrayかどうかで処理を変えるから、(利用するときは) this.lengthを直接用いる。

 自身の長さを変更するメソッド(pop(), push(), shift(), unshift(), splice())を呼び出すと lengthプロパティは必ず UINT32に変更される。

  • 自身の長さを変更しない reverse(), sort()や、自身を変更しない concat(), join(), slice()を呼び出しても lengthは変化しない。

 あんまり使えない。

 Arrayの多くのメソッドは自己破壊的で、Stringに適用することができない。

Firefox(JavaScript1.5?)では適用すると Array.prototype.method.call(string) is read-onlyという警告がいくつも出る。

一度の呼び出しでいくつも出ることから read-onlyなのはメソッドではなく、自己破壊的な Array.prototype.method()を適用された stringだと思われる。

警告が出るだけで、戻り値は得られるので pop()で末尾の文字、shift()で先頭の文字が得られる。

unshift()、push()の返す数値は使えない。(その長さを持った文字列は存在しないので)

reverse(), sort()は全く役に立たない。役に立つ値を返さないし、並べ替えには失敗しているから。

slice()は String.prototypeに同名のメソッドがすでに存在する。

splice()がちょっと面白く、splice(startと deleteCount) の引数はちょうど String.prototype.substr(start, length)と対応するが、返ってくるのが substr()->"文字列" なのに対し、splice()->["文","字","列"] となる。

string.split("")の代わりに Array.prototype.splice.call(string, 0)としてみるのはいかが? (警告が出る上に IE7では使えませんが)

 そもそも IEの JScript5.7では文字列に添え字を使ってアクセスできないので Arrayのメソッドを適用しても意味がない。

Stringではなく、配列のようなオブジェクト(コレクションとか argumentsとか)に適用するのが正解か。

 concat()と join()は広く使える。

 配列化1 (びみょ〜)
// unknown_objectが配列なら unknown_objectのコピー、
// それ以外なら unknown_objectを唯一の要素とする配列が返る。
var array = Array.prototype.concat.call(unknown_object);
 配列化2 (IEでは使えない)
// lengthプロパティと [] での要素アクセスが可能な、
// 配列っぽいオブジェクト(NodeListとか argumentsとか)を配列化。
var array = Array.prototype.slice.call(document.getElementsByTagName("pre"), 0);
 繰り返し文字列 (けっこう使える)
// "        " が得られます。
var softtab = Array.prototype.join.call({length:8+1}, " ");
// softtabを使ったレベル3のインデントが得られます。
var indent = new Array(3+1).join(softtab); // prototypeいらねー

二番目の書き方を使うなら Arrayのビルトインメソッドを他のオブジェクトに使い回すという趣旨が……。

[javascript] Arrayと Stringの故意に汎用的なメソッドたち (7)

今回から String.prototypeのメソッド。Stringは自己破壊的なメソッドを持っていないから、Arrayのビルトインメソッドと違って適用範囲が広いことを期待している。

……と思ったが、Stringのビルトインメソッドはどれも事前に ToString(this)を呼んでしまう。(ToString()については (1))

indexOfで配列の探索ができるんじゃないかと期待していたが無理だった。がっかり。

これにて終了。

split()に渡す正規表現のサブマッチに特別な意味があるとか、split()はグローバルフラグを無視するとか、グローバルフラグの立った正規表現で exec(string)を呼んで空文字列にマッチしたときはこちらで lastIndexプロパティを 1増やさないといけないとか、もやもやしてたことが Under Translation of ECMA-262 3rd Editionにはいろいろ書いてありました。訳者に感謝。

See also...

listed by...