LINDORのAdvent Calendar(本物)の8日目を開けたところ。
配列とかおれおれAdvent Calendar2018 – 08日目

配列はサブクラス化可能なように設計されています。 必要に応じて extends Array で継承して、独自の拡張配列クラスを用意することができます。

Arrayを継承したクラスの例

class SugoiArray extends Array {
  // ランダムに要素を返す
  random() {
    return this[Math.floor(Math.random() * this.length)];
  }

  // 空にする
  empty() {
    this.length = 0;
  }
}

const arr = new SugoiArray(10, 20, 30);
console.log(arr); // => [ 10, 20, 30 ]

console.log(arr.random());
console.log(arr.random());
console.log(arr.random());
console.log(arr.random());
console.log(arr.random());
console.log(arr.random());

arr.empty();
console.log(arr); // => []

仕様書の記載

ES 2018の仕様書の項目22.1.1The Array Constructorにそう記載があります。

The Array constructor … is designed to be subclassable.

たまたま動くだけじゃないんだよー。

各種メソッド

コンストラクターのメソッドもプロトタイプのメソッドも、全て this の種類に依存しない、汎用関数として設計されています。

例えば concat() の仕様。

Note 2

The concat function is intentionally generic; it does not require that its this value be an Array object. Therefore it can be transferred to other kinds of objects for use as a method.

ノート2

この concat 関数は、汎用であるよう意図されており、 this の値が配列オブジェクトであることを必要としません。従って、他の種類のオブジェクトへ移してメソッドとして利用することが可能です。

これとほぼ同じ文章が、全てのメソッドに記載されています。

これはいける!

例

ちなみに concat() その他のメソッドは Array.prototype.concat のようにして、関数オブジェクトとして配列コンストラクターのプロトタイプに設定されています。

コンストラクターメソッドは Array.of とか。そのまんま。

任意のオブジェクトで使う

関数オブジェクトのメソッド call() や apply() を使って、任意のオブジェクトを this に設定して関数実行してやることができます。

const arrayLike = {
  0: 11,
  1: 22,
  2: 33,
  length: 3,
};

Array.prototype.forEach.call(arrayLike, (value) => {
  console.log(value);
});

apply() の使い方はこんなん。(これは call() だけど。)

2011年てまじか。

任意のオブジェクトのメソッドにする

オブジェクトが決まっているなら、関数プロパティつまりメソッドとして設定してやるとなお素直な実装ができます。

const arrayLike = {
  0: 11,
  1: 22,
  2: 33,
  length: 3,
  forEach: Array.prototype.forEach,
};

arrayLike.forEach((value) => {
  console.log(value);
});

DOM APIの NodeList ( querySelectorAll() の結果)もこの方式です。

console.log(NodeList.prototype.forEach === Array.prototype.forEach);
// => true

任意のクラスのインスタンスメソッドにする

prototype に代入するしかないですかね、今は。

ArrayLike.prototype.forEach = Array.prototype.forEach;

TypeScriptならプロパティを書ける。 ひどい例だけどこれで大丈夫かな。

// TypeScript
class ArrayLike {
  public forEach = Array.prototype.forEach;

  public get length () {
    return 3;
  }

  constructor () {
    this[0] = 11;
    this[1] = 22;
    this[2] = 33;
  }
}

const arrayLike = new ArrayLike();
arrayLike.forEach((value) => {
  console.log(value);
});

IDでランダムアクセスできちゃう配列

夢のようなやつ。

class IndexedArray extends Array {
  constructor (...items) {
    super(...items);

    this._index = new Map();
    this._remap();
  }

  assertItem (item) {
    if (!item || !item.id || typeof item.id !== 'string') {
      console.warn(item);
      throw new Error('Item must have a string ID');
    }
  }

  add (item) {
    this.assertItem(item);

    // overwrite
    if (this._index.has(item.id)) {
      const index = this._index.get(item.id);
      this.splice(index, 1, item);
    }
    // add
    else {
      this._index.set(item.id, this.length);
      super.push(item);
    }
  }

  get (id) {
    const index = this._index.get(id);
    return this[index];
  }

  delete (id) {
    const index = this._index.get(id);
    if (index < 0) {
      return false;
    }

    this._index.delete(id);
    this.splice(index, 1);

    this._remap();

    return true;
  }

  _remap () {
    this.forEach((item, index) => {
      this.assertItem(item);
      this._index.set(item.id, index);
    });
  }
}
const db = new IndexedArray(
  { id: '11', name: 'Alice' },
  { id: '22', name: 'Bob' },
  { id: '33', name: 'Charlie' },
);

console.log(db);
console.log(db[0]); // => { id: '11', name: 'Alice' }
console.log(db.get('11')); // => { id: '11', name: 'Alice' }

console.log('Add Diana');
db.add({ id: '44', name: 'Diana' });
console.log(db[3]);
console.log(db.get('44'));

console.log('Diana was a kong!');
db.add({ id: '44', name: 'Donkey Kong' });
console.log(db[3]);
console.log(db.get('44'));

console.log('Delete Bob');
db.delete('22');
console.log(db);
console.log(db[1]);
console.log(db.get('33'));

といっても、本当はもっと push() とかを上書きしてやらないといけない。あと Array(3) みたいな入力への対処とか。( Array.of() の件。) いっそ Array を継承しない方が楽そう。

初期化子 [] は元のArrayのまま

自作コンストラクターで window.Array や global.Array を置き換えたとしても、配列初期化子 [] で生成されるのは元の Array のインスタンスになります。

これは初期化子 [] から生成する際に固有オブジェクト %ArrayPrototype% を利用するためです。

昔はそうなってなくて、そこから攻撃したりもできたらしい。といってもJavaScript 1.5なんて時代だそうだけど。

おしまい

関連

参考