4.4 配列と引数の渡し方

(1/1)
箱の中身を当てるゲームのイラスト(女性)
JavaScriptでは関数の引数として配列を渡すこともできる。

引数は、見た目の上では、Number型やString型のようなプリミティブ型の場合は参照渡しで、配列などオブジェクト型は値渡しで渡される。(JavaScriptの引数渡しは、他の言語の参照渡しや値渡しとは異なる。)

目次

サンプル・プログラム

平均を求める

合計点・平均点
関数の引数として配列を渡すこともできる。
例として、テストの平均点を求めるプログラム "average1.html" を作ってみよう。
左図がプログラムの実行例である。

average1.html

 115:     //点数表
 116:     let scores = [75, 68, 81, 90, 57, 52, 96, 72, 68, 72];

まず、メイン・プログラム側で、10人のテストの点数を配列 scores に代入しておく。
この配列をユーザー定義関数 dispScoreTable に渡し、平均点を求めるとともに、点数一覧表を作成する。

average1.html

  36: /**
  37:  * 点数表を作成し,画面に表示する.
  38:  * @param   Array scores 点数表
  39:  * @return  なし
  40: */
  41: function dispScoreTable(scores) {
  42:     //エラーメッセージをクリア
  43:     document.getElementById('error').innerHTML = '';
  44: 
  45:     //配列のバリデーション
  46:     if (! Array.isArray(scores)) {
  47:         //エラーを表示
  48:         document.getElementById('error').innerHTML = 'エラー:正しい点数表を用意してください.';
  49: 
  50:     //点数表を作成
  51:     } else {
  52:         //点数合計と要素数をカウント
  53:         let sum = 0;    //合計点数
  54:         for (let i = 0i < scores.lengthi++) {
  55:             let a = parseFloat(scores[i]);
  56:             //数字以外が含まれていたらNaNを代入して脱出
  57:             if (Number.isNaN(a)) {
  58:                 n = sum = NaN;
  59:                 return;
  60:             }
  61:             sum +a;
  62:         }
  63: 
  64:         //点数表の1行目
  65:         html =`
  66: <tr>
  67: <th>出席番号</th>
  68: <th>点数</th>
  69: </tr>
  70: `;
  71:         //計算結果のバリデーション
  72:         if (Number.isNaN(sum)) {
  73:             //エラーを表示
  74:             document.getElementById('error').innerHTML = 'エラー:正しい点数表を用意してください.';
  75: 
  76:         //点数表を作成
  77:         } else {
  78:             for (let i = 0i < scores.lengthi++) {
  79:                 html +=`
  80: <tr>
  81: <td>${i + 1}</td>
  82: <td>${scores[i]}</td>
  83: </tr>
  84: `;
  85:             }
  86:             html +=`
  87: <tr>
  88: <td>合計点</td>
  89: <td>${sum}</td>
  90: </tr>
  91: <tr>
  92: <td>平均点</td>
  93: <td>${sum / scores.length}</td>
  94: </tr>
  95: `;
  96:             //結果を表示
  97:             document.getElementById('scoreTable').innerHTML = html;
  98:         }
  99:     }
 100: }

引数として渡された配列は、これまで同様 score[i] と書くことで、これまで同様、目的の要素を参照することができる。
配列に格納されている要素の数は、length プロパティを使って取得できる。
要素の数だけ forループを回し、合計点と平均点を計算する。
あとは、点数表をTABLEタグとして組み立てる。

引数の参照渡し

さて、関数に渡された引数を、関数内で変更したらどうなるだろうか。

parameter1.html

  19: <script>
  20: // ユーザー定義関数 ========================================================
  21: /**
  22:  * 引数に+1する(その1)
  23:  * @param   Number a
  24:  * @return  Number a + 1
  25: */
  26: function plus1(a) {
  27:     let b = a + 1;
  28:     return b;
  29: }
  30: 
  31: /**
  32:  * 引数に+1する(その2)
  33:  * @param   Number a
  34:  * @return  Number a + 1
  35: */
  36: function plus2(a) {
  37:     a = a + 1;
  38:     document.getElementById('var3').innerHTML = 'a = ' + a.toString();
  39:     return a;
  40: }
  41: 
  42: // メイン・プログラム ======================================================
  43: window.onload = function() {
  44:     let a = 1;
  45:     document.getElementById('var1').innerHTML = 'A = ' + a.toString();
  46:     plus1(a);
  47:     document.getElementById('var2').innerHTML = 'A = ' + a.toString();
  48:     plus2(a);
  49:     document.getElementById('var4').innerHTML = 'A = ' + a.toString();
  50: }
  51: </script>

ユーザー定義関数 plus1 は、渡された引数に1を加算し、関数内ローカル変数bに代入する。結果は次のようになる。
A = 1
A = 1
a = 2
A = 1
つまり、メイン・プログラム側の変数aは、plus1 の実行の前後で変わらない。

ユーザー定義関数 plus2 は、渡された引数を、関数内でそのまま1を加算する。関数内の変数aは2になるが、メイン・プログラム側の変数aは plus1 の実行の前後で変わらない。

このように、JavaScriptでは、関数に渡された引数は、関数内ローカル変数として振る舞う。これを引数の値渡しと呼ぶ。

引数の値渡し

一方、関数に配列を引数として渡すと、参照渡しとならない。プログラム "average1.html" の実行結果は次のようになる。
A = 1,2,3,4,5
A = 1,2,3,4,5
a = 1,2,3,5,5
A = 1,2,3,5,5

parameter2.html

  19: <script>
  20: // ユーザー定義関数 ========================================================
  21: /**
  22:  * 配列引数に+1する(その1)
  23:  * @param   Array  a 配列
  24:  * @param   Number n 要素番号
  25:  * @return  Number a + 1
  26: */
  27: function plus3(a, n) {
  28:     let b = a[n+ 1;
  29:     return b;
  30: }
  31: 
  32: /**
  33:  * 配列引数に+1する(その2)
  34:  * @param   Array  a 配列
  35:  * @param   Number n 要素番号
  36:  * @return  Number a + 1
  37: */
  38: function plus4(a, n) {
  39:     a[n] = a[n+ 1;
  40:     document.getElementById('var4').innerHTML = 'a = ' + a.toString();
  41:     return a[n];
  42: }
  43: 
  44: // メイン・プログラム ======================================================
  45: window.onload = function() {
  46:     let a = [1, 2, 3, 4, 5];
  47:     let n = 3;
  48:     document.getElementById('var1').innerHTML = 'A = ' + a.toString();
  49:     plus3(a, n);
  50:     document.getElementById('var3').innerHTML = 'A = ' + a.toString();
  51:     plus4(a, n);
  52:     document.getElementById('var5').innerHTML = 'A = ' + a.toString();
  53: }
  54: </script>

ユーザー定義関数 plus1 は、渡された引数(配列)an 番目の要素を1を加算し、関数内ローカル変数bに代入する。メイン・プログラム側の配列 a は、plus3 の実行の前後で変わらない。

ユーザー定義関数 plus は、渡された引数(配列)an 番目の要素を、関数内でそのまま1を加算する。すると、メイン・プログラム側の配列aは1を加算された要素に変化する。

このように配列を引数として渡す場合、元の配列をそのまま処理していることになる。これを引数の参照渡しと呼ぶ。

配列をシャフルする

トランプなどのカード型ゲームを作るとき、あらかじめカードを配列に代入しておき、その配列をシャフルすることを行う。JavaScriptでは配列が参照渡しという性質を使って、渡した配列をシャフルする関数を簡単に作ることができる。

shuffle.html

  36: /**
  37:  * 指定した配列をシャフルする.
  38:  * @param   Array arr シャフルしたい配列
  39:  * @return  なし
  40: */
  41: function shuffle(arr) {
  42:     for (let i = arr.lengthi > 1i--) {
  43:         let k = Math.floor(Math.random() * i);
  44:         [arr[k], arr[i - 1]] = [arr[i - 1], arr[k]];
  45:     }
  46: }

配列をシャフルする
シャフルの方法は、左図のように、配列の前後の要素を入れ替える操作を、ランダムに要素の回数だけを行う。
要素の数は lengthプロパティによって取得できる。
ランダムに要素を取り出すには、Math.random 関数を使う。戻り値が浮動小数なので、Math.floor 関数を使い、小数点以下を切り捨てる。

コラム:完全数を求める

レオンハルト・オイラー
レオンハルト・オイラー
自分自身が自分自身を除く正の約数の和に等しくなる自然数のことを完全数(perfect number)と呼ぶ。
たとえば 6 の約数は 1, 2, 3, 6 だが、自分自身を除いた和 1 + 2 + 3 は 6 に等しくなるので、6 は完全数である。
古代から 6, 28, 496, 8128 の4つが完全数であることが知られているが、ピタゴラスが名付けたとか、世界を創造した6日間と月の公転周期の28日が含まれているからキリスト教の神を完全性を表しているなどと言われてきた。
完全数を求める方程式は発見されていない。自然数を1つずつ虱潰しにしていかなければならないわけだが、こういうときに黙々と作業をするコンピュータが重宝する。
この完全数の定義をそのままプログラムにしたものが "perfectNumbers1.html" である。探索範囲は、冒頭の定数 RANGE_MIN, RANGE_MAX で定める。

perfectNumbers1.html

  23: const RANGE_MIN = 2;                    //探索範囲の最小値
  24: const RANGE_MAX = 100000;               //探索範囲の最大値

perfectNumbers1.html

  38: /**
  39:  * 完全数かどうかを判定する.
  40:  * @param   Number num 判定する値
  41:  * @return  Boolean true:完全数である / false:完全数ではない
  42: */
  43: function isPerfectNumber(num) {
  44:     let sum = 0;
  45: 
  46:     // 自分自身以外の約数の和を求める
  47:     for (let i = 1i < numi++) {
  48:         if (num % i === 0) {
  49:             sum +i;
  50:         }
  51:     }
  52: 
  53:     // 和が元の数と等しければ完全数
  54:     return sum === num;
  55: }

perfectNumbers1.html

  57: /**
  58:  * 完全数を求める.
  59:  * @param   Number min 探索範囲の最小値
  60:  * @param   Number max 探索範囲の最大値
  61:  * @return  Array: 完全数の配列
  62: */
  63: function findPerfectNumbers(min, max) {
  64:     let perfectNumbers = [];        // 完全数を格納する配列
  65: 
  66:     for (let i = mini <maxi++) {
  67:         if (isPerfectNumber(i)) {
  68:             perfectNumbers.push(i);
  69:         }
  70:     }
  71: 
  72:     return perfectNumbers;
  73: }

perfectNumbers1.html

  88:     // 完全数を求める
  89:     console.time('計算時間');           // 計算時間の計測開始
  90:     perfectNumbers = findPerfectNumbers(RANGE_MIN, RANGE_MAX);
  91:     ss = '計算範囲:' + RANGE_MIN.toLocaleString() + '~' + RANGE_MAX.toLocaleString() + '<br><br>';
  92:     console.timeEnd('計算時間');        // 計算時間をconsoleに表示する
  93: 
  94:     // 完全数を表示する
  95:     for (let i = 0i < perfectNumbers.lengthi++) {
  96:         ss +perfectNumbers[i].toLocaleString() + '<br>';
  97:     }
  98:     document.getElementById('results').innerHTML = ss;

メイン・プログラムでは console.timeメソッドconsole.timeEndメソッドを使って計算時間をコンソールに出力するようにした。

10万までの探索をやらせると、けっこう時間がかかる。しかも悲しいことに、冒頭の4つの完全数しか見つからない。5番目の完全数は 33550336 であり、これに到達するまでには相当な時間がかかるだろう。2021年8月現在発見されている完全数は51個で、51個目は2486万2048桁もある。
偶数の完全数に限っては、18世紀の数学者レオンハルト・オイラーユークリッド・オイラーの定理を発見しており、これを使うと計算回数を劇的に少なくすることができる。
ユークリッド・オイラーの定理によると、
\[ P = 2^{p - 1} \times (2^p - 1) \]
\( p \)が素数であり、なおかつ \( 2^p - 1 \) も素数である場合(これをメルセンヌ素数と呼ぶ)、\( P \) は偶数の完全数になる。

perfectNumbers2.html

  24: const RANGE_MIN = 1;                    //探索範囲の最小値
  25: const RANGE_MAX = 100000;               //探索範囲の最大値

perfectNumbers2.html

  39: /**
  40:  * 素数かどうかを判定する.
  41:  * @param   Number num 判定する値
  42:  * @return  Boolean True:素数である / False:素数ではない
  43: */
  44: function isPrime(num) {
  45:     if (num < 2) {
  46:         return false;
  47:     }
  48:     for (let i = 2i <Math.sqrt(num); i++) {
  49:         if (num % i === 0) {
  50:             return false;
  51:         }
  52:     }
  53:     return true;
  54: }

perfectNumbers2.html

  56: /**
  57:  * メルセンヌ素数かどうかを判定する.
  58:  * @param   Number p 2のべき乗数
  59:  * @return  Boolean True:メルセンヌ素数である / False:メルセンヌ素数ではない
  60: */
  61: function isMersennePrime(p) {
  62:     let mersenneNumber = Math.pow(2, p- 1;
  63: 
  64:     return isPrime(mersenneNumber);
  65: }

perfectNumbers2.html

  67: /**
  68:  * 完全数を求める.
  69:  * @param   Number min 探索範囲の最小値
  70:  * @param   Number max 探索範囲の最大値
  71:  * @return  Array 完全数の配列
  72: */
  73: function findPerfectNumbers(min, max) {
  74:     let perfectNumbers = [];        // 完全数を格納する配列
  75: 
  76:     if (min < 2) {
  77:         min = 2;
  78:     }
  79:     p = 2;
  80:     while (true) {
  81:         if (isMersennePrime(p)) {
  82:             let num = Math.pow(2, p - 1* (Math.pow(2, p- 1);
  83:             if (num > max)      break
  84:             perfectNumbers.push(num);
  85:         }
  86:         p++;
  87:     }
  88:     return perfectNumbers;
  89: }

perfectNumbers2.html

 104:     // 完全数を求める
 105:     console.time('計算時間');           // 計算時間の計測開始
 106:     perfectNumbers = findPerfectNumbers(RANGE_MIN, RANGE_MAX);
 107:     ss = '計算範囲:' + RANGE_MIN.toLocaleString() + '~' + RANGE_MAX.toLocaleString() + '<br><br>';
 108:     console.timeEnd('計算時間');        // 計算時間をconsoleに表示する
 109: 
 110:     // 完全数を表示する
 111:     for (let i = 0i < perfectNumbers.lengthi++) {
 112:         ss +perfectNumbers[i].toLocaleString() + '<br>';
 113:     }
 114:     document.getElementById('results').innerHTML = ss;

"perfectNumbers2.html" を実行すると、コンソールに表示される計算時間が劇的に短くなったことが確認できるだろう。また、最初の4個の完全数は偶数であるため、たまたま "perfectNumbers1.html" と同じ結果が得られる。
ただし、偶数にしても奇数にしても、完全数が無限に存在するのかどうかは未解決の問題である。

このような計算量の多い問題では、前提条件を置くことで計算量を劇的に減らせることができる場合がある。結果を早く求めることができればユーザー喜ぶから、システムの非機能要件として計算量を減らすことを検討するといい。

読みやすいプログラム:コメント

文字化けした文章を見る人のイラスト
JavaScriptにおけるコメントの書き方は、「2.10 演算の優先順位、コメント - コメント」で紹介した。

かつて、アセンブリやC言語のようなプログラミング言語では、関数や変数名が省略形であったり、独特なプログラミング記述法があったために、コメントは不可欠であった。
だが、今日のJavaScriptのような、いわゆる高級言語では、仕様設計に記した内容をそのままプログラムとして書くことができる。
プログラムを読めば分かることをコメントに書くのは二度手間であるし、バージョンアップ時にいずれか一方の更新を忘れるとバグの温床になることから、コメントは必要最小限にとどめるべきというのが、今風の読みやすいプログラムとなる。

●コメントに記載する内容
  1. プログラム使用上の注意、制約条件など。
  2. プログラム使用にあたって準備すること、実施することなど。
  3. 参照しているライブラリ、フレームワーク、APIなどの情報。
  4. プログラムの著作権情報。
  5. コメントで補足しないと分からないような複雑な箇所。
●コメントに記載する必要がないもの
  1. 変数やプロパティの1つ1つに対する解説。
  2. プログラムを読めば分かる内容。ただし、読み手のスキルに応じてコメントを付加すること。
  3. プログラムの更新履歴。ただし、バージョン管理システムを利用していない場合はコメントとして記述すること。
本連載では、初心者を対象にしている関係で、プログラムを読めば分かる内容でもコメントを記載している。また、GitHub のようなバージョン管理システムを使っていないので、ファイルの末尾に更新履歴を付け加えている。 本連載では、クラスや関数の冒頭に、次のような書式で解説を付けている。
/**
* (1行目)機能概要【必須】
* (2行目)機能詳細,制約事項など【省略可能】
* @param{空白}変数の型{空白}変数名{空白}内容‥‥引数1の説明【省略可能】
* @param{空白}変数の型{空白}変数名{空白}内容‥‥引数2の説明【省略可能】
* (1行に1引数を記述する.)
* @return{空白}変数の型‥‥戻り値の説明【省略可能】
*/
たとえば、次のようなコメントである。
/**
* 点数表を作成し,画面に表示する.
* @param Array scores 点数表
* @return なし
*/
コメントからJavaScriptのドキュメントを自動作成する JSDoc に近い書式であるが、他言語でも利用できるよう汎用性を持たせており、当サイトの構築ツール(非公開)を使って下記のような一覧表を作成できるようになっている。
average1.html 関数/メソッド一覧
関数/メソッド 機能 詳細
getLastModified ファイル更新日を返す
dispScoreTable 点数表を作成し,画面に表示する.

読みやすいプログラム:引数の変更・上書きはしない

友達にプレゼントを贈っている人のイラスト
他のプログラミング言語にも値渡し参照渡しがあるのだが、JavaScriptの引数は、これらの言語で言うところの値渡し参照渡しとは異なる。
JavaScriptでは、同じ変数に対して代入演算子 = が実行されると、見た目は変数の内容を上書きするのだが、内部的には別領域に値を格納している。
let a = 1;
a = 2;
上のようなプログラムの場合、最初の領域に1が代入され、2行目には別領域が用意され、そこへ2が代入される。そして、変数 a の参照先を2番目の領域に更新する。
これが引数にしたときにも起きるので、じつは参照渡しに近いことをやっているのだが、結果的に値渡ししているように見える。

配列の場合は、配列が格納されている領域のポインタ(のようなもの)を渡す。引数で渡すときには、あらたな領域に代入されるのだが、そこには同じ値のポインタ(のようなもの)を代入するので、値渡しのように要素の値を変えることができる。

つまり、JavaScriptの場合、プリミティブ型でもオブジェクト型でも、仕様上は同じ方法で引数を渡している。これを共有渡しと呼ぶ方もいるが、正式な呼称はないようだ。
また、内部的にどのような渡し方をしているかはブラウザの実装によるので、実際に同じ方法で渡しているかどうかは分からない。
このあたりの事情は、@yuta0801氏の記事「JavaScriptに参照渡し/値渡しなど存在しない」に詳しい。

このようにJavaScriptの引数には癖があるため、引数の変更・上書きはしないことが読みやすいプログラムとなる。
(この項おわり)
header