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関数などを組み合わせたり新しい機能を使うと、バグを減らし副作用のない堅牢なコードも書けるようになると思います。
少しずつでもぜひ試してみてください。