みこむらめもむらむらむら

なんかHTML5とかJS勉強とかやりながらめもを綴るブログ

【JavaScript】引数情報を管理する(argumentsオブジェクト)

JabaScriptは引数の数をチェックしない

function showMessage(value) {
  document.writeln(value);
}

showMessage();  //undefined
showMessage('山田');  //山田
showMessage('山田', '鈴木');  //山田

ユーザ定義関数ShowMessageは引数をひとつ受け取る

このような関数に対して上記コードのように
それぞれ0、1、2個の引数を渡してやると
0個、2個の場合エラーが出るように感じるが
実際は正しく動作する

JavaScriptでは
与える引数の数が関数側で要求する数と異なる場合も
これをチェックしない

したがって引数0個の場合、仮引数valueの値は
undefined(未定義)であるものとして処理される
また、引数が多かった場合、多かった2つ目の引数('鈴木')は
とりあえず無視され結果として
引数を1個渡した場合と同様の結果が得られた
もっとも多かった引数は切り捨てられるわけではない
内部的には「引数情報のひとつ」として保持されて
あとからりようできる状態になっている
この引数情報を管理するのがargumentオブジェクトである
argumentオブジェクトは関数配下(関数を定義する本体部分)でのみ
利用できる特別なオブジェクトである

argumentオブジェクトは関数呼び出しのタイミングで生成されて
呼び出し元から与えられた引数の値を保持する
このargumentオブジェクトを利用することで、たとえば
「実際に与えられた引数の数と要求する引数の数を比較し、
異なる場合にはエラーを返す」といった処理も記述できる

function showMessage(value) {
  if (arguments.length != 1) {
    throw new Error('引数の数が違うのぜ(・∀・)' + arguments.length + '個じゃ多いのだわ');
  }
  document.writeln(value);
}

try {
  showMessage('山田', '鈴木');

} catch(e) {
  window.alert(e.message);
}

あれ?try..catch命令とかthrow命令って
どんなだっけってなったので復習

例外処理(try..catch..finally命令)

try {
  例外が発生するかもしれない命令(群)
} catch(例外情報を受け取る変数) {
  例外が発生した際に実行する命令(群)
} [finally {
  例外の有無に関わらず、査収的に実行される命令(群)
}]
  • 例外情報はcatchブロックにErrorオブジェクトとして引き渡される
throw new Error(エラーメッセージ)
  • 例外はプログラム中に発生したものを補足するばかりではなく自分で発生させることもできる
  • 例外を発生させることを「例外をスローする」ともいう
  • throw命令は多くの場合if命令のような条件分岐命令と一緒に使用される

うむ、そうでしたこんなんやりました

この例で注目すべきはif命令のあたり
lengthは、argumentsオブジェクトに属するプロパティの一つで
実際に関数に渡された引数の個数を表す
つまりここでは
「実際に渡された引数が1個でない場合に例外をスローしている」
のである
例では呼び出し元の引数が2個なので例外が発生し
エラーメッセージをダイアログ表示する

ここでは、単に日引数の個数をチェックしているだけだが
引数のデータ型や値の有効範囲などの妥当性をチェックすることもできる

argumentsとArgumentsどちらが正しい?

argumentsオブジェクトの実体は、厳密には
「Argumentsオブジェクトを参照するargumentsプロパティ」である
入門書によってはArgumentsオブジェクトと記載されているものも多い
しかし、関数のなかで「Arguments.length」と記述することはできない
Argumentsオブジェクトはあくまで関数内で暗黙的に生成されるもので
プログラマが意識することすらない存在である
なるほどー
この本では混乱しないように
argumentsオブジェクトという記述で統一しているみたいね、ふむ

補足:引数のデフォルト値を設定する

引数の数をチェックしない、ということはJavaScriptでは
すべての引数は省略可能であるということになる
ただし多くのケースでは、引数がただ省略されただけでは
正しく動作しないことがほとんどである

デフォルトで引数を設定しておく必要がある

function triangle(base, height) {
  if (base == undefined) { base = 1; }
  if (height == undefined) { height = 1; }
  return base * height / 2;
}

document.writeln(triangle(5));  //2.5

上記のtriangle関数において引数base、heightのデフォルト値はいずれも1である

JavaScriptでは、引数のデフォルト値を表現するための構文はない
そのため、例のように引数の内容をチェックし
undefined(未定義値)だった場合にそれぞれの値をセットしている

「document.writeln(triangle(5));」では引数が一つしか設定されていない
そのため、後方の引数heightが省略されているものとみなされ
「5×1÷2」で2.5という結果が得られる
引数baseだけを省略することはできない
「省略できるのは、あくまで後ろの引数だけである」という点に注意

可変長引数の関数を定義する

argumentsオブジェクトの用途は
なにも引数の妥当性チェックだけにあるわけではない
可変長引数の関数という重要な機能がある

可変長引数の関数とは「引数の個数があらかじめ決まっていない関数」のこと
たとえば、Functionコンストラクタを思い出してみるとわかりやすいかもしれない
Functionコンストラクタでは、生成する関数オブジェクトが
要求する引数の個数に応じて引数を自由に変更できる
Functionコンストラクタでは
生成する関数オブジェクト要求する引数の個数に応じて、引数を自由に変更できる

var showMessage = Function('msg', 'document.writeln(msg);');
//与える引数は2個(仮引数と処理内容が一つずつ)
var triangle = new Function('base', 'height', 'return base * height / 2;');
//与える引数は3個(仮引数が2個と処理内容が一つ)

このように、呼び出し元の都合で引数の個数が変動する可能性がある
宣言時に引数の個数が確定できない関数が可変長引数の関数である

可変数引数の関数を利用することで柔軟に処理を記述することができる
下記は引数に与えられた数値を合計するsum関数を定義する例である

function sum() {
  var result = 0;
    //与えられた引数を順番に取り出し、順に加算処理
  for (var i = 0; i < arguments.length; i++) {
    var tmp = arguments[i];
    if (isNaN(tmp)) {
      throw new Error('指定値が数値じゃないよ' + tmp);
    }
    result += tmp;
  }
  return result;
}

try {
  document.writeln(sum(1, 3, 5, 7, 9));//25

} catch(e) {
  window.alert(e.message);
}

この例ではforループの中でarugmentsオブジェクトから
すべての要素(引数の値)を取り出しその合計値を求める
arugmentsオブジェクトからi番目の要素を取り出すには
「arugments[i]」のように記述する
また、ここではグローバル関数isNaNで
取得した要素が数値であるかどうかを確認している点に要注目である
isNaNがtrueを返す(要素が数値でない)場合には
Errorオブジェクトを呼び出し元にスローし処理を中断する

明示的に宣言された引数と可変長数を混在させる

function printf(format) {
    //引数の2番目以降を順番に処理
  for (var i = 0; i < arguments.length; i++) {
    var pattern = new RegExp('\\{' + (i - 1) + '\\}', 'g');
    format = format.replace(pattern, arguments[i]);
  }
  document.writeln(format);
}

printf('こんにちは、{0}さん、私はみこむらです', '山田', 'みこむら');
//こんにちは、山田さん、私はみこむらです

printf関数は、第1引数で指定された書式文字列に含まれる
プレイスホルダ(パラメータの置き場所:{0}、{1}、{2}..)を
第2引数以降の値で置き換えたものを出力するための関数

printf関数で第1引数だけが仮引数formatとして明示的に宣言されており
第2引数以降がいわゆる可変長引数として扱われている点に注目
このようなケースでもargumentsオブジェクトには
明示的に宣言された引数→可変長引数の順番ですべての引数が格納される
可変長引数だけがargumentsオブジェクトに管理されるわけじゃない点に注意

つまり、ここではargumentsオブジェクトから
可変長引数の部分(arguments[1]..[n])を取り出し
順に対応するプレイスホルダ({0}、{1}、{2}..)と置き換えている

あ、一応replaceメソッドの復習

正規表現で文字列を置き換える

  • String.replaceメソッド
    • 正規表現でマッチした文字列を置換する
    • 置換後の文字列には「$1..$9」といった特殊変数を埋め込める
    • この特殊変数はサブマッチ文字列を保存するための変数
置き換え対象の文字列.replace(正規表現オブジェクト, 置換後の文字列)

うむー何とか意味が理解できた気がする

無名引数は必要最低限に

明示的に宣言された引数に対しては、
変数名formatで直接アクセスするほか
argumentsオブジェクト経由で
「arguments[0]」のようにアクセスすることもできる
つまり先ほどの例のような場合でも(構文的には)
すべての引数を可変長引数として記述できる

しかし、これはコードの可読性という観点から推奨できない
インデックス番号で引数を管理するargumentsオブジェクトよりも
名前で管理する引数の方が
直観的に引数の内容を把握しやすいからである

なにからなにまでargumentsオブジェクトに委ねるのではなく
内容や個数があらかじめ想定できる引数については
できるだけ明示的に名前を付けておくこと

ふむふむ了解である

可変長引数にも仮の名前を付ける

「コードの可読性」という意味では
可変長引数にも仮の名前を付けておくのが望ましい

たとえば先ほどの例だと以下のようになる

function printf(format, var_args) {

これによって後からコードを読んだ人間は
printf関数の末尾に可変長関数を指定できることを理解できる
ただし、この引数var_argsはあくまで便宜的な名前なので
関数の中ではこれまでと同じく
argumentsオブジェクト経由でアクセスする必要がある
(引数var_argsにすべての可変長引数が格納されるわけではない)

再起呼び出しを定義する(calleeプロパティ)

argumentsオブジェクトでもう一つ重要なのが
現在実行中の関数自身を参照するために用意されている
calleeプロパティである
argumentsオブジェクトのその他のプロパティに比べると
calleeプロパティを目にする機会はそれほど多くはない
しかし、calleeプロパティを利用すれば、
関数などの特定の処理の中で自分自身を呼び出す
再帰呼び出し(Recursive Call)の処理を
容易に記述できるようになる
これによりたとえば階乗計算のように
同種の手続きを階層的に何度も呼び出すような処理を
シンプルなコードで表現できる

function factorial(n) {
  if (n != 0) { return n * arguments.callee(n - 1); }
  return 1;
}

document.writeln(factotial(5));  //120

factorial関数は与えられた自然数nの
階乗を求めるためのユーザ定義関数である
ここでは自然数nの階乗が「n×(n-1)!」で
求められることに着目している
これを再帰的に表現しているのが
「return n * arguments.callee(n - 1);」である
つまり与えられた数値から1を差し引いたもので
自分自身を再帰的に呼び出しているのである
冒頭で述べたように、
関数自身を表すのはarguments.calleeプロパティである
これを念頭において先ほどのコードをみてみると
内部的には以下のような手順で処理が行われていることになる

factorial(5)
  → 5 * factorial(4)
    → 5 * 4 * factorial(3)
      → 5 * 4 * 3 * factorial(2)
        → 5 * 4 * 3 * 2 * factorial(1)
          → 5 * 4 * 3 * 2 * 1 * factorial(0)
            → 5 * 4 * 3 * 2 * 1 * 1
          → 5 * 4 * 3 * 2 * 1 
        → 5 * 4 * 3 * 2
      → 5 * 4 * 6
    → 5 * 24
  → 120

このように何段階にわたる処理も再帰呼び出しを利用すれば
これだけ短いコードで記述できる
なお、階乗計算の場合、
自然数nが0である場合に戻り値を1とすること」を
忘れないように気を付ける
さもないとこの再帰呼び出しは無限ループになってしまう

また、この例であれば下記記述を

return n * arguments.callee(n - 1);

calleeプロパティを使わずに以下のように記述することもできる

return n * factorial(n - 1);

しかしこのように記述すると、
もともとの関数名が変更された場合に
関数本体の記述も変更しなければならないことから
あまり好ましくない

また、以下のように匿名関数として記述された場合には、
そもそも再帰呼び出しするための関数名がないため
calleeプロパティを利用「しなければならない」

function (n) {
  if (n != 0) { 
    return n * ???(n - 1);  //←関数名が指定できない
  }
  return 1;
}

document.writeln(factotial(5));  //120

再帰呼び出しとcalleeプロパティ
いずれも重要な事項なので
ここで合わせてきちんと理解する必要がある、うむ

うむ!
使う機会があまり想像できないのでその辺は先生に!