
配列はサブクラス化可能なように設計されています。
必要に応じて 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
concatfunction is intentionally generic; it does not require that itsthisvalue 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なんて時代だそうだけど。
おしまい
関連
- 他人の能力を自分のものにできる .apply()で高度な成り済ましを(JavaScript おれおれ Advent Calendar 2011 – 15日目)
- 配列のコンストラクターを改めて見てみる(配列とかおれおれAdvent Calendar2018 – 01日目)
- Array(3)の代わりにArray.of(3)を使おう。(配列とかおれおれAdvent Calendar2018 – 03日目)
 
          