JavaScript/関数
関数( function )は、他の言語のサブルーチンやプロシージャと類似したサブプログラムです。JavaScriptでは関数もオブジェクト( Functionオブジェクト )であり第一級関数です[1]。
概要
[編集]関数は、0個以上のパラメータを受け取り、1つの戻り値を返す(あるいは返さない)プログラムの実行単位です。
多くのプログラム言語では、構文の一部として提供されている「命令」や「組込み関数」で入出力や数値演算などの機能を提供しますが、JavaScriptでは標準組込みクラスとホスト環境の提供するオブジェクトがその役割を担います。 またユーザーが自分で必要な関数やオブジェクトを定義できます。
JavaScriptの関数は function文を使って定義・作成します。
- 関数定義の構文
function関数名(引数1,引数2,…引数n){/* 処理内容; */[return[戻り値];]}
- 例
"use strict";function/*足し算*/add(a,b){returna+b;}console.log(`\typeof add = ${typeofadd}add.toString() = ${add.toString()}add(1, 2) = ${add(1,2)}add("abc", "xyz") = ${add("abc","xyz")}`);
- 実行結果
typeof add = function add.toString() = function /*足し算*/ add(a, b) { return a + b; } add(1, 2) = 3 add("abc", "xyz") = abcxyz
- 関数は、グローバルオブジェクト(windowやglobalThis)に束縛されます。
- 例えば、 add() は、 globalThis.add() です。
関数を呼出すには、add(1, 2)のように関数の名前の後に()を付けて、()の中に引数として関数に渡す値を入れます。つまり
- 構文
関数名([引数1[,引数2[...,引数n]]])
なお、上記の関数の内容は数値a, bを受け取り、aとbを足した結果sumを返すadd関数の例です。aやbのように関数が受け取るデータを引数( parameter )といいます。
作成した関数が値を返す場合、return文を使って返します。return文によって、return文の直後にある値を、呼出し元の関数に返します。また、その値を戻値( return value )といいます。上記コード例の場合なら a + b の式の値が戻値です。
関数でreturn文が実行されると、制御が呼出し元のコードに移るため、その関数のreturn文以降のコードは実行されません。
上記コード例の場合、関数 add が呼出されると、1はa、2はbに代入されて、関数の本体が実行され、+演算子式の評価結果が返されます。
aやbのように関数定義の引数を仮引数( argument )と呼びます。
数学関数
[編集]三角関数など、いくつかの数学の関数は、たとえばコサイン関数を呼出したい場合には、
- 数学メソッドの呼出例
Math.cos(引数)
- なお、引数の単位はラジアンです。
"use strict";console.log(`\Math.cos(Math.PI / 4) = ${Math.cos(Math.PI/4)}Math.cos(Math.PI / 4) ** 2 = ${Math.cos(Math.PI/4)**2}`)
- 実行結果
Math.cos(Math.PI / 4) = 0.7071067811865476 Math.cos(Math.PI / 4) ** 2 = 0.5000000000000001
- Mathオブジェクトは、標準組込みオブジェクトなので、importなどの特別な手続きなく、またホスト環境に関係なく使うことができます。
- また、Math.cos() などの数学メソッドは、Mathオブジェクトに属し、Mathオブジェクトをnewを使ってインスタンス化することはできません。
- Mathオブジェクトには円周率などの定数もMath.PIの形でプロパティとして用意されています。
関数式
[編集]関数はオブジェクト( object )の一種であり、ArrayオブジェクトやStringオブジェクトなど、他のオブジェクトと同じように操作できます。
- 構文
function(引数1[,引数2[,引数3]…[,引数n]]){// 処理内容[return[戻値];]}
- 例
"use strict";constadd=function(a,b){// 2つの引数をとり +演算子を適用して値を返す。returna+b;};functionfsub(){// 無名関数を返す関数returnfunction(a,b){// 関数オブジェクトの本体returna-b;};}constsub=fsub();console.log(`\add(1, 1) = ${add(1,1)}add.name = "${add.name}"typeof add = ${typeofadd}add.toString() = ${add.toString()}sub(1, 1) = ${sub(1,1)}sub.name = "${sub.name}"typeof sub = ${typeofsub}sub.toString() = ${sub.toString()}`);
- 実行結果
add(1, 1) = 2 add.name = "add" typeof add = function add.toString() = function (a, b) { // 2つの引数をとり +演算子を適用して値を返す。 return a + b; } sub(1, 1) = 0 sub.name = "" typeof sub = function sub.toString() = function (a, b) { // 関数オブジェクトの本体 return a - b; }
- 関数式では関数本体の関数名は省略可能で、省略された場合に関数式がスカラ変数の初期値であった場合 Function.nameはスカラ変数の変数名になります。
- 関数の戻り値で関数式を返した場合、Function.nameは "" となります。
関数式の呼出すときは、一般の関数と同様に呼出し元で ()
(関数呼出し演算子)を使います。
- 構文
関数式の値([引数1[,引数2[...,引数n]]])
関数スコープ
[編集]関数の中でvarキーワードを用いて宣言された変数は、関数の中からしか見えません。 言い換えれば、関数の中でvarキーワードを用いて宣言された変数と、関数の外で宣言された変数は別物です。
varf=function(){vari=0;returni+1;};console.log(f());// 1console.log(i);// 0 は表示されず ReferenceError: i is not defined となります。
関数ブロック内での var による変数宣言は、C言語など他言語でいう「ローカル変数」に似ていますがC言語には関数スコープはなく似て非なるものです。
なお、最後の2個のconsole.logの順番を入れ替えると、下記のようになります。
varf=function(){vari=0;returni;};vari=1;console.log(i);// 1console.log(f());// 0
関数ブロック外(トップレベル)での var による変数宣言は、C言語など他言語でいう「グローバル変数」に相当し、その実体はグローバルオブジェクト(典型的には window)のプロパティです。
varx=0;x===window.x;// true
ECMA2015(ES6)で{ }
で囲まれた範囲をスコープとする letと const が導入されました。 let とconst のスコープをブロックスコープと呼びます。
関数本体もブロックなので、let が関数の外から参照されることも有りませんしvar の様な巻き上げも起こりません。letについて詳しくは『JavaScript/変数#let』の節で説明しています。 letとconstが導入されても、varの意味が変わることは有りません。 もし変わったのならば深刻な非互換性を引き起こします。 逆に、var の意味論的な位置づけを保つ必要があったので新しく let と const が導入されたといえます。 なお、関数の内外でletを使用えますし推奨されます。 たとえば、いくつか前の節で紹介した足し算を関数にしたコード例の var を let に置き換えても同様の結果です。
let に置き換え
functionadd(a,b){letsum=a+b;returnsum;}letthree=add(1,2);// 1 + 2console.log(three);// 3
const に置き換え
functionadd(a,b){constsum=a+b;returnsum;}constthree=add(1,2);// 1 + 2console.log(three);// 3
一度しか代入されない変数は、const に置き換えることも出来ます。
このことから、変数の宣言には
- constに出来るか?
- letに出来るか?(クロージャが必要か)?
- 上記にあてはまらない場合に限り var で宣言
- 宣言せずのいきなりの代入は論外!
という一般則が成り立ちます。
var なし 関数の中で変数を宣言せず代入するとグローバル変数を置き換えます(下記コードのi = 0;
の箇所のことです)。
i=3;varf=function(){i=0;returni+1;};console.log(f());// 1console.log(i);// 0
グローバル変数が置き換えられているので、「3」ではなく(「3」はもはや置き換えによって値が失われた)、「0」が表示されます。
ですが、JavaScript では、このような用法は非推奨です。var のキーワード無しの変数宣言をJavaScriptは非推奨にしているからです。strictモードでは未宣言のグローバル変数への代入は ReferenceError になります。 strictモード下でグローバル変数にアクセスは、globalThis.i = 0;
の様にグローバルオブジェクトのプロパティとしてアクセスします。
globalThis.i=3;varf=function(){globalThis.i=0;returnglobalThis.i+1;};console.log(f());// 1console.log(globalThis.i);// 0
関数コンストラクタを使った関数の生成
[編集]function文による定義や関数式での生成とは別に、関数コンストラクタ[2]を使って関数を生成する方法もあります。 ただし、関数コンストラクタにより生成する方法は稀にしか使われず、function文か関数式を使うのが一般的です。
関数コンストラクタの構文
[編集][new]Function(引数1,引数2,関数の本体)
関数リテラルと似ていますが、最後の引数が関数の本体であるところが違います。 また、関数コンストラクタでは引数は全て文字列です。 加えて、関数コンストラクタはグローバルスコープで実行される関数のみを生成します。
- 例
constadd=newFunction('a','b','return a + b');console.log(add(1,1));// 1 + 1 == 2console.log(add.toString());/*function anonymous(a,b) {return a + b}*/
プロパティ
[編集]- Functionオブジェクトのプロトタイプです。
標準グローバル関数
[編集]再帰呼出し
[編集]再帰呼出しとは、関数が自分自身を呼出すことをいいます。
- 再帰呼出しの例
// 階乗 n!functionfactorial(n){returnn?n*factorial(n-1):1;}console.log(`factorial(5) = ${factorial(5)}`);// n 番目のフィボナッチ数functionfibonacci(n){returnn<2?n:fibonacci(n-2)+fibonacci(n-1);}console.log(`fibonacci(10) = ${fibonacci(10)}`);// a, b の最大公約数functiongcd(a,b){returnb?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){returnn?n*arguments.callee(n-1):1;})(5);// n 番目のフィボナッチ数(function(n){returnn<2?n:arguments.callee(n-2)+arguments.callee(n-1);})(10);// a, b の最大公約数(function(a,b){returnb?arguments.callee(b,a%b):Math.abs(a);})(42,56);
arguments.calleeプロパティを使用せずに無名再帰を行うには、不動点コンビネータ( fixed-point operator )を用います。
// Z不動点コンビネータvarZ=function(f){returnfunction(x){returnfunction(y){returnf(x(x))(y);};}(function(x){returnfunction(y){returnf(x(x))(y);};});};// 階乗 n!Z(function(f){returnfunction(n){returnn?n*f(n-1):1;};})(5);// n 番目のフィボナッチ数Z(function(f){returnfunction(n){returnn<2?n:f(n-2)+f(n-1);};})(10);// a, b の最大公約数Z(function(f){returnfunction(a){returnfunction(b){returnb?f(b)(a%b):Math.abs(a);};};})(42)(56);
ラムダ計算も参照してください。
アロー関数
[編集]関数リテラルのもう1つの構文にアロー関数構文があります。 アロー関数では、他の関数リテラル異なる this はアロー関数が宣言された場所によって決まます。
以下は全て同じ意味になります。
varf=function(x){return`x: ${x}`;}varf=Function('x','`x: ${x}`');varf=(x)=>{return`x: ${x}`;}varf=x=>{return`x: ${x}`;}varf=x=>`x: ${x}`;
前節の不動点コンビネータをアロー関数を使って書いてみます。
// Z不動点コンビネータvarZ=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){// 処理}
a=>0
は a>=0
の間違えですがエラーとはならず、 (a) => { return 0; }
と解され、Booleanコンテキストでは真となってしまったことが原因です。
クロージャ
[編集]クロージャ( レキシカルスコープ )で解決する関数のことです。教科書などによく出てくる典型的なクロージャは、次のようなカウンタ変数を用いた例です。
// 関数を返す関数functionf(){vari=0;returnfunction(){returni++;// ここで参照される i が問題};};constg=f();console.log(typeofg);// functionconsole.log(g());// 0console.log(g());// 1console.log(g());// 2vari=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(letindex=from;index<=to;index++){yieldindex;}}for(constnofRange(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 の様な反復動作/反復構造をユーザーが実現できるということだけ覚えてください。
- オブジェクトのメソッドとして再実装
letrange={start:0,end:Infinity,*[Symbol.iterator](){for(letvalue=this.start;value<=this.end;value++){yieldvalue;}}};range.start=2;range.end=4;for(constnofrange){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として再実装
classRange{constructor(start=0,end=Infinity){this.start=start;this.end=end;}*[Symbol.iterator](){for(letvalue=this.start;value<=this.end;value++){yieldvalue;}}forEach(f){for(letvalue=this.start;value<=this.end;value++){f(value);}}};r=newRange(2,4);r.forEach(x=>console.log(x));/* 2 3 4 */console.log([...newRange(1,10)]);// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]console.log(Array.from(newRange(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にならないvardream=true;console.log(dream);// true})();
- 上記コードでは表示結果として、trueが表示されます。
- なお、上記コードの様に無名関数を定義すると同時に呼ぶ出すことをは即時実行と呼びます。
- 関数内でvarで宣言した場合にだけ、巻き上げが発生します。
- 巻き上げが起きるのは、あくまで関数内での出来事であるので関数を用いてない場所では巻き上げは行われません。
- これに巻き込まれないよう、var宣言する前には変数を使わないように注意しましょう。
var
の使用にはこのように問題があるので、 var を使わずに const あるいは let を使うことが望ましいです。- const と let はブロックスコープで巻き上げは起こりません。
- また strict モードを使用することで、ミススペルなどで偶発的にvar相当の変数を無宣言で作ってしまうことをエラーを上げることで検知できます。
関数の巻上げ
[編集]関数にも巻上げが起こます。
- 関数定義
func("text");functionfunc(str){console.log(str);}
- 上記のコードは、func() が前方参照になっているにも関わらずエラーなく実行されます("text"がconsole.logされます)。
- 関数式でconstで宣言された変数を初期化
func("text");constfunc=function(str){console.log(str);}
3行目を関数式から関数リテラルに変えconstに保持すると、 ReferenceError: Cannot access 'func' before initialization となります。
- 関数式でvarで宣言された変数を初期化
func("text");varfunc=function(str){console.log(str);}
constをvarに変えると、TypeError: func is not a function。
- 関数式でletで宣言された変数を初期化
func("text");letfunc=function(str){console.log(str);}
let に変えると、ReferenceError: func is not defined となります。
このように、関数の巻上げは関数式では起こらず関数定義に限られます。
メソッド
[編集]オブジェクトのプロパティが関数の場合を、メソッドあるいは関数プロパティと呼びます。
constobj={val:42,func:function()/* プロパティvalの値をヘッダーつきで文字列化します */{return`val = ${this.val}`;},};console.log(obj.func());// "val = 42"consto2={val:99};o2.func=obj.func;console.log(o2.func());// "val = 99"console.log(o2.func.toString());// 下の3行が表示されます。// function() /* プロパティvalの値をヘッダーつきで文字列化します */ {// return `val = ${this.val}`;// }
メソッドは以下のように簡略表記ができます。
constobj={val:42,func()/* プロパティvalの値をヘッダーつきで文字列化します */{return`val = ${this.val}`;},};console.log(obj.func());// "val = 42"consto2={val:99};o2.func=obj.func;console.log(o2.func());// "val = 99"console.log(o2.func.toString());// 下の3行が表示されます。// func() /* プロパティvalの値をヘッダーつきで文字列化します */ {// return `val = ${this.val}`;// }
デフォルト引数
[編集]関数の引数が省略した場合の値を定義出来ます。
従来
functionVector3(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}`}}letv=newVector3(1,3);console.log(""+v);// x:1, y:3, z:0
ECMACScript 2015/ES6以降
functionVector3(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}`}}letv=newVector3(1,void0,7);console.log(""+v);// x:1, y:0, z:7
デフォルト引数とともにプロパティ名と同じ関数名による簡略表記をつかっています。 引数が省略された場合の他、引数の値に undefined
が渡された場合も既定値が渡されたとみなす。 void 0
は undefined
をタイプ数少なく書くイディオム。
残余引数
[編集]残余引数( Rest parameters )は、可変引数数関数を作る仕組みです[3]。 従来は arguments を使うところですが、strict モードで禁止になり非推奨なのでES6以降は残余引数を使います。
- 例
functionsum(...args){returnargs.reduce((result,current)=>result+current,0);}constary=[2,3,5,7,11];console.log(sum(...ary));// 28
- 残余引数構文、引数リストを配列として保持します。
- よく似ているがスプレッド構文で異なる構文です。
分割代入を使った引数
[編集]JavaScriptでは、ES6以降、分割代入という機能を使用して、引数の受け取り方をより柔軟にすることができます。
オブジェクト
[編集]具体的には、以下のように引数を分割代入することができます。
functiongreet({name,age}){console.log(`Hello, ${name}! You are ${age} years old.`);}constperson={name:'Alice',age:30};greet(person);
この場合、引数としてperson
オブジェクトを渡していますが、関数内では分割代入を使用して、name
とage
を抽出しています。
また、分割代入を使用して、デフォルト値を指定することもできます。
functiongreet({name='Anonymous',age=18}){console.log(`Hello, ${name}! You are ${age} years old.`);}greet({name:'Bob'});
この場合、name
には渡されたオブジェクトのname
プロパティが、age
にはデフォルト値の18
が設定されます。ただし、渡されたオブジェクトにage
プロパティがある場合は、その値が優先されます。
分割代入を使用することで、引数の受け取り方をより柔軟にすることができますが、あまり複雑にすると可読性が低下することもあるため、適切なバランスを保つ必要があります。
配列
[編集]JavaScriptの分割代入では、オブジェクトだけでなく配列に対しても使うことができます。また、残余引数(rest parameters)を使って、可変長引数を受け取ることもできます。これらを組み合わせることで、柔軟に引数を受け取ることができます。
例えば、以下のように可変長引数を受け取りつつ、先頭の要素を変数に分割代入することができます。
functiongreet([name,...others],message){console.log(`${message}, ${name}!`);console.log(`Other members: ${others.join(', ')}`);}constmembers=['Alice','Bob','Charlie'];greet(members,'Welcome');
この場合、greet
関数は配列を受け取り、先頭の要素をname
に、残りの要素をothers
に分割代入しています。また、第二引数としてmessage
を受け取っています。
このように分割代入と残余引数を組み合わせることで、可変長引数を受け取りつつ、必要な要素を柔軟に受け取ることができます。ただし、引数の受け取り方が複雑になりすぎないように、適切なバランスを保つ必要があります。
引数の数
[編集]関数の引数の数は、Function.prototype.length プロパティ[4]で得ることが出来ます。
- 例
functionadd(a,b){returna+b;}functionsum(...args){returnargs.reduce((result,current)=>result+current,0);}console.log(add.length);// 2console.log(sum.length);// 0!console.log([].forEach.length);// 1
- 普通に引数数2
- 残余引数構文
- 2を返します;普通です
- 残余引数構文は0を返します;1ではありません
- 組込み標準オブジェクトのメソッドにも使えます;1を返します;
Array.prototype.forEach()
は.forEach(callback(currentValue[, index[, array]]) [, thisArg]);
なので省略可能な引数の数は含まれません。
コンストラクタ
[編集]オブジェクトの生成と初期化のためのメソッドをコンストラクタと呼び、new 演算子と組み合わせて使われます。
functionComplex(real,imag=0){this.real=real;this.imag=imag;}letc=newComplex(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 宣言を使って書くと...
classComplex{constructor(real,imag=0){this.real=real;this.imag=imag;}toString(){return`r=${this.real}, i=${this.imag}`}}letc=newComplex(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を関数型言語とされることもありますが、主要な制御構造が式ではないので一般的な認識ではありません。
- ^コンストラクタとは違います
- ^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