JavaScript/関数
関数(かんすう、function)他の言語のサブルーチンやプロシージャと類似したサブプログラムです。JavaScriptでは関数もオブジェクト( Function )であり第一級関数です[1]。
概要編集
関数は、0個以上のパラメータを受け取り、1つの戻り値を返す(あるいは返さない)プログラムの実行単位です。
多くのプログラム言語では、構文の一部として提供されている「命令」や「組込み関数」で入出力や数値演算などの機能を提供しますが、JavaScriptでは標準組込みクラスとホスト環境の提供するオブジェクトがその役割を担います。 またユーザーが自分で必要な関数やオブジェクトを定義できます。
JavaScriptの関数は function文を使って定義・作成します。(なお、ウェブサービスに関連したプログラム言語でいうと、PHP(別のプログラム言語のひとつ)でも同様に関数の宣言のさいに function 文で定義する仕様である。なお、C言語の関数では、宣言の際にキーワード function は使いません)。
- JavaScriptにおける関数定義の構文
function 関数名(引数1, 引数2) { /* 処理内容; */ [return [ 戻り値 ];] }
- JavaScriptでのコード例
function /*足し算*/ add(a, b) { var sum = a + b; return sum; } console.log(typeof add) // function console.log(add.toString()) // コメントを含め関数本体 var three = add(1, 2); // 1 + 2 console.log(three); // 3
関数は、定義すると関数名を名前とする変数に保持されます。 関数は使用の際この変数を介し使われます[2]。
関数を呼び出すには、add(1, 2)のように関数の名前の後に()を付けて、()の中に引数として関数に渡す値を入れます。つまり
関数名([引数1[,引数2[..., 引数n]]])
を呼出し元のコードで記述するだけです。
なお、上記の関数の内容は数値a, bを受け取り、aとbを足した結果sumを返すadd関数の例です。aやbのように関数が受け取るデータを引数(ひきすう、parameter)といいます。
作成した関数が計算結果などを返す場合には、return文を使って返します。return文によって、return文の直後にある値を、呼び出し元の関数に返します。また、その値を戻り値(もどりち、return value)あるいは返り値(かえりち)といいます。上記コード例の場合なら sum が戻り値(返り値)です。
JavaScriptにかぎらずC言語などでも一般に、あるユーザー定義関数のreturn文が実行されると、制御が呼び出し元のコードに移るため、そのユーザー定義関数のreturn文以降のコードは実行されません。
上記コード例の場合、関数 add が呼び出されると、1はa、2はbに代入されて、関数の本体が実行され、足し算の結果が返されます。
aやbのように関数が受取る引数を仮引数(かりひきすう、parameter)、1や2のように関数呼出しに渡される引数を実引数(じつひきすう、argument)と呼びます。
数学関数編集
三角関数など、いくつかの数学の関数は、たとえばコサイン関数を呼び出したい場合には、
Math.cos(引数)
のようにして使います。
なお、引数の単位はラジアンです。たとえば
console.log(Math.cos(3.141));
なら、結果は -0.9999998243 くらいになります。
定義・作成の必要は無く、最初から用意されています。
これらの Math.〇〇 といった数学関数は、形式的には、Mathオブジェクトに所属する「メソッド」といわれる処理を呼び出している(メソッドは関数プロパティとも呼ばれます)。
たとえば Math.cos なら、Mathオブジェクトのcosメソッドを呼び出すという形式になっている。
Mathオブジェクトには円周率などの定数もプロパティとして用意されています。
console.log(Math.cos(Math.PI));
は、結果は正しく -1 を返します。
なお、Mathオブジェクトのメソッドは、インスタンスメソッドではなく静的メソッドなのでMathオブジェクトをコンストラクタとして new 演算子を適用してはならない。
関数式編集
JavaScriptにおける関数はオブジェクト(object)の一種であり、ArrayオブジェクトやStringオブジェクトなど、他のオブジェクトと同じように操作できます。
- 構文
function(引数1[,引数2[,引数3]..[,引数n]]){ // 処理内容 return 戻り値; }
- コード例
var add = function(a, b) { return a + b }; function fsub() { return function(a, b) { return a - b } } var sub = fsub() console.log(`add(1, 1) = ${add(1, 1)} add.name = "${add.name}" sub(1, 1) = ${sub(1, 1)} sub.name = "${sub.name}"`)
- 実行結果
add(1, 1) = 2 add.name = "add" sub(1, 1) = 0 sub.name = ""
- 関数式では関数本体の関数名は省略可能で、省略された場合に関数式がスカラ変数の初期値あるいはスカラ変数に代入されていた場合 Function.nameはスカラ変数の変数名になります。
- 関数の戻り値で関数式を返した場合、Function.nameは "" となります。
関数式の呼出すときは、一般の関数と同様に呼出し元で ()
(関数呼出し演算子)を使います。
- 構文
関数式の値([引数1[,引数2[..., 引数n]]])
関数スコープ編集
関数の中でvarキーワードを用いて宣言された変数は、関数の中からしか見えません。 言い換えれば、関数の中でvarキーワードを用いて宣言された変数と、関数の外で宣言された変数は別物です。
var f = function() {
var i = 0;
return i + 1;
};
console.log(f()); // 1
console.log(i); // 0 は表示されず ReferenceError: i is not defined となる。
関数ブロック内での var による変数宣言は、C言語など他言語でいう「ローカル変数」に似ていますがC言語には関数スコープはなく似て非なるものです。
なお、最後の2個のconsole.logの順番を入れ替えると、下記のようになります。
var f = function() {
var i = 0;
return i;
};
var i = 1;
console.log(i); // 1
console.log(f()); // 0
関数ブロック外(トップレベル)での var による変数宣言は、C言語など他言語でいう「グローバル変数」に相当し、その実体はグローバルオブジェクト(典型的には window)のプロパティです。
var x = 0;
x === window.x; // true
var と let そして const |
ECMA2015(ES6)で 関数本体もブロックなので、let が関数の外から参照されることも有りませんしvar の様な巻き上げも起こりません。letについて詳しくは『JavaScript/変数#let』の節で説明しています。 letとconstが導入されても、varの意味が変わることは有りません。 もし変わったのならば深刻な非互換性を引き起こします。 逆に、var の意味論的な位置づけを保つ必要があったので新しく let と const が導入されたといえます。 なお、関数の内外でletを使用えますし推奨されます。 たとえば、いくつか前の節で紹介した足し算を関数にしたコード例の var を let に置き換えても同様の結果です。 let に置き換え function add(a, b) {
let sum = a + b;
return sum;
}
let three = add(1, 2); // 1 + 2
console.log(three); // 3
const に置き換え function add(a, b) {
const sum = a + b;
return sum;
}
const three = add(1, 2); // 1 + 2
console.log(three); // 3
一度しか代入されない変数は、const に置き換えることも出来ます。 このことから、変数の宣言には
という一般則が成り立ちます。 |
var なし
関数の中で変数を宣言せず代入するとグローバル変数を置き換えます(下記コードのi = 0;
の箇所のことです)。
i = 3;
var f = function() {
i = 0;
return i + 1;
};
console.log(f()); // 1
console.log(i); // 0
グローバル変数が置き換えられているので、「3」ではなく(「3」はもはや置き換えによって値が失われた)、「0」が表示されます。
ですが、JavaScript では、このような用法は非推奨です。var のキーワード無しの変数宣言をJavaScriptは非推奨にしているからです。strictモードでは未宣言のグローバル変数への代入は ReferenceError になります。
strictモード下でグローバル変数にアクセスは、globalThis.i = 0;
の様にグローバルオブジェクトのプロパティとしてアクセスします。
globalThis.i = 3;
var f = function() {
globalThis.i = 0;
return globalThis.i + 1;
};
console.log(f()); // 1
console.log(globalThis.i); // 0
関数コンストラクタを使った関数の生成編集
function文による定義や関数式での生成とは別に、関数コンストラクタ[3]を使って関数を生成する方法もあります。 ただし、関数コンストラクタにより生成する方法は稀にしか使われず、function文か関数式を使うのが一般的です。
関数コンストラクタの構文編集
new Function(引数1, 引数2, 関数の本体);
※ new はなくても構いません。
関数リテラルと似ていますが、最後の引数が関数の本体であるところが違います。 また、関数コンストラクタでは引数は全て文字列です。 加えて、関数コンストラクタはグローバルスコープで実行される関数のみを生成します。
コード例
const add = new Function('a', 'b', 'return a + b');
console.log(add(1, 1)); // 1 + 1 == 2
console.log(add.toString());
/*
function anonymous(a,b
) {
return a + b
}
*/
プロパティ編集
- Functionオブジェクトのプロトタイプです。
標準グローバル関数編集
再帰呼出し編集
再帰呼出しとは、関数が自分自身を呼び出すことをいいます。
- 再帰呼出しの例
// 階乗 n! function factorial(n) { return n ? n * factorial(n - 1) : 1; } console.log(`factorial(5) = ${factorial(5)}`); // n 番目のフィボナッチ数 function fibonacci(n) { return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1); } console.log(`fibonacci(10) = ${fibonacci(10)}`); // a, b の最大公約数 function gcd(a, b) { return b ? gcd(b, a % b) : Math.abs(a); } console.log(`gcd(42, 56) = ${gcd(42, 56)}`);
- 実行結果
factorial(5) = 120 fibonacci(10) = 55 gcd(42, 56) = 14
脱出条件を間違えて無限再帰にならないように注意してください。
即時関数編集
即時関数(そくじかんすう)とは、定義後その場で評価される関数です。
(function(a, b){
console.log(a + b); // "3" と表示
})(1, 2);
無名再帰編集
無名関数の再帰を無名再帰(むめいさいき)または匿名再帰(とくめいさいき、anonymous recursion)といいます。JavaScriptで無名再帰を行うには、関数の中で自分自身を指すarguments.calleeプロパティを使用します。 strict モードでの、arguments.callee の使用は TypeError となります。
// 階乗 n!
(function(n){
return n
? n * arguments.callee(n - 1)
: 1;
})(5);
// n 番目のフィボナッチ数
(function(n){
return n < 2
? n
: arguments.callee(n - 2) + arguments.callee(n - 1);
})(10);
// a, b の最大公約数
(function(a, b){
return b
? arguments.callee(b, a % b)
: Math.abs(a);
})(42, 56);
arguments.calleeプロパティを使用せずに無名再帰を行うには、不動点コンビネータ(ふどうてんコンビネータ、fixed point combinator、不動点演算子、ふどうてんえんざんし、fixed-point operator)を用います。
// Z不動点コンビネータ
var Z = function(f) {
return function(x) {
return function(y) {
return f(x(x))(y);
};
}(function(x) {
return function(y) {
return f(x(x))(y);
};
});
};
// 階乗 n!
Z(function(f) {
return function(n) {
return n ?
n * f(n - 1) :
1;
};
})(5);
// n 番目のフィボナッチ数
Z(function(f) {
return function(n) {
return n < 2 ?
n :
f(n - 2) + f(n - 1);
};
})(10);
// a, b の最大公約数
Z(function(f) {
return function(a) {
return function(b) {
return b ?
f(b)(a % b) :
Math.abs(a);
};
};
})(42)(56);
ラムダ計算も参照してください。
アロー関数編集
関数リテラルのもう1つの構文にアロー関数構文がある。 アロー関数では、他の関数リテラル異なる this はアロー関数が宣言された場所によって決まる。
以下は全て同じ意味になる。
var f = function(x) { return `x: ${x}`; }
var f = Function('x','`x: ${x}`');
var f = (x) => { return `x: ${x}`; }
var f = x => { return `x: ${x}`; }
var f = x => `x: ${x}`;
前節の不動点コンビネータをアロー関数を使って書いてみる。
// Z不動点コンビネータ
var Z = f => (
x => y => f(x(x))(y)
)(x => y => f(x(x))(y))
// 階乗 n!
Z(f => n => n ? n * f(n - 1) : 1)(5)
// n 番目のフィボナッチ数
Z(f => n => n < 2 ? n : f(n - 2) + f(n - 1))(10)
// a, b の最大公約数
Z(f => a => b => b ? f(b)(a % b) : Math.abs(a))(42)(56)
簡素に書くことが出来ることが判ると思う。
比較演算子に擬態したアロー関数に注意 |
次のようなコードは常に処理が実行されます。 if (a=>0) {
// 処理
}
|
クロージャ編集
クロージャ(closure、閉包、へいほう)とは、引数以外のすべての変数を静的スコープ(せいてきスコープ、static scoping、構文スコープ、lexical scoping、レキシカルスコープ)で解決する関数のことです。教科書などによく出てくる典型的なクロージャは、次のようなカウンタ変数を用いた例です。
// 関数を返す関数
function f() {
var i = 0;
return function() {
return i++; // ここで参照される i が問題
};
};
const g = f();
console.log(typeof g); // function
console.log(g()); // 0
console.log(g()); // 1
console.log(g()); // 2
var i = 0; // グローバルな i を書き換えても
console.log(g()); // 3 -- 値は変わらない
関数fは変数iをインクリメントして返す関数を返す関数です。f()によって生成された関数gを呼び出すと、iの値が0, 1, 2, ...と1ずつ増やして返されます。ここでiの値を書き換えても、変数gが示す関数に束縛されたiの値は変わりません。変数gが示す関数と環境はクロージャになっているからです(gを呼び出したときではなく、gが示す関数を定義したときのiを参照している)。
f() 中の変数iに注目してください。iは外側の関数式の中でvarキーワードを用いて宣言されているので、関数スコープになり、関数定義を出た時点で消滅します。しかし、関数gを呼び出すとiをインクリメントした値が返ってきます。繰り返しになりますが、クロージャとはすべての変数を、呼び出した時点ではなく定義した時点で束縛した関数と環境のことでした。f() が呼び出され内側の関数式を返した時点のiが束縛されたので、グローバルなiが見えなくなっても環境のiの値を参照しつづけられるというわけです。
ジェネレーター関数編集
ジェネレーター関数は、Generator オブジェクトを返す特殊な関数です。
- ジェネレーター関数定義
- 書式:
function* 関数名(引数列) { 処理 }
- ジェネレーター関数式
- 書式:
function* (引数列) { 処理 }
返されたGenerator オブジェクトは反復動作(例えば for..of)と反復構造(例えばスプレッド構文 ...iter)の両方をサポートします。
JavaScriptにありそうでないRangeオブジェクトを作ってみます。
- ジェネレーター関数によるRangeの実装
function* Range(from = 0, to = Infinity) { for (let index = from; index <= to; index++) { yield index; } } for (const n of Range(2, 4)) { console.log(n) } /* 2 3 4 */ console.log([...Range(1, 10)]); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] console.log(Array.from(Range(0, 12))); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
yield
演算子は(ジェネレータではない)関数の return に相当する制御演算子です。反復動作/反復構造については、改めて詳しく説明したいと思います。
ここでは、ジェネレーター関数/Generatorオブジェクトを定義することで Array や Set の様な反復動作/反復構造をユーザーが実現できるということだけ覚えてください。
- オブジェクトのメソッドとして再実装
let range = { start: 0, end: Infinity, *[Symbol.iterator]() { for (let value = this.start; value <= this.end; value++) { yield value; } } }; range.start = 2; range.end = 4; for (const n of range){ console.log(n) } /* 2 3 4 */ range.start = 1; range.end = 10; console.log([...range]); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] range.start = 0; range.end = 12; console.log(Array.from(range)); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
- classとして再実装
class Range { constructor(start = 0, end = Infinity) { this.start = start; this.end = end; } *[Symbol.iterator]() { for (let value = this.start; value <= this.end; value++) { yield value; } } forEach(f) { for (let value = this.start; value <= this.end; value++) { f(value); } } }; r = new Range(2, 4); r.forEach(x => console.log(x)); /* 2 3 4 */ console.log([...new Range(1,10)]); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] console.log(Array.from(new Range(0,12))); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
- この例では、forEach メソッドも定義し中間配列を生成せずRangeオブジェクト自身で反復を可能にしています。
- 他の配列の操作(map,filter,find,some,every)も同様に実装することで中間配列を撲滅できます。
varの巻上げ編集
var
で宣言された変数を関数内で使う場合に、値の代入は代入した場所で行われるが、宣言(および宣言に伴う代入)は関数内のどこでしても関数の先頭でしたことになるという落とし穴が存在します。この挙動はvar
の巻き上げ(var
hoisting)と呼ばれます。
(function() {
console.log(dream); // undefined -- なぜかReferenceErrorにならない
var dream = true;
document.write(dream); // true
})();
上記コードでは表示結果として、trueが表示されます。
なお、上記コードの関数は、無名関数と言われる種類の関数です。
let では、巻き上げは発生しないです。キーワード無しでも巻き上げは発生しないです。
関数内でvarで宣言した場合にだけ、巻き上げが発生します。
巻き上げが起きるのは、あくまで関数内での出来事であるので関数を用いてない場所では巻き上げは行われません。
これに巻き込まれないよう、var宣言する前には変数を使わないように注意しましょう。
var
にはこういった問題があります。なので、どうしてもvarを使う場合には「常に関数の先頭で全部の変数を宣言するようにし[4]、その冒頭で必要な値の代入も済ましておく」という(まるでPASCAL言語の変数宣言のような)方法を取るのが安全です。
この手法は必要な場所のすぐ近くで変数を宣言するという他の言語のプログラマーに一般的にあるノウハウとは、JavaScriptの巻き上げ防止ノウハウのそれとは異なっている[5]ことに、私達プログラマーは留意する必要があります。
あるいは、そもそも var を使わずに const あるいは let で済ますという手もあります。const と let はブロックスコープで巻き上げは起こらない。
関数の巻上げ編集
関数にも巻上げが起こる。
- 関数定義
func("text"); function func(str) { console.log(str); }
上記のコードは、func() が前方参照になっているにも関わらずエラーなく実行される("text"がconsole.logされる)。
- 関数式でconstで宣言された変数を初期化
func("text"); const func = function (str) { console.log(str); }
3行目を関数式から関数リテラルに変えconstに保持すると、 ReferenceError: Cannot access 'func' before initialization となる。
- 関数式でvarで宣言された変数を初期化
func("text"); var func = function (str) { console.log(str); }
constをvarに変えると、TypeError: func is not a function。
- 関数式でletで宣言された変数を初期化
func("text"); let func = function (str) { console.log(str); }
let に変えると、ReferenceError: func is not defined となる。
このように、関数の巻上げは関数式では起こらず関数定義に限られる。
メソッド編集
オブジェクトのプロパティが関数の場合を、メソッドあるいは関数プロパティと呼びます。
const obj = {
val: 42,
func: function() /* プロパティvalの値をヘッダーつきで文字列化します */ {
return `val = ${this.val}`;
},
};
console.log(obj.func()); // "val = 42"
const o2 = { val: 99 };
o2.func = obj.func;
console.log(o2.func()); // "val = 99"
console.log(o2.func.toString()); // 下の3行が表示されます。
// function() /* プロパティvalの値をヘッダーつきで文字列化します */ {
// return `val = ${this.val}`;
// }
メソッドは以下のように簡略表記ができます。
const obj = {
val: 42,
func() /* プロパティvalの値をヘッダーつきで文字列化します */ {
return `val = ${this.val}`;
},
};
console.log(obj.func()); // "val = 42"
const o2 = { val: 99 };
o2.func = obj.func;
console.log(o2.func()); // "val = 99"
console.log(o2.func.toString()); // 下の3行が表示されます。
// func() /* プロパティvalの値をヘッダーつきで文字列化します */ {
// return `val = ${this.val}`;
// }
デフォルト引数編集
関数の引数が省略した場合の値を定義出来ます。
従来
function Vector3( x, y, z ) {
this.x = x || 0;
this.y = y || 0;
this.z = z || 0;
this.toString = function() { return `x:${this.x}, y:${this.y}, z:${this.z}` }
}
let v = new Vector3(1, 3);
console.log("" + v); // x:1, y:3, z:0
ECMACScript 2015/ES6以降
function Vector3( x = 0, y = 0, z = 0 ) {
Object.assign(this, {x, y, z });
this.toString = function() { return `x:${this.x}, y:${this.y}, z:${this.z}` }
}
let v = new Vector3(1, void 0, 7);
console.log("" + v); // x:1, y:0, z:7
デフォルト引数とともにプロパティ名と同じ関数名による簡略表記をつかっている。
引数が省略された場合の他、引数の値に undefined
が渡された場合も既定値が渡されたとみなす。
void 0
は undefined
をタイプ数少なく書くイデオム。
残余引数編集
残余引数(Rest parameters)は、可変引数数関数を作る仕組みです[6]。 従来は arguments を使うところですが、strict モードで禁止になり非推奨なのでES6以降は残余引数を使います。
使用例
function sum(...args) {
return args.reduce((result, current) => result + current, 0);
}
const ary = [ 2, 3, 5, 7, 11 ];
console.log(sum(...ary)); // 28
- 残余引数構文、引数リストを配列として保持する。
- よく似ているがスプレッド構文で異なる構文です。
引数の数編集
関数の引数の数は、Function.prototype.length プロパティ[7]で得ることが出来ます。
使用例
function add(a, b) {
return a + b;
}
function sum(...args) {
return args.reduce((result, current) => result + current, 0);
}
console.log(add.length); // 2
console.log(sum.length); // 0!
console.log([].forEach.length); // 1
- 普通に引数数2
- 残余引数構文
- 2を返します;普通です
- 残余引数構文は0を返します;1ではありません
- 組込み標準オブジェクトのメソッドにも使えます;1を返します;
Array.prototype.forEach()
は.forEach(callback(currentValue[, index[, array]]) [, thisArg]);
なので省略可能な引数の数は含まれません。
コンストラクタ編集
オブジェクトの生成と初期化のためのメソッドをコンストラクタと呼び、new 演算子と組み合わせて使われます。
function Complex(real, imag = 0) {
this.real = real;
this.imag = imag;
}
let c = new Complex(10, 14);
console.log(c); // Complex {real: 10, imag: 14}
Complex.prototype.toString = function() {
return `r=${this.real}, i=${this.imag}`;
}
console.log(c.toString()); // r=10, i=14
同等の機能を ES6 で追加された class 宣言を使って書くと...
class Complex {
constructor(real, imag = 0) {
this.real = real;
this.imag = imag;
}
toString() { return `r=${this.real}, i=${this.imag}` }
}
let c = new Complex(10, 14);
console.log(c); // Complex {real: 10, imag: 14}
console.log(c.toString()); // r=10, i=14
のようになります。
一見すると prototype は関係ないように見えますが
console.log(Complex.prototype.toString.toString()) // "toString() { return `r=${this.real}, i=${this.imag}` }"
と、実体は function をコンストラクタに使った記法と同じく prototype プロパティにメソッドが追加されています。
脚注編集
- ^ このことから、JavaScriptを関数型言語とされることもありますが、主要な制御構造が式ではないので一般的な認識ではありません。
- ^ グローバル関数の正体は、グローバルオブジェクト(windowやglobal)のメソッド、グローバル変数はグレーバルオブジェクトのプロパティです
- ^ コンストラクタとは違います
- ^ 山田祥寛『改訂新版 JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで~ 』、技術評論社、2019年8月17日 初版 第6刷、189ページ
- ^ 山田祥寛『改訂新版 JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで~ 』、技術評論社、2019年8月17日 初版 第6刷、189ページ
- ^ https://262.ecma-international.org/#sec-functiondeclarationinstantiation ECMAScriptR 2020 Language Specification :: 9.2.10 FunctionDeclarationInstantiation ( func, argumentsList )
- ^ https://262.ecma-international.org/#sec-function-instances-length ECMAScriptR 2020 Language Specification :: 19.2.4.1 length