JavaScriptのrepeat()のポリフィルを読む

とてもつらいことに、開発中のサービスでIE11対応のお達しが出た。それまでまぁ対応しているようないないような、まぁしてないんだけどと曖昧な態度でのらりくらりとかわしてきたのだけれど、そうもいかなくなり。

ということで、ECMAScript 6で追加された仕様であるStringのrepeat()が使えなくなり、ポリフィルで対応することになった。読んでみると、色々と勉強になったのでメモ。

スポンサーリンク

ポリフィル

String.prototype.repeat() | MDN」にString.prototype.repeat()のポリフィルがある。以下のようなコードだ。

if (!String.prototype.repeat) {
  String.prototype.repeat = function(count) {
    'use strict';
    if (this == null) {
      throw new TypeError('can\'t convert ' + this + ' to object');
    }
    var str = '' + this;
    count = +count;
    if (count != count) {
      count = 0;
    }
    if (count < 0) {
      throw new RangeError('repeat count must be non-negative');
    }
    if (count == Infinity) {
      throw new RangeError('repeat count must be less than infinity');
    }
    count = Math.floor(count);
    if (str.length == 0 || count == 0) {
      return '';
    }
    // Ensuring count is a 31-bit integer allows us to heavily optimize the
    // main part. But anyway, most current (August 2014) browsers can't handle
    // strings 1 << 28 chars or longer, so:
    if (str.length * count >= 1 << 28) {
      throw new RangeError('repeat count must not overflow maximum string size');
    }
    var rpt = '';
    for (;;) {
      if ((count & 1) == 1) {
        rpt += str;
      }
      count >>>= 1;
      if (count == 0) {
        break;
      }
      str += str;
    }
    // Could we try:
    // return Array(count + 1).join(this);
    return rpt;
  }
}

一つずつ追っていく。

    count = +count;

算術演算子の単項プラス。countが数値じゃない時に、数値に変換する。「算術演算子 | MDN」に詳細。nullも0になるのか…。functionをつっこむとNaNになる。

    if (count != count) {
      count = 0;
    }

countがNaNの時には0にする。NaNを比較すると偽を返す、というのはIEEE 754での決めごとらしい(参考サイト「NaN === NaN が false な理由とutil.isDeepStrictEqual – from scratch」)。

    // Ensuring count is a 31-bit integer allows us to heavily optimize the
    // main part. But anyway, most current (August 2014) browsers can't handle
    // strings 1 << 28 chars or longer, so:
    if (str.length * count >= 1 << 28) {
      throw new RangeError('repeat count must not overflow maximum string size');
    }

JavaScriptの数値は基本的に符号付きなので、31bit。符号なし右ビットシフト>>>を使うと(num >>> 0みたいに)unsignedになるみたいだが、その他のビット演算をすると何かとややこしいことになるらしい(参考「JavaScript のビット演算子に unsigned を期待してはいけない : document」)。で、2014年8月の時点で、たいていのブラウザは28ビット分の整数値を超える文字数の文字列を扱えないので、文字数*リピート回数の文字数がそれを超える時は、オーバーフローとしている。

    var rpt = '';
    for (;;) {
      if ((count & 1) == 1) {
        rpt += str;
      }
      count >>>= 1;
      if (count == 0) {
        break;
      }
      str += str;
    }

repeat()の肝となるところ。rptが返り値となる。strはリピートしたい文字列。

たとえばcount=1のとき、最初の(count & 1) == 1はtrueなのでrptにはstrがそのまま入る。で、符号なし右ビットシフトしてcount=0になり、break。

count=2、つまりcount=10(2)のとき、最初の(count & 1) == 1はfalseなのでrptそのまま。で、右ビットシフトでcountは10(2) -> 1(2) となる。で、strの文字列は二倍の長さになる。次のループのとき、countは1(2)。なので、最初の(count & 1) == 1はtrue。よって、rptには前のループで2倍の長さになったstrが入る。その後の右ビットシフトで、countは0になってbreak。

つまり、ループを回すたびにstrが倍々になっていく。で、倍々になったstrを、countを2進数表記して1が立つ時に文字列を加えていく……とまぁ変な日本語だが、なるほどなぁと思った。

これだと、たとえば10万回のリピートであっても、10万は17ビットで表現できるから、たった17回のループでできるわけだ。頭がよいなぁ。

関連コンテンツ

関連記事

スポンサーリンク

コメントを残す

メールアドレスが公開されることはありません。