[EffectiveJavaScript輪読会7]「配列のようなオブジェクト」にも、総称的な配列メソッドを再利用できる

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。

前回のおさらい

前回は、「列挙の実行中にオブジェクトを変更しない」をお届けしました。

[EffectiveJavaScript輪読会7]複数のループよりも反復メソッドが好ましい
どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。前回のおさらい前回は、「列挙の実行中にオブジェク...

今日は4回目で、 「「配列のようなオブジェクト」にも、総称的な配列メソッドを再利用できる」 をお届けします。

テキスト第5章「配列とディクショナリ」の項目51に対応しています。

今日のアジェンダ

  • ジェネリックメソッド
  • ジェネリック配列メソッド
  • 「配列のようなオブジェクト」のルール
  • 非ジェネリック配列メソッド「concat」

ジェネリック配列メソッド

項目51の原題はこうです。

Item 51: Reuse Generic Array Methods on Array-Like Objects

この、ジェネリック配列メソッドとはなんでしょうか。

ジェネリックメソッドとは、主に静的型付け言語で用いられる、使える型を限定しないメソッドです。

function myFunction51_00() {

  /** 大きい方の整数を返す(整数版) */
  function getMaxInt(x, y) {
    if (!Number.isInteger(x)) return;
    if (!Number.isInteger(y)) return;
    return x > y ? x : y;
  }

  console.log(getMaxInt(5, 7)); // => 7
  console.log(getMaxInt(5.1, 7)); // => undefined
  console.log(getMaxInt(5, 7.2)); // => undefined

  /** 大きい方の整数を返す(小数版) */
  function getMaxFloat(x, y) {
    if (Number.isInteger(x)) return;
    if (Number.isInteger(y)) return;
    return x > y ? x : y;
  }

  console.log(getMaxFloat(5.1, 7.2)); // => 7.2
  console.log(getMaxFloat(5.1, 7)); // => undefined
  console.log(getMaxFloat(5, 7.2)); // => undefined

  /** 大きい方の整数を返す(ジェネリックメソッド) */
  function getMaxNumber(x, y) {
    return x > y ? x : y;
  }

  console.log(getMaxNumber(5, 7)); // => 7
  console.log(getMaxNumber(5.1, 7.2)); // => 7.2
  console.log(getMaxNumber(5.1, 7)); // => 7
  console.log(getMaxNumber(5, 7.2)); // => 7.2
}

組み込みオブジェクトのArrayのメンバーは、もちろん、Arrayオブジェクトのメンバーです。

しかし、実は「配列のようなオブジェクト」にも使えるよ(なのでジェネリック配列メソッドと呼ぶ)、というのが今回のお話です。

ジェネリック配列メソッド

たとえば、ArrayオブジェクトのforEach()メソッドを、argumentsオブジェクトでも使えるようにしてみましょう。(argumentsオブジェクトはArrayオブジェクトではありません。詳しくは項目22を参照ください)

Arrayオブジェクトから、forEach()メソッドを抽出し、callメソッドでargumentsに結合します。

function myFunction51_01() {

  function printArray_() {
    [].forEach.call(arguments, (a) => console.log(a));
  }

  const scores = [98, 74, 85, 77, 93, 100, 89];
  printArray_(scores);// => [ 98, 74, 85, 77, 93, 100, 89 ]

  const names = ['Bob', 'Tom', 'Ivy'];
  printArray_(names);// => [ 'Bob', 'Tom', 'Ivy' ]

}

その他のジェネリック配列メソッド

その他のメソッドも確認してみましょう。

function myFunction51_01_02() {

  const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };

  // indexOf
  let result = Array.prototype.indexOf.call(arrayLike, 'c');
  console.log(result); // => 2

  // join
  result = Array.prototype.join.call(arrayLike, ':');
  console.log(result); // => a:b:c

  // map
  result = Array.prototype.map.call(arrayLike, (value, index) => index + ":" + value);
  console.log(result); // => [ '0:a', '1:b', '2:c' ]

  // some
  result = Array.prototype.some.call(arrayLike, (value) => value === 'a');
  console.log(result); // => true

}

V8構文

ちなみに、V8ではargumentsオブジェクトの代わりにレスト構文を使えるようになりましたので、このように書けます。

function myFunction51_01_02() {

  const printArray_ = (...args) => args.forEach(a => console.log(a));

  const scores = [98, 74, 85, 77, 93, 100, 89];
  printArray_(scores);// => [ 98, 74, 85, 77, 93, 100, 89 ]

}

「配列のようなオブジェクト」のルール

では、「配列のようなオブジェクト」とは、どのように定義できるのでしょうか。

  1. lengthプロパティを持つ
    • 整数
    • 範囲:0~2^32 -1
  2. lengthプロパティが、そのオブジェクトの最大インデックスよりも大きい
    • インデックス:0~2^32 – 2
    • それを文字列で表現したものが、オブジェクトにおけるプロパティのキーであること

このような用件を満たせば、配列のようなオブジェクトを定義できます。

function myFunction51_02(){

  const arrayLike = {0:'a', 1:'b', 2:'c', length : 3 };
  const result = Array.prototype.map.call(arrayLike,(s) => s.toUpperCase());

  console.log(result); // =>  [ 'A', 'B', 'C' ]
}

JavaScriptのString、Array、TypedArray、Map、Setオブジェクトは、すべての組み込み反復可能オブジェクトです。

なので、文字列型にも、ジェネリック配列メソッドは使えます。

function myFunction51_03() {

  const str = 'abc';
  const result = Array.prototype.map.call(str, (s) => s.toUpperCase());

  console.log(result); // =>  [ 'A', 'B', 'C' ]
}

JavaScriptの配列の特殊性

しかしながら、JavaScriptの配列を完全に再現するのはむずかしいようです。

なぜなら、JavaScriptの配列は、内部で特殊な振舞いをしているからです。

プロパティの自動削除

配列は、lengthプロパティの設定値よりも小さい値nを設定されたら、インデックスがn以上のプロパティを自動削除します。

function myFunction51_04() {

const array = [1,2,3];
array.length = 1;

console.log(array);  // => [ 1 ]
}

プロパティの自動追加

配列は、lengthプロパティの設定値以上のインデックスnを持つプロパティを追加すると、lengthプロパティに自動的にn + 1が設定されます。

function myFunction51_05() {

const array = [];
array.length = 3;

array.push(4);

console.log(array.length);  // => 4
}

このような振舞いを、実装でカバーするのは困難でしょう。

しかし、Arrayオブジェクトのメソッドは、強制的にlengthプロパティを更新するので、気にしなくていいようです。

非ジェネリック配列メソッド「concat」

Arrayオブジェクトのメンバーの中で、唯一、concatメソッドだけは、ジェネリックメソッドではありません。

それは、concatメソッドが、内部プロパティの[[Class]]を参照しているからだそうです。(裏取りができませんでした)

[[Class]]を参照して、本物の配列であれば、concatは正しく挙動(配列の要素を結合する)するそうです。

なので、argumentオブジェクトでは、ただしく挙動しません。

function myFunction51_06() {

  const namesColumnBefore_ = function () {
    return ['names'].concat(arguments);
  }

  const names = ['Bob', 'Tom', 'Ivy'];
  console.log(namesColumnBefore_(names)); // => ['names',{ '0': [ 'Bob', 'Tom', 'Ivy' ]}]

  console.log(namesColumnBefore_('Bob', 'Tom', 'Ivy')); // =>  ['names',{ '0': 'Bob', '1': 'Tom', '2': 'Ivy'}]

}

これを解決するためには、配列のようなオブジェクトにたいし、slice()メソッドを結合します。

function myFunction51_07() {

  const namesColumnAfter_ = function () {
    return ['names'].concat([].slice.call(arguments));
  }

  const names = ['Bob', 'Tom', 'Ivy'];
  console.log(namesColumnAfter_(names)); // [ 'names', [ 'Bob', 'Tom', 'Ivy' ] ]

  console.log(namesColumnAfter_('Bob', 'Tom', 'Ivy')); // [ 'names', 'Bob', 'Tom', 'Ivy' ]
}

まとめ

以上で、「配列のようなオブジェクト」にも、総称的な配列メソッドを再利用できる、をお届けしました。

第7回目の輪読会は、ここまでです。

Special Thanks

このシリーズの目次

タイトルとURLをコピーしました