[EffectiveJavaScript輪読会8]undefinedは「値なし」として扱う

GAS

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

前回のおさらい

前回は、「一貫した規約を維持しよう」と「メソッドチェーンをサポートしよう」を、2本お届けしました。

今日は2回目で、「undefinedは「値なし」として扱う」をお届けします。

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

今日のアジェンダ

  • undefinedとは
  • 値なし以外にundefinedを使う
  • undefinedを表すプロパティを使う
  • undefinedのテスト
  • デフォルト引数を使うときの真偽値

undefinedとは

JavaScriptのnullとundefinedは、どちらも「値がないこと」を表す特殊な値です。

この2つの値の違いの考察は、いつかチャレンジしてみたいと思いますが、まず、JavaScriptが、undefinedを返す状況を復習しましょう。

function myFunction8_54_01() {

  /** 初期化されていない変数 */
  let x;
  console.log(x); // => undefined

  /** 存在しないプロパティ */
  const obj = {};
  console.log(obj.property); // => undefined

  /** 値のない戻り値 */
  const f = () => { return; };
  console.log(f()); // => undefined

  /** 戻り値のない関数 */
  const g = () => { };
  console.log(g()); // => undefined

  /** 値が指定されなかった引数 */
  const h = (x) => { return x; };
  console.log(h()); // => undefined
}

本来、undefinedは、値がないことを表すべきです。

しかしながら、undefinedも「値」である以上、effectiveな使い方ができそうです。(逆に、注意喚起でもあります)

「値なし」以外にundefinedを使う

RangeオブジェクトのsetBackground()メソッドは、引数になにも渡されないばあいは、デフォルトの背景色が設定されます。(通常は無色)

function myFunction8_54_02() {

  /** クラスCustomSheet */
  class CustomSheet {

    /** カスタムシートのコンストラクタ
      * @constructor
      */
    constructor() {
      this.range = SpreadsheetApp.getActiveSheet().getRange('A1');
    }

    /** セルの背景色にハイライトをつけるメソッド */
    highlight(color) {
      this.range.setBackground(color);
    }

  }

  const element = new CustomSheet();

  element.highlight('red');
  element.highlight();

}

もし、特定のcolorではなく、ランダムな色を設定したい場合は、undefinedを使う発想もあります。

function myFunction8_54_03() {

  /** クラスCustomSheet */
  class CustomSheet {

    /** カスタムシートのコンストラクタ
      * @constructor
      */
    constructor() {
      this.range = SpreadsheetApp.getActiveSheet().getRange('A1');
    }

    /** セルの背景色にハイライトをつけるメソッド */
    highlight(color) {

      if (color !== undefined) {
        this.range.setBackground(color);
        return
      }

      const max = 3, min = 1;
      const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;

      switch (randomNum) {
        case 1:
          color = 'red';
          break;
        case 2:
          color = 'blue';
          break;
        case 3:
          color = 'yellow';
          break;
      }
    }
    
  }

  element.highlight(undefined);

}

しかし、存在しないプロパティなど、undefinedが渡されたときは、デフォルトのcolorを設定したばあい、破綻します。

const config = {};
element.highlight(config.highlightColor); // 存在しないプロパティ → 「デフォルト値」にしたいが、ランダムになってしまう

そのようなばあいは、「random」という引数を用意した方が、わかりやすいです。

function myFunction8_54_04() {

  /** クラスCustomSheet */
  class CustomSheet {

    /** カスタムシートのコンストラクタ
      * @constructor
      */
    constructor() {
      this.range = SpreadsheetApp.getActiveSheet().getRange('A1');
    }

    /** セルの背景色にハイライトをつけるメソッド */
    highlight(color) {

      if (color !== 'random') {
        this.range.setBackground(color);
        return
      }

      const max = 3, min = 1;
      const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;

      switch (randomNum) {
        case 1:
          color = 'red';
          break;
        case 2:
          color = 'blue';
          break;
        case 3:
          color = 'yellow';
          break;
      }

      this.range.setBackground(color);

    }

  }

  const element = new CustomSheet();

  element.highlight('random');
  element.highlight(undefined);

}

undefinedを表すプロパティを使う

いちばんいい方法は、randomプロパティを持つオブジェクトを引数にとることです。

function myFunction8_54_05() {


  /** クラスCustomSheet */
  class CustomSheet {

    /** カスタムシートのコンストラクタ
      * @constructor
      */
    constructor() {
      this.range = SpreadsheetApp.getActiveSheet().getRange('A1');
    }

    /** セルの背景色にハイライトをつけるメソッド */
    highlight(obj) {

      if (!obj.random) {
        this.range.setBackground(obj.color);
        return
      }

      const max = 3, min = 1;
      const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;

      switch (randomNum) {
        case 1:
          color = 'red';
          break;
        case 2:
          color = 'blue';
          break;
        case 3:
          color = 'yellow';
          break;
      }

      this.range.setBackground(color);

    }

  }

  const element = new CustomSheet();
  element.highlight({ random: true, color: '' });
  element.highlight({ random: false, color: 'purple' });
  element.highlight({ random: false });

}

undefinedのテスト

コンストラクタServerは、第2引数を取らないばあいは、デフォルト値として、’localhost’をもつようにしたいです。

「第2引数を取らない」という条件判定にたいし、argumentsオブジェクトのlengthで判定してみましょう。

しかし、存在しないプロパティによるundefinedが渡されたときに、破綻します。

function myFunction8_54_06() {

  /** クラスServer */
  class Server {

    constructor(port, hostname) {
      this.port = port;
      this.hostname = arguments.length < 2 ? 'localhost' : String(hostname);
    }

  }

  const s1 = new Server(80, 'example.com');
  console.log(s1); // => 	{ port: 80, hostname: 'example.com' }

  const s2 = new Server(80);
  console.log(s2); // => 	{ port: 80, hostname: 'localhost' }

  const config = {};
  const s3 = new Server(80, config.hostname);
  console.log(s3); // => 	{ port: 80, hostname: undefined }

}

これを解決するためには、undefinedを判定する必要があります。

V8から導入された、デフォルト引数を活用しましょう。

function myFunction8_54_07() {

  /** クラスServer */
  class Server {

    constructor(port, hostname = 'localhost') {
      this.port = port;
      this.hostname = String(hostname);
    }

  }

  const s1 = new Server(80, 'example.com');
  console.log(s1); // => 	{ port: 80, hostname: 'example.com' }

  const s2 = new Server(80);
  console.log(s2); // => 	{ port: 80, hostname: 'localhost' }

  const config = {};
  const s3 = new Server(80, config.hostname);
  console.log(s3); // => 	{ port: 80, hostname: 'localhost' }

}

論理演算子で判定する方法も、ご紹介しておきます。(前述のデフォルト引数の方がいいでしょう)

function myFunction8_54_08() {

  /** クラスServer */
  class Server {

    constructor(port, hostname) {
      this.port = port;
      this.hostname = String(hostname || 'localhost');
    }

  }

  const s1 = new Server(80, 'example.com');
  console.log(s1); // => 	{ port: 80, hostname: 'example.com' }

  const s2 = new Server(80);
  console.log(s2); // => 	{ port: 80, hostname: 'localhost' }

  const config = {};
  const s3 = new Server(80, config.hostname);
  console.log(s3); // => 	{ port: 80, hostname: 'localhost' }

}

デフォルト引数を使うときの真偽値

論理演算子でデフォルト引数を判定するばあいは、注意が必要です。

以下のような、思いがけないバグを生んでしまいます。

function myFunction8_54_09() {

  /** クラスElement */
  class Element {
    constructor(width, height) {
      this.width = width || 320;
      this.height = height || 240;
    }
  }

  const c1 = new Element(0, 0);
  console.log(c1.width); // => 320
  console.log(c1.height); // => 240

}

やはり、デフォルト引数が有効です。

function myFunction8_54_10() {

  /** クラスElement */
  class Element {
    constructor(width = 320, height = 240) {
      this.width = width;
      this.height = height;
    }
  }

  const c2 = new Element(0, 0);
  console.log(c2.width); // => 0
  console.log(c2.height); // => 0

}

まとめ

以上で、「undefinedは「値なし」として扱う」をお届けしました。

次回は、「オプションオブジェクトで、キーワード付き引数群を受け取ろう」 をお届けします。

参考資料

このシリーズの目次

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