2022/04/30(土)~05/03(火) JSprimer(関数と宣言)

関数とは?

関数とは、ある一連の手続き(文の集まり)を1つの処理としてまとめる機能です。
一度定義した関数を呼び出すことで同じ処理を何回でも実行できます。つまり、関数を使いまわすことが出来るようになります。

関数宣言

functionからはじまる文は関数宣言と呼び、関数を定義するために使われます。

// 関数宣言
function 関数名(仮引数1, 仮引数2) {
    // 関数が呼び出されたときの処理をブロック内に記述する
    return 関数の返り値;
}
// 関数呼び出し
const 関数の結果 = 関数名(引数1, 引数2); // 関数の結果を変数で受け取る
console.log(関数の結果); // => 関数の返り値

関数の構成要素

関数は次の4つの要素で構成されています。

  1. 関数名 - 関数の名前。関数を呼び出す際に使われる名前です。利用できる名前は変数名と同じです。.

  2. 仮引数 - 関数の呼び出し時に渡される実引数を受け取るための変数です。カンマで区切ると、複数の仮引数を記述できます。

  3. 関数の中身(実際の処理) - {}の間に関数の処理を書きます。

  4. 関数の返り値 - 関数の処理内でreturn文を記述すると、関数の実行結果を戻り値として呼び出し元に返すことが出来ます。

関数の特徴

  • 宣言した関数は、関数名()のように関数名にカッコをつけることで呼び出せます。
  • 関数を引数と共に呼ぶ際は、関数名(引数1, 引数2)とし、引数が複数ある場合は,(カンマ)で区切ります。
  • 関数の処理としてreturn文が実行されると、関数内のそれ以降の処理は行われません。
  • 関数が値を返す必要がない場合は、戻り値を省略できますが、return文そのものを省略することも出来ます。どちらの場合も未定義の値undefinedが返ります。
function fn() {
    // 何も返り値を指定してない場合は`undefined`を返す
    // return文を省略した場合にもundefinedが返る
    return;
    // すでにreturnされているため、これ以降の行は実行されません
}
console.log(fn()); // => undefined

関数の引数

JavaScriptでは、関数に定義した仮引数の個数と実際に呼び出したときの実引数の個数が違っても、関数を呼び出せます。

仮引数に比べて、呼び出し時の実引数の数が少ないとき

定義した関数の仮引数よりも呼び出し時の実引数が少ない場合、余った仮引数にはundefinedという値が代入されます。

// 実引数を1つ受けとって、実引数の値を返すecho関数
function echo(x) {
    return x;
}

console.log(echo(1)); // => 1

// 実引数を1つも渡さなかった場合、仮引数xにはundefindが代入される
console.log(echo()); // => undefined
// 実引数を2つ受け取り、実引数を要素とする配列を作成して返すargumentsToArray関数
function argumentsToArray(x, y) {
    return [x, y];
}

console.log(argumentsToArray(1, 2)); // => [1, 2]
// 実引数を1つしか渡さなかった場合、仮引数のxには1、yにはundefinedが代入される
console.log(argumentsToArray(1)); // => [1, undefined]

呼び出し時の実引数の数が少ないときの対応策:[ES2015] デフォルト引数

デフォルト引数デフォルトパラメータ)は、仮引数に対応する実引数が渡されていない場合に、デフォルトで代入される値を指定できます。 仮引数に対して仮引数 = デフォルト値という構文で、仮引数ごとにデフォルト値を指定できます。

// デフォルト値を設定した時の関数宣言
function 関数名(仮引数1 = デフォルト値1, 仮引数2 = デフォルト値2) {
  // 関数の処理
}
  • 実引数が渡されなかった場合にデフォルト値が仮引数に代入されます。
  • ES2015でデフォルト引数が導入されるまでは、OR演算子||)を使ったデフォルト値の指定がよく利用されていました。しかし、OR演算子||)を使ったデフォルト値の指定にはfalsyな値を渡すとデフォルト値が入ってしまうという問題点があります。
  • falsyな値とは、真偽値へと変換するとfalseとなる次のような値のことです。
    • false
    • undefined
    • null
    • 0
    • 0n
    • NaN
    • ""(空文字列)
// OR演算子(||)を使ったデフォルト値指定の問題点

function addPrefix(text, prefix) {
    // 仮引数prefixにデフォルト値をor演算子で設定する
    const pre = prefix || "デフォルト:";
    return pre + text;
}

// 2つ目の実引数を渡さなかった場合以外に、falsyな値(空文字や0など)を渡した場合でもデフォルト値が代入されてしまう
console.log(addPrefix("こんにちは")); // => "デフォルト:こんにちは"
console.log(addPrefix("こんにちは", "")); // => "デフォルト:こんにちは"
console.log(addPrefix("こんにちは", 0)); // => "デフォルト:こんにちは"

// 実引数を2つとも渡した場合は、デフォルト値は適用されず渡した実引数が代入される
console.log(addPrefix("こんにちは", "カスタム:")); // => "カスタム:こんにちは"
  • 上記から分かるように、2つ目の実引数を渡さなかった場合以外に、falsyな値(空文字や0など)を渡した場合でもデフォルト値が代入されてしまいます。 これが本当に0や空文字を渡したい場合だとデフォルト値が入るのは問題です。 このようにfalsyな値を実引数として渡した場合にデフォルト値が代入される事が意図した挙動なのかどうかがわかりにくくバグに繋がりやすいです。
  • ES2020から導入されたNullish coalescing演算子(??)を利用することでも、 OR演算子||)の問題を避けつつデフォルト値を指定できます。
function addPrefix(text, prefix) {
    // prefixがnullまたはundefinedの時、デフォルト値を返す
    const pre = prefix ?? "デフォルト:";
    return pre + text;
}

console.log(addPrefix("文字列")); // => "デフォルト:文字列"
// falsyな値でも意図通りに動作する
console.log(addPrefix("文字列", "")); // => "文字列"
console.log(addPrefix("文字列", "カスタム:")); // => "カスタム:文字列"

OR演算子||)とNullish coalescing演算子(??)の違い

記述方法 評価方法
OR演算子 A || B 左辺のオペランドがfalsyな値の場合に右辺のオペランドを評価する。
Nullish coalescing演算子 A ?? B 左辺のオペランドがnullまたはundefinedの場合に右辺のオペランドを評価する。

仮引数に比べて、呼び出し時の実引数の数が多いとき

関数の仮引数に対して実引数の個数が多い場合、あふれた実引数は単純に無視されます。

// 実引数を2つ受け取り、その実引数を足し算して返すadd関数
function add(x, y) {
    return x + y;
}
console.log(add(1, 3)); // => 4
// 実引数を3つ以上指定した場合、3つ目以降は無視される
console.log(add(1, 3, 5)); // => 4
  • 上記から実引数を3つ以上渡しても無視されてしまうためadd(1, 3)add(1, 3, 5)の結果は同じになります。

呼び出し時の実引数の数が多いときの対応策:可変長引数

関数において引数の数が固定ではなく、任意の個数分引数を受け取りたい場合可変長引数を使います。 可変長引数を使うと関数宣言時に受け取る実引数の個数をあらかじめ固定する必要がなく、呼び出し時に個数の制限なく実引数を渡すことができるようになります。 可変長引数を実現するためには、Rest parametersargumentsという特殊な変数を利用します。

可変長引数を実現する方法①[ES2015] Rest parameters

Rest parametersは、仮引数名の前に...をつけた仮引数のことで、残余引数とも呼ばれます。 Rest parametersには、関数に渡された値が配列として代入されます。Rest parametersを使うことで実引数を何個でも受け取ることができるようになります。

// あらかじめ受け取る実引数の個数を決めないfn関数(実質何個でも実引数を受け取れる)
function fn(...args) {
    // argsは引数の値が順番に入った配列
     return console.log(args);
}
fn("a", "b", "c");  // => [ 'a', 'b', 'c' ]
fn("a", "b", "c", "d", "e", "f"); // => [ 'a', 'b', 'c', 'd', 'e', 'f' ]
fn("a", "b", "c", "d", "e", "f", "g", "h", "i"); // => ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']

Rest parametersは、通常の仮引数と組み合わせても定義できます。 ほかの仮引数と組み合わせる際には、必ずRest parameters末尾の仮引数として定義する必要があります。

// 1番目の実引数はarg1で受け取り、2番目以降の実引数は配列として、restArgsで何個でも受け取るfn関数
function fn(arg1, ...restArgs) {
    console.log(arg1); // => "a"
    console.log(restArgs); // => ["b", "c"]
}
fn("a", "b", "c"); // => a  と [ 'b', 'c' ]
fn(); // => undefined  と  []
  • 1番目の実引数はarg1に代入され、残りの実引数がrestArgs配列としてまとめて代入されます。

RestParametersの似ている記述にSpread構文がありますが、使い方に違いがあるので注意しましょう。

構文の意味 使い方
RestParameters 実引数を任意の個数受け取りたい時に実引数を配列としてまとめて受け取る構文です。Rest parametersを関数の仮引数に指定して使います。この時仮引数の前に...をつけます。 ...仮引数
Spread構文 関数呼び出し時に、渡したい実引数を配列としてまとめて渡すことができます。実際には、実引数として渡された配列の展開した要素が関数の仮引数に渡されます。関数を呼び出す際、配列名の前に...をつけて使います。 ...配列
// Spread構文を使った例

function fn(x, y, z) {
    console.log(x); // => 1
    console.log(y); // => 2
    console.log(z); // => 3
}
const array = [1, 2, 3];
// Spread構文で配列を引数に、展開して関数を呼び出す
fn(...array);
// 次のように書いたのと同じ意味
fn(array[0], array[1], array[2]);

可変長引数を実現する方法②arguments

可変長引数を扱う方法として、argumentsという関数の中でのみ参照できる特殊な変数があります。 argumentsは関数に渡された引数の値がすべて入ったArray-likeなオブジェクトです。 Array-likeなオブジェクトは、配列のようにインデックスで要素へアクセスできますが、Arrayのメソッドは利用できないという特殊なオブジェクトです。

function fn() {
    // `arguments`はインデックスを指定して各要素にアクセスできるArray Likeなオブジェクト
    // `arguments`は関数に渡された実引数の値がインデックス番号と紐づいて格納されるオブジェクト(仮引数の定義とは関係なく)
    console.log(arguments[0]); // => "a"
    console.log(arguments[1]); // => "b"
    console.log(arguments[2]); // => "c"
    console.log(arguments);  // => { '0': 'a', '1': 'b', '2': 'c' }
}
fn("a", "b", "c");

argumentsの問題点

  • Arrow Functionでは利用できない

  • Array-likeオブジェクトであるため、Arrayのメソッドを利用できない

  • arguments変数は仮引数の定義とは関係なく、実際に渡された実引数がすべて含まれているオブジェクトであるため、仮引数だけを見て関数が可変長引数を受けつけるのかを判断できない

上記の問題点から可変長引数が必要な場合はarguments変数よりも、Rest parametersで実装するようにしましょう。

[ES2015] 関数の引数と分割代入

関数の引数においても分割代入が利用できます。 分割代入はオブジェクトや配列からプロパティを取り出し、変数として定義し直す構文です。

分割代入を使わない場合

function printUserId(user) {
    // 関数内で、Userオブジェクトのidプロパティにアクセスする
    console.log(user.id); // => 42
}
const user = {
    id: 42
};
printUserId(user); // printUserId関数の引数としてuserオブジェクトを渡す

分割代入を使う場合(オブジェクトvar)

// 第1引数のオブジェクトから`id`プロパティを変数`id`として定義する
// 関数が呼び出された時にuserオブジェクトのidプロパティが分割代入される
// const id = user; つまり、const id = user.id;と同じこと
function printUserId({ id }) {
    // 変数名idでuserオブジェクトのidプロパティにアクセスできるようになる
    console.log(id); // => 42
}
const user = {
    id: 42
};
printUserId(user);
  • 関数の引数における分割代入では、仮引数に定義したい変数名を定義し、実引数として渡すオブジェクトから対応するプロパティを代入します。 const id = user.id;
  • 関数の引数における分割代入は、オブジェクトだけではなく配列についても利用できます。

関数の引数における分割代入(配列var)

// 引数に渡された配列arrayの1番目の要素がfirstに、2番目の要素がsecondに代入(実引数で渡されなかった仮引数にはundifinedが代入)
function print([first, second, third]) {
    console.log(first); // => 1
    console.log(second); // => 2
    console.log(third); // => undefined
}
const array = [1, 2];
print(array);

関数はオブジェクト

JavaScriptでは、関数関数オブジェクトとも呼ばれ、オブジェクトの一種です。 関数はただのオブジェクトとは異なり、関数名()をつけることで、関数としてまとめた処理を呼び出すことができます。 一方で、()をつけて呼び出されなければ、関数をオブジェクトとして参照できる他、変数へ代入したり、関数の引数として渡すなど値として扱うことが可能です。

🌟このように関数が値として扱えることを、ファーストクラスファンクション第一級関数)と呼びます。

関数を値として扱うことができると..? - 関数を変数に代入できる - 関数の引数として渡すことができる(コールバック関数)

関数を値として定義する方法①関数式

関数式とは、関数を値として変数へ代入している式のことを言います。 関数宣言は文でしたが、関数式では関数をとして扱っています。

// 関数式
const 関数名 = function(仮引数) { // 関数式は変数名で参照できるため、"関数名"を省略できる
    // 関数を呼び出したときの処理
    // ...
    return 関数の返り値;
};

// functionキーワード**を使った関数宣言
function 関数名(仮引数) { // 関数宣言では"関数名"は省略できない
    // 関数を呼び出したときの処理
    // ...
    return 関数の返り値;
}
  • 関数式変数名で参照できるため、"関数名"を省略できます。
  • functionキーワードを使った関数宣言では"関数名"は省略できません。
  • 関数式では、名前を持たない関数(匿名関数・無名関数)を変数に代入できます。
  • 関数式で定義した関数は、一般的に変数などに代入するため名前をつけない匿名関数として定義されることが多いです。 しかし一方で、関数式で定義した関数に名前をつけた場合、定義した関数名は関数の中からしか呼べなくなります。

関数を値として定義する方法②[ES2015] Arrow Function

関数式にはfunctionキーワードを使った方法以外に、Arrow Functionと呼ばれる書き方があります。 名前のとおり矢印のような=>アロー記号)を使い、匿名関数を定義する構文です。

// Arrow Functionを使った関数定義
const 関数名 = (仮引数) => {
    // 関数を呼び出したときの処理
    // ...
    return 関数の返す値;
};

Arrow Functionの省略記法

  • 関数の仮引数が1つのときは()を省略できる

  • 関数の処理が1つの式である場合に、ブロック{}return文を省略できる

    • その式の評価結果をreturnの返り値とする
// 仮引数の数と定義
const fnA = () => { /* 仮引数がないとき */ };
const fnB = (x) => { /* 仮引数が1つのみのとき */ };
const fnC = x => { /* 仮引数が1つのみのときは()を省略可能 */ };
const fnD = (x, y) => { /* 仮引数が複数のとき */ };
// 値の返し方
// 次の2つの定義は同じ意味となる
const mulA = x => { return x * x; }; // ブロックの中でreturn
const mulB = x => x * x;            // 1行の式のみの場合はreturnとブロックを省略できる

Arrow Functionの特徴

  • 名前をつけることができない(常に匿名関数
  • thisが静的に決定できる
  • functionキーワードに比べて短く書くことができる
  • newできない(コンストラクタ関数ではない)
  • arguments変数を参照できない(functionキーワードに比べて、人による解釈や実装の違いが生まれにくい)

functionキーワードの関数式でコールバック関数を定義した場合

const array = [1, 2, 3];
// 1,2,3と順番に値が渡されコールバック関数(匿名関数)が処理する
// mapメソッドは、array配列の要素を順番にコールバック関数(匿名関数)へ渡し、そのコールバック関数が返した値を新しい配列にして返します。
const doubleArray = array.map(function(value) {
    return value * 2; // 返した値をまとめた配列ができる
});
console.log(doubleArray); // => [2, 4, 6]
  • mapメソッドの引数としてfunctionキーワードを使った匿名関数(コールバック関数)を定義しています。
  • mapメソッドは、順番にarray配列の要素をコールバック関数に渡し、そのコールバック関数が返した値を新しい配列にして返します。
  • 今回の場合、1,2,3と順番に要素の値が渡され、コールバック関数で要素の値を2倍にして戻り値として返します。 mapメソッドは戻り値を受け取り、要素を2倍にした値を要素とする新たな配列を作成して返します。

Arrow Functionでコールバック関数を定義した場合

const array = [1, 2, 3];
// 仮引数が1つなので`()`を省略できる
// 関数の処理が1つの式なので`return`文を省略できる
const doubleArray = array.map(value => value * 2);
console.log(doubleArray); // => [2, 4, 6]
  • mapメソッドの引数としてArrow Functionを使った匿名関数(コールバック関数)を定義しています。
  • Arrow Functionでは関数の処理が1つの式だけである場合に、return文を省略し暗黙的にその式の評価結果をreturnの返り値とします。
  • Arrow Functionの仮引数が1つである場合は()を省略できます。
  • Arrow Functionを使う方がコードの見通しを良く記述できます。

上記の特徴からArrow Functionは、人による解釈や実装の違いが生まれにくく、thisの問題の多くを解決できるという利点があるため、積極的にArrow Functionを使うようにしましょう。 Arrow Functionを使うことに問題がある場合にのみfunctionキーワードを使いましょう。

同じ名前の関数宣言は上書きされる

関数宣言で定義した関数は、引数の数や型の違いで区別されることなく、関数の名前でのみ区別されます。 そのため、同じ関数名を複数回宣言した場合には、後ろで宣言された同じ関数名によって上書きされます。

(TypeScriptで宣言した関数は、名前以外にも引数の数や型の違いで区別されます。そのため、同じ関数名を複数回宣言した場合にも上書きされることなく、引数の数や型が異なる同じ関数が共存する状態になります。)

// 仮引数の数が異なる同じfn関数を2つ定義
function fn(x) {
    return `最初の関数 x: ${x}`;
}
// 仮引数の定義が異なっていても同じ名前のfn関数なので上書きされる
function fn(x, y) {
    return `最後の関数 x: ${x}, y: ${y}`;
}
// fn関数を呼び出すと、最後に定義されたfn関数が優先されて実行
console.log(fn(2, 10)); // => "最後の関数 x: 2, y: 10"

ただし、同じ関数名で複数の関数を定義することは基本的に意味がないため避けるべきです。

コールバック関数

関数ファーストクラスであるため、匿名関数を関数の引数(値)として渡すことができます。 ある関数やメソッドの引数として渡される関数のことをコールバック関数と呼びます。 一方、コールバック関数を引数として使う関数やメソッドのことを高階関数と呼びます。

function 高階関数(コールバック関数) {
    コールバック関数();
}

コールバック関数は非同期処理においてもよく利用されます。

メソッド

JavaScriptではオブジェクトのプロパティ関数である場合にそれをメソッドと呼びます。JavaScriptにおいて、関数メソッドの機能的な違いはありません。 一般的にはメソッドも含めたもの関数と言い、関数宣言などとプロパティである関数を区別する場合にメソッドと呼びます。

// objのmethod1プロパティとmethod2プロパティに関数を定義した場合、obj.method1プロパティとobj.method2プロパティがメソッドになる
const obj = {
    method1: function(引数) {
        // `function`キーワードでのメソッド
        console.log('Hello World');
    },
    method2: (引数) => {
        // Arrow Functionでのメソッド
        console.log('Good Night');
    }
};
obj.method1();  // => Hello World
obj.method2();  // => Good Night
  • obj.method1プロパティとobj.method2プロパティがメソッドです。
// 空オブジェクトのobjを定義してから、methodプロパティへ関数を代入してもメソッドを定義できる
const obj = {};
obj.method = function() {
    return "this is method";
};
// メソッドを呼び出す際は、オブジェクト.メソッド名()とする
console.log(obj.method()); // => "this is method"
  • 空のオブジェクトobjを定義後、関数を値とするmethodプロパティをobjに代入すると、obj.methodプロパティがメソッドになります。
  • メソッドを呼び出す場合は、関数呼び出しと同様にオブジェクト名.メソッド名()と書くことで呼び出せます。

[ES2015] メソッドの短縮記法

ES2015からは、メソッドの短縮記法が追加されました。短縮記法はオブジェクトのメソッド定義だけではなく、クラスのメソッドを定義する共通の書き方になります。

const obj = {
    key: 'value', // keyとvalueのプロパティ
    method() { // メソッドの短縮記法で定義したメソッド
        // メソッドの処理
        return "this is method";
    }
};
console.log(obj.method()); // => this is method
console.log(obj.key); // => value
  • オブジェクトリテラルの中で メソッド名(){ /*メソッドの処理*/ }と記述するとメソッドの短縮記法が使えます。

メソッドまとめ

名前 関数 メソッド
関数宣言(function fn(){}) x
関数式(const fn = function(){})
Arrow Function(const fn = () => {})
メソッドの短縮記法(const obj = { method(){} }) x

メソッドの定義方法(3種類)

const obj = {
    // `function`キーワードを使ったメソッド
    method1: function() {
    },
    // Arrow Functionを使ったメソッド
    method2: () => {
    },
    // メソッドの短縮記法で定義したメソッド(関数ではない)
    method3() {
    }
};

参考

JSPrimer-関数と宣言