JavaScript の便利な書き方

目次

はじめに

基本的に、cybozu developer network の API ドキュメントのサンプルコードやチュートリアルなどの JavaScript は、ECMA Script6 以降のバージョンで書かれています。
今回は ECMA Script5 からの変更点や、kintone の JavaScript カスタマイズでの使い所を紹介します。

変数宣言を const / let にして安全に初期化する

var abc = 123; のように、ECMA Script5 以前での変数宣言では var を使っていましたが、今後は constlet を使っていくことをおすすめします。

  • var は関数スコープで、constlet はブロックスコープです。

    スコープとは変数の名前や関数などの参照できる範囲を決めるものです。
    スコープの中で定義された変数はスコープの内側でのみ参照でき、スコープの外側からは参照できません。
    var はスコープが広く、関数スコープまでです。
    constlet はブロックスコープといって、if ブロックなど {} の中の範囲で有効になります。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    // 関数内で変数宣言する
    function someFunc() {
      var var_func_number = 1000;
      const const_func_number = 2000;
      let let_func_number = 3000;
    }
    
    // typeofで宣言済みか調べる(宣言済みの場合trueと表示される)
    // 下記はすべて関数の外から参照できないので false と表示される
    console.log('var: ', typeof var_func_number !== 'undefined');
    console.log('const: ', typeof const_func_number !== 'undefined');
    console.log('let: ', typeof let_func_number !== 'undefined');
    
    // ifブロック内で変数宣言する
    if (true) {
      var var_block_number = 1000;
      const const_block_price = 2000;
      let let_block_price = 3000;
    }
    
    // typeofで宣言済みか調べる(宣言済みの場合trueと表示される)
    // varで宣言するとブロック外からも参照できてしまう
    console.log('var: ', typeof var_block_number !== 'undefined');
    // const / letで宣言したものはブロック外から参照できない
    console.log('const: ', typeof const_block_number !== 'undefined');
    console.log('let: ', typeof let_block_number !== 'undefined');
  • const は再代入と再宣言ができません。

    この変数は代入し直さない、とわかっている場合は const を使いましょう。
    長いコードになってくると、同じ変数名を間違って再度宣言したり、意図せず再利用してしまうこともありますが、積極的に const を使うとそれを防げます。
    ただし、Object や配列の中身は書き換えることができますのでご注意ください。

    1
    2
    3
    4
    5
    6
    7
    
    var var_price = 1000;
    var_price = 2000; // varで宣言した変数は書き換えてもエラーにならない
    var var_price = 3000; // 再宣言もできてしまう
    
    const const_price = 1000;
    const_price = 2000; // constで宣言した変数は書き換えるとエラーになる
    const const_price = 3000; // constは再宣言もできない
    
  • let は再代入が可能です。(const と同様再宣言は不可)

    letconst 同様に再宣言できませんが、再代入はできます。
    値を途中で書き換えることがわかっている場合は let で変数を宣言しましょう。
    基本的には const を使い、明らかに再代入する場合は let を使う、という風に使い分けていくといいと思います。

    1
    2
    3
    4
    5
    6
    7
    
    var var_price = 1000;
    var_price = 2000; // varで宣言した変数は書き換えてもエラーにならない
    var var_price = 3000; // 再宣言もできてしまう
    
    let let_price = 1000;
    let_price = 2000; // letで宣言した変数は再代入が可能
    let let_price = 3000; // letは再宣言できない
    

for 文の使用を必要最低限にする

たとえばレコード配列のデータなどを繰り返し処理をするケースは多々ありますが、それを for でやると無限ループさせてしまったり、記述が長くなってしまったりしてしまいます。
そのため、配列を扱うときはなるべく次に紹介する forEach() / filter() / map() / reduce() を使ってみるようにしましょう。

forEach()

単純にすべてのレコードのデータを参照したい場合は forEach を使うのが適しています。
詳細は次の記事を参考にしてください。
Array.prototype.forEach() (External link)

例)会社名一覧を console.log に表示
  • for で記述した場合

    弱点として、レコードの数の指定を間違えると無限ループに陥ってしまいますし、records[i] と書く必要があり i が何なのか常に意識せざるをえません。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    const records = [
      {
        $id: {
          value: 1,
          type: 'RECORD_NUMBER',
        },
        '会社名': {
          value: 'ABC株式会社',
          type: 'SINGLE_LINE_TEXT',
        },
      },
      {
        $id: {
          value: 2,
          type: 'RECORD_NUMBER',
        },
        '会社名': {
          value: 'サンプル株式会社',
          type: 'SINGLE_LINE_TEXT',
        },
      },
      {
        $id: {
          value: 3,
          type: 'RECORD_NUMBER',
        },
        '会社名': {
          value: '株式会社XYZ',
          type: 'SINGLE_LINE_TEXT',
        },
      },
    ];
    
    for (let i = 0; i < records.length; i++) {
      console.log(records[i]['会社名'].value); // 結果表示
    }
  • forEach で記述した場合

    わざわざ、何行目かを意識せずとも、record 変数に records のデータが 1 つずつ入って繰り返し処理を行ってくれますので見通しもよく便利です。
    今まで for でやっていることは、たいていこのように実装できるはずですので、まず forEach で実装できないか考えてみましょう。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    const records = [
      {
        $id: {
          value: 1,
          type: 'RECORD_NUMBER',
        },
        '会社名': {
          value: 'ABC株式会社',
          type: 'SINGLE_LINE_TEXT',
        },
      },
      {
        $id: {
          value: 2,
          type: 'RECORD_NUMBER',
        },
        '会社名': {
          value: 'サンプル株式会社',
          type: 'SINGLE_LINE_TEXT',
        },
      },
      {
        $id: {
          value: 3,
          type: 'RECORD_NUMBER',
        },
        '会社名': {
          value: '株式会社XYZ',
          type: 'SINGLE_LINE_TEXT',
        },
      },
    ];
    
    records.forEach(function(record) {
      console.log(record['会社名'].value); // 結果表示
    });

filter()

filter を使えば条件に一致した配列を取得できます。
たとえば、売上 n 円以上のデータだけが欲しい、ということを簡単に書くことができます。
詳細は次の記事を参考にしてください。
Array.prototype.filter() (External link)

例)小計が 10,000 円以上のレコードのみを求める
  • for で記述した場合

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    
    const records = [
      {
        $id: {
          value: 1,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '1000',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 2,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '20000',
          type: 'NUMBER',
    
        },
      },
      {
        $id: {
          value: 3,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '30000',
          type: 'NUMBER',
        },
      },
    ];
    
    let results = [];
    
    for (let i = 0; i < records.length; i++) {
      if (records[i]['小計'].value > 10000) {
        results.push(records[i]);
      }
    }
    console.log(results); // 結果表示
    
  • filter で記述した場合

    if 文を使わずとも任意のデータが取得できるので見た目もすっきりします。
    実際には、この程度の条件なら kintone API を呼ぶときに指定すればいいですが、もうちょっと複雑な条件を指定したいときには重宝します。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    const records = [
      {
        $id: {
          value: 1,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '1000',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 2,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '20000',
          type: 'NUMBER',
    
        },
      },
      {
        $id: {
          value: 3,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '30000',
          type: 'NUMBER',
        },
      },
    ];
    
    const results = records.filter(function(record) {
      return (record['小計'].value > 10000);
    });
    console.log(results); // 結果表示
    

map

map を使えば、配列すべてに処理を行えます(新しい配列が返却されます)。
詳細は次の記事を参考にしてください。
Array.prototype.map() (External link)

例)小計を求める(消費税込)
  • for で記述した場合

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    
    const records = [
      {
        $id: {
          value: 1,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '0',
          type: 'NUMBER',
        },
        '単価': {
          value: '1000',
          type: 'NUMBER',
        },
        'ユーザー数': {
          value: '2',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 2,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '0',
          type: 'NUMBER',
        },
        '単価': {
          value: '3000',
          type: 'NUMBER',
        },
        'ユーザー数': {
          value: '4',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 3,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '0',
          type: 'NUMBER',
        },
        '単価': {
          value: '20000',
          type: 'NUMBER',
        },
        'ユーザー数': {
          value: '8',
          type: 'NUMBER',
        },
      },
    ];
    
    let results = [];
    for (let i = 0; i < records.length; i++) {
      results.push(records[i]['小計'].value = records[i]['単価'].value * records[i]['ユーザー数'].value * 1.08);
    }
  • map で記述した場合

    これもタイプ数は大分減らせることができます。
    records[i] から始まらない分、短くてすみますし、バグを引き起こす可能性を低くできます。
    実際には、小計を出す程度だと自動計算フォームを使えばいいですが、それではできない複雑な計算をする場合に使えます。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    
    const records = [
      {
        $id: {
          value: 1,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '0',
          type: 'NUMBER',
        },
        '単価': {
          value: '1000',
          type: 'NUMBER',
        },
        'ユーザー数': {
          value: '2',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 2,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '0',
          type: 'NUMBER',
        },
        '単価': {
          value: '3000',
          type: 'NUMBER',
        },
        'ユーザー数': {
          value: '4',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 3,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '0',
          type: 'NUMBER',
        },
        '単価': {
          value: '20000',
          type: 'NUMBER',
        },
        'ユーザー数': {
          value: '8',
          type: 'NUMBER',
        },
      },
    ];
    
    const results = records.map(function(record) {
      record['小計'].value = record['単価'].value * record['ユーザー数'].value * 1.08;
      return record;
    });
    console.log(results); // 結果出力
    

reduce

reduce を使うと records 配列のすべての合計値などを求めることができます。
reduce は上記に挙げたものと比べると少し難しく見えるかもしれませんが、慣れてしまえば簡単にデータの合計値などを求めることができます。
reduce は左から右に配列を処理しますが、逆の reduceRight もあります。

Array.prototype.reduce() (External link)

例)すべての小計の合計を求める
  • for で記述した場合

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    const records = [
      {
        $id: {
          value: 1,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '10000',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 2,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '20000',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 3,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '30000',
          type: 'NUMBER',
        },
      },
    ];
    
    var results = 0;
    for (var i = 0; i < records.length; i++) {
      results += Number(records[i]['小計'].value);
    }
    console.log(results); // 結果出力
    
  • reduce で記述した場合

    タイプ数はほとんど変わりませんので違いがわかりにくいですが、やはり index 変数 i を使わない分バグの確率は減ります。
    forEach でも実装はできますが、こちらは results の初期化も不要ですし、reduce 内で処理が完結するのでまとまりがあります。
    例のように合計値を求めたり、records 配列を loop してデータを計算したいときは積極的につかっていきたい関数です。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
    const records = [
      {
        $id: {
          value: 1,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '10000',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 2,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '20000',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 3,
          type: 'RECORD_NUMBER',
        },
        '小計': {
          value: '30000',
          type: 'NUMBER',
        },
      },
    ];
    
    var results = records.reduce(function(prev, record) {
      return prev + Number(record['小計'].value);
    }, 0);
    console.log(results); // 結果出力
    

Arrow 関数を使ってシンプルに関数を記述する

関数の宣言には次の種類があります。

  • 今までの関数宣言(通常の function 式)

    1
    2
    3
    4
    5
    6
    7
    8
    
    function someFunc() {
      // 処理内容
    };
    
    // もしくは
    var someFunc = function() {
      // 処理内容  
    }
  • Arrow 関数

    1
    2
    3
    
    const someFunc = () => {  
      // 処理内容
    };

ES6 からは上記 Arrow 関数が使えるようになりました。
=> この形が矢印に見えるので Arrow 関数と呼ばれています。

Arrow 関数のメリット

Arrow 関数にはいくつかメリットがあります。

  • 短くかける。

    上記のように関数宣言しているだけだと、ほとんど差はありません。
    ですが、 Callback 関数など関数を多様するときにかなりスマートにかけます。
    とあるレコード一覧のデータを Array.map()filter() を使って必要なものだけとりだし、それぞれ処理をするパターンをみていきましょう。

    Array.prototype.filter() 関数で税込にするべきレコードか判別し、その後 Array.prototype.map() で税込にした金額を配列で取得する場合を考えてみます。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    
    const records = [
      {
        $id: {
          value: 1,
          type: 'RECORD_NUMBER',
        },
        '税種別': {
          value: '税込',
          type: 'RADIO_BUTTON',
        },
        '小計': {
          value: '10000',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 2,
          type: 'RECORD_NUMBER',
        },
        '税種別': {
          value: '税抜',
          type: 'RADIO_BUTTON',
        },
        '小計': {
          value: '20000',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 3,
          type: 'RECORD_NUMBER',
        },
        '税種別': {
          value: '税込',
          type: 'RADIO_BUTTON',
        },
        '小計': {
          value: '30000',
          type: 'NUMBER',
        },
      },
    ];
    
    // 税種別に[税込]か[税抜]が入り
    // 小計フィールドに金額が入るものとします。
    
    // 今までの関数宣言の場合
    const includedTaxPriceArray = records.filter(function(record) {
      return record['税種別'].value === '税込';
    }).map(function(record) {
      return record['小計'].value * 1.1;
    });
    
    // Arrow関数の場合
    const includedTaxPriceArrayWithArrowFunc = records.filter((record) => {
      return record['税種別'].value === '税込';
    }).map((record) => {
      return record['小計'].value * 1.1;
    });
    
    console.log(includedTaxPriceArray);
    console.log(includedTaxPriceArrayWithArrowFunc)

    このように function がなくなっただけでもスッキリしますね。
    また、さらにルールがあって、引数が 1 つなら引数の括弧が省略できる、1 行で結果を返せるなら return を省略できるというルールがあり、それを適用すると次のようにもっと短くかけます。

    1
    2
    
    // Arrow 関数(括弧 Return 省略版)  
    const includedTaxPriceArrayWithArrowFunc = records.filterrecord => record['税種別'].value === '税込'.maprecord => record.price.value * 1.1;

    この書き方を利用して書き換えた場合は、次のようになります。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    
    const records = [
      {
        $id: {
          value: 1,
          type: 'RECORD_NUMBER',
        },
        '税種別': {
          value: '税込',
          type: 'RADIO_BUTTON',
        },
        '小計': {
          value: '10000',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 2,
          type: 'RECORD_NUMBER',
        },
        '税種別': {
          value: '税抜',
          type: 'RADIO_BUTTON',
        },
        '小計': {
          value: '20000',
          type: 'NUMBER',
        },
      },
      {
        $id: {
          value: 3,
          type: 'RECORD_NUMBER',
        },
        '税種別': {
          value: '税込',
          type: 'RADIO_BUTTON',
        },
        '小計': {
          value: '30000',
          type: 'NUMBER',
        },
      },
    ];
    
    // 税種別フィールドに[税込]か[税抜]が入り
    // 小計フィールドに金額が入るものとします。
    
    
    // Arrow関数の場合
    const includedTaxPriceArrayWithArrowFunc = records.filter(record => record['税種別'].value === '税込').map(record => record['小計'].value * 1.1);
    
    console.log(includedTaxPriceArrayWithArrowFunc);

    return がいらない分、関数を作る関数(カリー化)を作りやすくなります。
    ... spread opereator などは後述で説明します。

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    
    const contracts = [
      {
        '契約名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': '契約C'
        },
        '料金': {
          'type': 'NUMBER',
          'value': '50000'
        },
        '商品ID': {
          'type': 'NUMBER',
          'value': '1'
        },
        '顧客ID': {
          'type': 'NUMBER',
          'value': '3'
        },
        '日付': {
          'type': 'DATE',
          'value': '2019-08-31'
        },
        '$id': {
          'type': '__ID__',
          'value': '3'
        }
      },
      {
        '契約名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': '契約B'
        },
        '料金': {
          'type': 'NUMBER',
          'value': '150000'
        },
        '商品ID': {
          'type': 'NUMBER',
          'value': '2'
        },
        '顧客ID': {
          'type': 'NUMBER',
          'value': '4'
        },
        '日付': {
          'type': 'DATE',
          'value': '2019-08-30'
        },
        '$id': {
          'type': '__ID__',
          'value': '2'
        }
      },
      {
        'レコード番号': {
          'type': 'RECORD_NUMBER',
          'value': '1'
        },
        '契約名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': '契約A'
        },
        '料金': {
          'type': 'NUMBER',
          'value': '100000'
        },
        '商品ID': {
          'type': 'NUMBER',
          'value': '3'
        },
        '顧客ID': {
          'type': 'NUMBER',
          'value': '1'
        },
        '日付': {
          'type': 'DATE',
          'value': '2019-08-29'
        },
        '$id': {
          'type': '__ID__',
          'value': '1'
        }
      }
    ];
    
    const customers = [
      {
        '顧客名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': 'サンプルカンパニー'
        },
        '$id': {
          'type': '__ID__',
          'value': '5'
        }
      },
      {
        '顧客名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': 'まぐろ水産'
        },
        '$id': {
          'type': '__ID__',
          'value': '4'
        }
      },
      {
        '顧客名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': 'サンプル株式会社'
        },
        '$id': {
          'type': '__ID__',
          'value': '3'
        }
      },
      {
        '顧客名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': '町田商事'
        },
        '$id': {
          'type': '__ID__',
          'value': '2'
        }
      },
      {
        '顧客名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': '金都運総研'
        },
        '$id': {
          'type': '__ID__',
          'value': '1'
        }
      }
    ];
    
    const products = [
      {
        '商品名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': 'パソコン'
        },
        '$id': {
          'type': '__ID__',
          'value': '3'
        }
      },
      {
        '商品名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': '業務用冷蔵庫'
        },
        '$id': {
          'type': '__ID__',
          'value': '2'
        }
      },
      {
        '商品名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': '大型テレビ'
        },
        '$id': {
          'type': '__ID__',
          'value': '1'
        }
      }
    ];
    
    // それぞれ、契約アプリのレコード、それに紐付いた顧客アプリのレコード・商品アプリのレコードがあるとします。
    
    // ある複数のレコードを結合させるための関数を返す関数を作成
    const joinRecordsCreator =
      (leftRecords) =>
      (rightRecords, joinKey) =>
      leftRecords.map(leftRecord => ({...rightRecords.find((rightRecord) => rightRecord[joinKey].value === leftRecord.$id.value), ...leftRecord}))
    
    // 上記からContractsを他のアプリのレコードに結合させる関数を作成
    const joinContracts = joinRecordsCreator(contracts);
    
    // joinContracts関数を使ってContractsとCustomersを結合した配列を取得
    const contractsJoinedWithCustomers = joinContracts(customers, '$id');
    
    // joinContracts関数を使ってContractsとProductsを結合した配列を取得
    const contractsJoinedWithProducts = joinContracts(products, '$id');
    
    console.log(contractsJoinedWithCustomers);
    console.log(contractsJoinedWithProducts);
  • this を固定できる。

    kintone の JS カスタマイズでは DOM を操作するときに this を参照するケースはあると思います。
    通常の関数宣言の場合「this は何を指しているか?」を意識する必要がありましたが、arrow 関数の場合 this が固定されるのでその混乱はなくなります。
    その代わり currentTarget などをつかって押された要素自体を取得するなどができます。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    // 通常の関数の場合、thisはボタン自身になる
    $('#button1').click(function() {
      console.dir(this); // button要素を示す。
    });
    
    // アロー関数の場合、thisは固定されているためbutton要素ではない。
    // その代わり、コールバックの引数のcurrentTargetを使う
    $('#button2').click((e) => {
      console.dir(e.currentTarget); // button要素を示す。
    });

Promise の代わりに Async/Await を使う

複数のアプリのレコードからデータを取得したい場合などに、Promise による非同期処理をしなければいけませんが、
Promise は .then() に次の処理を書く都合上、入れ子構造になり慣れていないとあまり直感的とはいえません。
下記あるアプリのレコードを 4 件取得して合計値を求めるサンプルで試したいと思います。

  • 例)4 件のデータを取得し合計値を求める。(Promise)

    レコード一括取得 API を使うほうが適切ですが Promise のサンプルのためにあえて 1 件ずつ取得します。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    (function() {
      'use strict';
      // 4件のデータを取得し合計値を求めることを想定
      // params1-4は省略します。
    
      // kintoneはPromiseを返すことで保存前処理など、処理をまってくれる
      kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], function(event) {
        return new kintone.Promise(function(resolve, reject) {
          return kintone.api('/k/v1/record', 'GET', params1);
        }).then(function(resp1) { // レスポンス内容がresp1に入る。ちゃんと待ってから次の処理に移る
          return kintone.api('/k/v1/record', 'GET', params2);
        }).then(function(resp2) { // レスポンス内容がresp2に入る。ちゃんと待ってから次の処理に移る
          return kintone.api('/k/v1/record', 'GET', params3);
        }).then(function(resp3) { // レスポンス内容がresp3に入る。ちゃんと待ってから次の処理に移る
          return kintone.api('/k/v1/record', 'GET', params4);
        }).then(function(resp4) { // レスポンス内容がresp4に入る。ちゃんと待ってから次の処理に移る
    
          // ここにくるまでに全てのデータの取得を完了しているので計算できる
          // resp1 - 4 までの合計をたして「合計フィールド」にセット
          event.record.合計.value = resp1.record.小計.value + resp2.record.小計.value + resp3.record.小計.value + resp4.record.小計.value;
          resolve(event); // resolveでプロミスの処理が終了したことを伝える
        });
      });
    })();

    そこで、Promise をそのままつかうのではなく、Async/Await を用いることで次のように普通の関数を使うようにかけるようになりました。
    どちらが正しい、とかではないのですが、複数の Promise だったり複雑だったりすると Async/Await の書き方のほうが見通しがよいかと思います。

  • 例)4 件のデータを取得し合計値を求める(Async/Await)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    (() => {
      'use strict';
      // 4件のデータを取得し合計値を求めることを想定
      // params1-4は省略します。
    
      kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], async function(event) { // async をつける
        // 待ちたい処理にawaitをつける
        const resp1 = await kintone.api('/k/v1/record', 'GET', params1); // レスポンス内容がresp1に入る。ちゃんと待ってから次の処理に移る
        const resp2 = await kintone.api('/k/v1/record', 'GET', params2); // レスポンス内容がresp2に入る。ちゃんと待ってから次の処理に移る
        const resp3 = await kintone.api('/k/v1/record', 'GET', params3); // レスポンス内容がresp3に入る。ちゃんと待ってから次の処理に移る
        const resp4 = await kintone.api('/k/v1/record', 'GET', params4); // レスポンス内容がresp4に入る。ちゃんと待ってから次の処理に移る
    
        // ここにくるまでに全てのデータの取得を完了しているので計算できる
        // resp1 - 4 までの合計をたして「合計フィールド」にセット
        event.record.合計.value = resp1.record.小計.value + resp2.record.小計.value + resp3.record.小計.value + resp4.record.小計.value;
        return event;
      });
    })();

    ただし、注意点としては、async をつけることで PromiseObject が返却されてしまうので、Promise に対応していないイベントハンドラーでは即時実行関数で包むなどしてあげないとエラーが発生してしまいます。

分割代入でスマートに変数を取り出す

これも便利な機能です。
通常、次のように kintone.events.on() の中で record 変数を取り出すときはこうします。

  • kintone.events.on() の中で event.record から record 変数を取り出す。(通常)

    1
    2
    3
    4
    
    kintone.events.on('app.record.create.submit', (event) => {
      const record = event.record;
      console.log(record);
    });

    分割代入を使うと次のように 2 回 record と書かなくて良くなります。

  • kintone.events.on() の中で event.record から record 変数を取り出す。(分割代入)

    1
    2
    3
    4
    
    kintone.events.on('app.record.create.submit', (event) => {
      const {record} = event;
      console.log(record);
    });

    このように、わざわざ record と 2 回書く手間が省けます。
    さらに潜って recordpricecustomer フィールドを取りたい場合はこのようにします。

    1
    2
    3
    4
    5
    
    kintone.events.on('app.record.create.submit', (event) => {
      const {record} = event;
      const {price, customer} = record;
      console.log(price, customer);
    });

    先に pricecustomer をとってしまうことも可能です。

    1
    2
    3
    4
    
    kintone.events.on('app.record.create.submit', (event) => {
      const {record: {price, customer}} = event;
      console.log(price, customer);
    });

    必須な書き方ではありませんが値取り出すために数行かかってしまうと見通しが悪くなってしまうこともあるので、スマートに取りたい場合は有効です。

スプレッド構文を使って展開する

スプレッド構文も使いこなせば便利です。
スプレッド構文は 3 つのピリオド ... を使い、配列・関数の引数・オブジェクトを展開できます。

配列で使うスプレッド構文

  • kintone のレコードの配列 records 配列を展開し結合します。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    
    const records1 = [
      {
        '契約名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': '契約C'
        },
        '料金': {
          'type': 'NUMBER',
          'value': '50000'
        },
        '日付': {
          'type': 'DATE',
          'value': '2019-08-31'
        },
        '$id': {
          'type': '__ID__',
          'value': '3'
        }
      },
      {
        '契約名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': '契約B'
        },
        '料金': {
          'type': 'NUMBER',
          'value': '150000'
        },
        '日付': {
          'type': 'DATE',
          'value': '2019-08-30'
        },
        '$id': {
          'type': '__ID__',
          'value': '2'
        }
      },
    ];
    
    const records2 = [
      {
        '契約名': {
          'type': 'SINGLE_LINE_TEXT',
          'value': '契約A'
        },
        '料金': {
          'type': 'NUMBER',
          'value': '100000'
        },
        '日付': {
          'type': 'DATE',
          'value': '2019-08-29'
        },
        '$id': {
          'type': '__ID__',
          'value': '1'
        }
      }
    ];
    
    // レコード配列 records1, records2を結合して一つの配列にする
    const resultRecords = [...records1, ...records2];
    
    console.log(resultRecords);

    結合は Array.prototype.concat() でもできますが、こちらのほうが直感的でしょう。
    records1records2 でそれぞれ 3 つ要素があるとすれば、[...records1, ...records2] とすることで、次の記載と同義になります。

    1
    
    [records1-1, records1-2, records1-3, records2-1, records2-2, records2-3]

    ... で配列を一度展開していると思えばわかりやすいかと思います。

関数の引数で使うスプレッド構文

関数の引数に使えば可変数の引数に対応できます。

  • 複数の契約レコードの合計金額を集計する関数

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    
    const record1 = {
      '契約名': {
        'type': 'SINGLE_LINE_TEXT',
        'value': '契約C'
      },
      '料金': {
        'type': 'NUMBER',
        'value': '50000'
      },
      '日付': {
        'type': 'DATE',
        'value': '2019-08-31'
      },
      '$id': {
        'type': '__ID__',
        'value': '3'
      }
    };
    
    const record2 = {
      '契約名': {
        'type': 'SINGLE_LINE_TEXT',
        'value': '契約B'
      },
      '料金': {
        'type': 'NUMBER',
        'value': '150000'
      },
      '日付': {
        'type': 'DATE',
        'value': '2019-08-30'
      },
      '$id': {
        'type': '__ID__',
        'value': '2'
      }
    };
    
    
    const record3 = {
      '契約名': {
        'type': 'SINGLE_LINE_TEXT',
        'value': '契約A'
      },
      '料金': {
        'type': 'NUMBER',
        'value': '100000'
      },
      '日付': {
        'type': 'DATE',
        'value': '2019-08-29'
      },
      '$id': {
        'type': '__ID__',
        'value': '1'
      }
    };
    
    
    const sumContractPriceRecords = (...records) => {
      // 引数recordsは配列となる
      return records.reduce((sum, record) => sum + Number(record.料金.value), 0);
    };
    
    // record1,2,3のそれぞれの小計の合計をだす
    const sum = sumContractPriceRecords(record1, record2, record3);
    
    console.log(sum);

オブジェクトで使うスプレッド構文

オブジェクトも配列のように展開がされますので、次の用にオブジェクト同士の結合ができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const contract = {
  '契約名': {
    'type': 'SINGLE_LINE_TEXT',
    'value': '契約C'
  },
  '料金': {
    'type': 'NUMBER',
    'value': '50000'
  },
  '日付': {
    'type': 'DATE',
    'value': '2019-08-31'
  },
  '$id': {
    'type': '__ID__',
    'value': '3'
  }
};


const customer = {
  '顧客名': {
    'type': 'SINGLE_LINE_TEXT',
    'value': 'サンプルカンパニー'
  },
  '$id': {
    'type': '__ID__',
    'value': '5'
  }
};

// 顧客情報をもつ customerRecord と契約情報をもつ contractRecord を結合する
// このとき、$idなどかぶる物がある場合後ろの方が優先される
const joinedRecord = {...contract, ...customer};


console.log(joinedRecord);

これも、配列と同様に contract レコードと customer レコードが一度展開されているイメージです。

副作用をなくして安全にコードを書く

プログラミングで、状態を変化させることを副作用と呼びますが、副作用を起こすコードはなるべく少ないほうがバグを回避できます。
言葉だけではわかりにくいので、起こりうるバグを見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// priceフィールドがあるkintoneのrecord
const record = {
  $id: {
    type: 'RECORD_NUMBER',
    value: 1,
  },
  price: {
    type: 'NUMBER',
    value: 1000,
  },
};

// 消費税(10%)を足すための関数
const addTax = (record) => {
  record.price.value = record.price.value * 1.1;
  return record;
};

// 消費税を計算したrecordを保持
const addedTaxRecord = addTax(record);

// 消費税を計算したrecordの数値と、計算する前のrecordの数値を比較するとどうなるか?
console.log('税込金額', addedTaxRecord.price.value);
console.log('税抜金額', record.price.value);

直感的には、上記のコードはコンソールに、 税込金額(1100)と税抜金額(1000)を表示してくれそうです。
しかし実際には次のように表示されます。

なんと、両方とも税込金額(1100)になってしまっていますね。
これは「もとの変数に対して副作用が起きている」状態となります。
このように、副作用がいつ起こるかというのを意識していなければ、思わぬバグを生んでしまいます。

JavaScript では常に変数は値への参照なので起きてしまう現象です。

これを回避するために、SpreadOperator で次のように addTax 関数を書き換えましょう。
新しい Object を作り、返すようなイメージです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// priceフィールドがあるkintoneのrecord
const record = {
  $id: {
    type: 'RECORD_NUMBER',
    value: 1,
  },
  price: {
    type: 'NUMBER',
    value: 1000,
  },
};

// 消費税(10%)を足すための関数(副作用を起こさないバージョン)
const addTax = (record) => {
  return {
    ...record, // record配列を展開
    price: {   // priceは上書き
      ...record.price,  // typeなどは残したいのでpriceの中身も展開
      value: record.price.value * 1.1, // valueのみ上書き
    },
  };
};
// 消費税を計算したrecordを保持
const addedTaxRecord = addTax(record);

// 消費税を計算したrecordの数値と、計算する前のrecordの数値を比較するとどうなるか?
console.log('税込金額', addedTaxRecord.price.value);
console.log('税抜金額', record.price.value);

jQuery を使わず素の JS で DOM を操作する

もちろん込み入った DOM 操作をしたい場合 jQuery は有用です。
ですが 1 つの DOM を操作したい場合に、わざわざ jQuery を読み込むのは冗長なこともあります。
素の JS でも DOM を操作しやすくなっているので紹介します。

  • 要素を取得する。
    document.querySelector() / querySelectorAll() を利用することで要素を取得できます。

    html は次のとおりです。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    <div id="foo">
    foo
    </div>
    
    <div id="bar">
    bar
    </div>
    
    <div class="foo">
    foo class1
    </div>
    
    <div class="foo">
    foo class2
    </div>
    
    <div class="foo">
    foo class3
    </div>

    javascript は次のとおりです。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    /* 要素id = fooの要素を取得する場合 */
    
    // jQuery
    const elementJquery = $('#foo');
    
    // JavaScript
    const element = document.querySelector('#foo');
    
    // Classなど複数の要素を取得したい場合はquerySelectorAll
    const elements = document.querySelectorAll('.foo');
    
    
    console.log('取得した要素のtext: ', elementJquery.text());
    
    console.log('取得した要素のtext: ', element.innerText);
    
    console.log('取得した要素のtext: ', [...elements].map(e => e.innerText));

    document.getElementById() などもありますが、上記のように class/id に縛られない querySelector のほうが柔軟に要素を指定できます。

  • 取得した要素の Style を変更する。
    要素 .style.プロパティ = "値"; とすることで、要素に対して style を変更できます。
    CSS で設定できる style を割り当てられます。

    1
    2
    3
    4
    
    // id: fooの要素の文字を、赤い太文字にする
    const element = document.querySelector('#foo');
    element.style.color = `red`;
    element.style.fontWeight = `bold`;

    もちろん kintone.app.record.getFieldElement などで取得した要素にも同様のことが可能です。

TypeScript を導入する

TypeScript で kintone カスタマイズ開発をしてみよう の記事で紹介されています。
kintone のサブテーブルの Record 構造などは結構難しいので、TypeScript を使うと、オブジェクトのキーの指定や型の間違いなどをなくすことができます。
Visual Studio Code などの IDE を使えば補完しながらコードを書くこともできます。
がっつり JavaScript カスタマイズをしたい場合は導入したほうがよいです。

おわりに

JavaScript はよりプログラミングしやすくなるように進化していっています。
constlet、Spread 構文や Arrow 関数などを組み合わせたり新しい機能を使うと、バグを減らし副作用のない堅牢なコードも書けるようになると思います。
少しずつでもぜひ試してみてください。