JavaScript: 関数の引数 - 可変長, キーワード引数



JavaScript は, 古い仕様の上に新しい仕様を積み重ねているため、仕様がだいぶ混乱している。過去のコードの互換性のため、今さら変更できない。

このページは、関数呼出しの引数に注目します。新しい仕様である可変長引数、キーワード引数。

伝統的な挙動

関数側で値を受取る変数を「仮引数」, 呼出すときの値を「実引数」という。

仮引数 parameter
関数定義の冒頭で書かれる変数
実引数 argument
関数を実際に使用するときに, その関数に引き渡される値

JavaScript の古い仕様で特徴的なのは, function call のところ。

構文は, 次のようになっている (5.1 edition; 2011年)。ここは変哲もない。

CallExpression
MemberExpression Arguments
Arguments
( ArgumentListopt )
ArgumentList
AssignmentExpression |
ArgumentList , AssignmentExpression

関数呼出しの挙動は, 次のようになっている。JavaScript では, 関数もオブジェクトなので、プロパティを持つ。

  1. 実引数によってリストを生成する
  2. this と 1. の実引数リストとで, 関数オブジェクトの [[Call]] 内部プロパティを呼び出す。
  3. 実引数のほうが仮引数より少ないとき, 足らない引数には undefined が入る (10.5 Declaration Binding Instantiation)。

実引数のほうが多くても少なくてもエラーにならない。マジか~, そこはエラーにしてほしい・・・

実引数は, 関数のなかでは暗黙に, ローカル変数 arguments に格納される。

JavaScript
[RAW]
  1. function s(a, b) {
  2. // arguments には, 明示的な仮引数に対応する分も含めて, 実引数が全部格納される.
  3. console.log(a, b, arguments);
  4. }
  5. s(1); //=> 1 undefined Arguments(1)
  6. s(1, 2, 3); //=> 1 2 Arguments(3)

モダンな挙動

ECMAScript 2015 (ES2015; ES6) は 5.1 edition までと別物です。

  1. アロー関数 Arrow Function
  2. 関数の可変長パラメータ Rest Parameter
  3. 関数のデフォルトパラメータ Default Parameter
  4. 分割代入 Destructuring Assignment によるキーワード引数
  5. スプレッド構文 Spread Syntax による配列展開 (関数呼出し側)

古い構文に比べると、かなり首尾一貫した挙動になっています。アロー関数だけは, ソースコードを前から読むだけでは構文解析できない、省略しすぎ感ある。

1) アロー関数

アロー関数は, 新しく導入されたため, 古い仕組みの一部が変更になっている。暗黙のローカル変数 arguments はエラーになる (未定義)。後述のレストパラメータを使えばよい。

this の束縛はレキシカルスコープになる。function はダイナミックスコープ。

JavaScript
[RAW]
  1. // アロー関数では arguments はエラー.
  2. const w = (a1) => {
  3. //console.log(arguments); ReferenceError: arguments is not defined
  4. console.log(a1);
  5. }
  6. w(1, 2, 3); //=> 1

2) 可変長引数, 3) デフォルト引数

普通の仮引数の後ろにスプレッド構文 ...variable を書けば、rest parameter として残りの実引数を配列として得ることができる (可変長引数)。

古い arguments の代わりになる。arguments と異なり, 仮引数と明示的に対応しない実引数だけが得られる。

それぞれの仮引数にデフォルト値を書けるようになった。デフォルト値は、実引数が undefined のときに使われる。個数が足らないときではないことに注意。なので、デフォルト値を持つ仮引数の後ろにさらに仮引数を書いてもよい。

JavaScript
[RAW]
  1. function s(...args) { // スプレッド構文 = Rest parameters
  2. console.log(args);
  3. }
  4. s(1, 2); //=> [1, 2] 配列になる
  5. s({a: 1, b:'foo'}); //=> [{…}] オブジェクトは, 一つのオブジェクト (展開されない)
  6. function t(a, b, ...rest) {
  7. console.log(rest);
  8. }
  9. t(1); //=> [] 残りがないので空配列
  10. t('a', 'b', 'c'); //=> ["c"]
  11. function u(a = 'str', b) { // デフォルト引数ありの後ろになしもOK. マジか
  12. console.log(a, b);
  13. }
  14. u(); //=> 'str' undefined
  15. u({b: 'hoge'}); //=> {b: "hoge"} undefined 第1引数に格納 (キーワード引数ではない)
  16. u(undefined, 10); //=> 'str' 10 -- undefined よりデフォルト値が優先。マジか。

デフォルト引数とレストパラメータの組み合わせも問題ない.

JavaScript
[RAW]
  1. // レストパラメータ
  2. const x = (a1, a2 = 'foo', ...args) => {
  3. console.log(a1, a2, args);
  4. }
  5. x(1); //=> 1 "foo" [] レストパラメータは配列に入る。
  6. x(1, 'bar', 3, 365); //=> 1 "bar" [3, 365]

4) キーワード引数

分割代入構文を使って, キーワード引数を実現できる。

JavaScript
[RAW]
  1. // キーワード引数のみ. デフォルト値も持てる
  2. function f({name, age = -100}) {
  3. console.log(name, age);
  4. }
  5. // 呼出し側は, オブジェクトでなければならない
  6. f({name:'Sato'}); //=> 'Sato' -100
  7. let obj = {name:'Yamada', age:20}
  8. f(obj); //=> 'Yamada' 20
  9. // 通常の仮引数と組み合わせ
  10. function g(a, b, {name, age}) {
  11. console.log(a, b, name, age);
  12. }
  13. g(1, 2, {name:'Taro'}); //=> 1 2 'Taro' undefined
  14. g('Sato', 25, {}); //=> 'Sato' 25 undefined undefined
  15. //g('Hanako', 10); TypeError. undefined を分割代入しようとして。
  16. // ほかの分割代入も普通に書ける. 実引数が配列のときの展開.
  17. function h(x, [a, b]) {
  18. console.log(x, a, b);
  19. }
  20. h(1, [0, 2, 4]); //=> 1 0 2

レストパラメータと組み合わせることもできる。レストパラメータは配列として受け取るので、それを分割代入で展開する。

分割代入のなかで、もう一度レストパラメータが使える。

JavaScript
[RAW]
  1. // レストパラメータは代入で使えるものなら使える
  2. const y = function(a1, ...[r1, r2, ...rest]) {
  3. console.log(a1, r1, r2, rest);
  4. }
  5. y(1, 21, 22, 101, 102); //=> 1 21 22 [101, 102]

関数呼出しでの配列展開

今度は逆に, 関数呼出し時に引数を展開する。apply() で配列を実引数として展開できる。これと同じ動作。

JavaScript
[RAW]
  1. function q(x, y, z) {
  2. console.log(x, y, z);
  3. }
  4. q.apply(null, [2, 4, 8]); //=> 2 4 8
  5. let args = [3, 9, 81]
  6. q(...args) //=> 3 9 81