[EffectiveJavaScript輪読会8]配列と「配列のようなもの」を区別しよう

GAS

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

前回のおさらい

前回は、「柔軟なインターフェイスのために、構造的な型付けを使う」をお届けしました。

[EffectiveJavaScript輪読会8]柔軟なインターフェイスのために、構造的な型付けを使う
どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。前回のおさらい前回は、「不必要な状態を排除する」...

今日は6回目で、「配列と「配列のようなもの」を区別しよう」をお届けします。

テキスト第6章「ライブラリとAPI設計」の項目58に対応しています。

今日のアジェンダ

  • 多重定義とは
  • 型の判定
  • ユーザー定義型

多重定義とは

多重定義(オーバーロード)とは、同名のメソッドを定義することです。

Javaでは、同じメソッド名の中からどのメソッドを呼び出すかは、指定する引数の型や数、順番を元にコンパイラーが判断してくれますが、JavaScriptでは、最後に読まれたメソッドを呼び出します

function myFunction8_58_01() {

  /** クラスBitVector */
  class BitVector {

    constructor() {
      this.bit = [];
    }

    /** bitを判定するメソッド */
    enable(x) {
      if (typeof x === 'number') this.enableBit(x);
    }

    /** bitを判定するメソッド */
    enable(x) {
      for (let i = 0; i < x.length; i++) {
        this.enableBit(x[i]);
      }
    }

    /** bitをプロパティに格納するメソッド */
    enableBit(x) {
      this.bit.push(x);
    }

    /** ビット配列に引数があるか返すメソッド */
    bitAt(x) {
      return this.bit.includes(x) ? 1 : 0;
    }
  }

  const bits = new BitVector();

  bits.enable(4);
  bits.enable([1, 3, 8, 17]);

  console.log(bits.bitAt(4)); // => 0 ←正しくない結果
  console.log(bits.bitAt(8)); // => 1
  console.log(bits.bitAt(9)); // => 0

}

複数の引数を受け取りたいばあいは、このように、メソッド内で引数の判定を行うといいでしょう。(このようなメソッド、複数のシグニチャを許容するメソッドもまた、多重定義と呼びます)

function myFunction8_58_02(){

/** クラスBitVector */
  class BitVector {

    constructor() {
      this.bit = [];
    }

    /** bitを格納するメソッド */
    enable(x) {

      if (typeof x === 'number') {
        this.enableBit(x);
      } else { //xが配列のようなものと仮定する
        for (let i = 0; i < x.length; i++) {
          this.enableBit(x[i]);
        }
      }

    }

    /** ビットを格納するメソッド */
    enableBit(x) {
      this.bit.push(x);
    }

    /** ビット配列に引数があるか返すメソッド */
    bitAt(x) {
      return this.bit.includes(x) ? 1 : 0;
    }


  }

  const bits = new BitVector();

  bits.enable(4);
  bits.enable([1, 3, 8, 17]);

  console.log(bits.bitAt(4)); // => 1
  console.log(bits.bitAt(8)); // => 1
  console.log(bits.bitAt(9)); // => 0

}

テキストでは、文字列を追加するクラスも紹介されていました。

このように、複数の引数を受け取ることができるメソッドを、多重定義と呼びます。

function myFunction8_58_03() {

  /** クラスStringSet */
  class StringSet {

    constructor() {
      this.string = '';
    }

    /** 引数を判定するメソッド */
    add(x) {
      if (typeof x === 'string') {
        this.addString(x);
      } else if (Array.isArray(x)) { //本当の配列かテスト
        x.forEach(s => this.addString(s), this);
      } else {
        for (const key in x) this.addString(key);
      }
    }

    /** プロパティに文字列があるか判定するメソッド */
    contains(word) {
      return this.string.includes(word);
    }

    /** プロパティにstringを追加するメソッド */
    addString(x) {
      this.string += x;
    }

  }

  const set = new StringSet();

  //複数の引数を受け取ることができる=メソッドが多重定義されている
  set.add('Hamlet');
  set.add(['Rosencrants', 'Guildenstern']);
  set.add({ 'Ophelia': 1, 'Polonius': 1, 'Horatio': 1 });

  console.log(set.contains('Polonius')); // => true
  console.log(set.contains('Guildenstern')); // => true
  console.log(set.contains('Falstaff')); // => false

}

型の判定

JavaScriptの型には、プリミティブ型と組み込みオブジェクト型の2種類があります。

typeof演算子で判定できるものや、組み込みオブジェクトのメンバーで型判定できるときは、積極的に使いましょう。

function myFunction8_58_04() {

  const string = 'Tom';
  console.log(typeof string === 'string'); // => true

  const bool = false;
  console.log(typeof bool === 'boolean'); // => true

  const number = 10;
  console.log(Number.isInteger(number)); // => true

  const array = [];
  console.log(Array.isArray(array)); // => true

  const date = new Date();
  console.log(date instanceof Date); // => true

  //Objectの判定はinstanceof演算子では行わない
  const object = [];
  console.log(object instanceof Object); // => true

}

項目52の復習ですが、もしES5の環境でないばあいなどは、call()メソッドでも判定できます。

function myFunction8_58_05() {

  console.log(Object.prototype.toString.call({})); // => [object Object]
  console.log(Object.prototype.toString.call(new Date())); // => [object Date]
  console.log(Object.prototype.toString.call('a')); // => [object String]
  console.log(Object.prototype.toString.call(/.*/)); // => [object RegExp]
  console.log(Object.prototype.toString.call([])); // => [object Array]
  console.log(Object.prototype.toString.call(function () { })); // => [object Function]
  console.log(Object.prototype.toString.call(JSON)); // =>  [object JSON]
  console.log(Object.prototype.toString.call(SpreadsheetApp)); // => [object Object]

  class Test { }
  console.log(Object.prototype.toString.call(new Test())); // => [object Object]

}

ユーザー定義型

C++の設計者であるBjarne Stroustrup氏は、「数値型とか文字列型などのデータ型だけが型じゃなくて、データをどう処理するか決めたものも型って言えるんじゃない?」 という考え方をプログラミング言語に取り入れました。

あらかじめ決められたデータ型に対して、いわば、ユーザーが独自に定義する、ユーザー定義型といったところです。

そして、このユーザー定義型に、クラスという名前を付けました。

function myFunction8_58_06(){

  /**
   * Personクラス
   */
  class Person {
    constructor() {
      this.name = 'Tom';
      this.age = 30;
    }

    /**
      * 年齢を倍にして返すメソッド
      * @return {number} 倍の年齢
      */
    getDoubleTomsAge() { return this.age * 2; }

  }

  const p = new Person();
  console.log(p, p.getDoubleTomsAge()); //	{ name: 'Tom', age: 30 } , 60

}

ユーザー定義型の判定

では、なにをもってユーザー定義型と言えるのでしょうか。

それは、プロパティの有無です。

JavaScriptのような動的型付けの言語では、さまざまなデータ型を格納できるため、最終的なデータ型は、プログラムを実行してみないとわかりません

しかし、バグの早期発見や、エラー検出のため、型は早めに判定できたほうが便利です。

なので、データ型ではなく、クラスがもつプロパティで、型を判定しよう、という考え方が**ダックタイピング(または、ダックテスティング)**です。

in演算子は、プロパティが含まれるかどうか、プロトタイプチェーンをさかのぼって参照する演算子です。

クラス動詞でプロパティの重複があるばあいは、完璧に判定できませんが、型を判定する助けになるでしょう。

function myFunction8_58_07(){

  /**
   * Personクラス
   */
  class Person {
    constructor() {
      this.name = 'Tom';
      this.age = 30;
    }

    /**
      * 年齢を倍にして返すメソッド
      * @return {number} 倍の年齢
      */
    getDoubleTomsAge() { return this.age * 2; }

  }

  const p = new Person();

  //in演算子による型判定
  console.log('getDoubleTomsAge' in p); //true

}

まとめ

以上で、「配列と「配列のようなもの」を区別しよう」をお届けしました。

型のお話については、オブジェクト指向のシリーズで、詳しくお届けします。

ダックテスティングは、非推奨とされていますが、その理由は、別のブログで考察したいと思います。

次回は、最終回で 「過剰な型強制を防ごう」 をお届けします。

参考資料

このシリーズの目次

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