JavaScript 再入門(その13) 関数

関数の宣言

JavaScript には、関数の宣言方法が、大きく分けて3つの方法があります。

  • function を利用した関数宣言
  • 無名関数(関数式)での宣言
  • アロー関数式による宣言

function を利用した関数宣言

// 平均値を求める関数
function ave( argA , argB ) {
	return retArg = ( argA + argB ) / 2 ;
}
console.log( ave( 2 , 3 ) ) ; // 2.5

無名関数(関数式)での宣言

関数を変数に代入したり、関数の引数として使われます。

// 平均値を求める関数
const ave = function( argA , argB ) {
	return retArg = ( argA + argB ) / 2 ;
}
console.log( ave( 2 , 3 ) ) ; // 2.5

関数の引数としての使用例。
下の例は forEach の引数として関数を使用しています。

let ary = [ '車寅次郎' , '諏訪さくら' , '諏訪博' , '諏訪満男' , 'タコ社長' , '御前様' ] ;
ary.forEach(
	function( val , idx , ary) {
		if ( idx === 0 ) { console.log( ary ) ; } // (6) ['車寅次郎', '諏訪さくら', '諏訪博', '諏訪満男', 'タコ社長', '御前様']
		console.log( idx , val ) ;
	}
) ;
/*
(6) ['車寅次郎', '諏訪さくら', '諏訪博', '諏訪満男', 'タコ社長', '御前様']
0 '車寅次郎'
1 '諏訪さくら'
2 '諏訪博'
3 '諏訪満男'
4 'タコ社長'
5 '御前様'
*/

アロー関数式による宣言

アロー関数式 *ES2015 は、ES2015で追加された関数式宣言です。
function を使った関数式宣言とは、this の扱いや arguments オブジェクトを持たないことや、コンストラクタとして使用できないなど注意が必要です。

// 平均値を求める関数
const ave = ( argA , argB ) => {
	return retArg = ( argA + argB ) / 2 ;
}
console.log( ave( 2 , 3 ) ) ; // 2.5

アロー関数を関数の引数として使用

let ary = [ '車寅次郎' , '諏訪さくら' , '諏訪博' , '諏訪満男' , 'タコ社長' , '御前様' ] ;
ary.forEach(
	( val , idx , ary) => {
		if ( idx === 0 ) { console.log( ary ) ; } // (6) ['車寅次郎', '諏訪さくら', '諏訪博', '諏訪満男', 'タコ社長', '御前様']
		console.log( idx , val ) ;
	}
) ;
/*
(6) ['車寅次郎', '諏訪さくら', '諏訪博', '諏訪満男', 'タコ社長', '御前様']
0 '車寅次郎'
1 '諏訪さくら'
2 '諏訪博'
3 '諏訪満男'
4 'タコ社長'
5 '御前様'
*/

arguments オブジェクト

arguments オブジェクトは関数へ渡された値をすべて保持する配列のようなオブジェクトです。
平均値を求める関数を沢山の値をとれるように書き直します。

// 平均値を求める関数
const ave = function() {
	let sumVal = 0;
	for( arg of arguments ) { // 関数に渡された値を arguments オブジェクトから取得
		sumVal += arg ;
	}
	return retArg = sumVal / arguments.length ; // arguments オブジェクトは length プロパティを持ちます。
}
console.log( ave( 2 , 3 ) ) ; // 2.5
console.log( ave( 2 , 3 , 4 , 5 ) ) ; // 3.5

アロー関数では arguments オブジェクトを持たないため、結果が NaN になってしまいます。

// 平均値を求める関数
const ave = () => {
	let sumVal = 0;
	for( arg of arguments ) { // アロー関数では arguments オブジェクトを持たないため sumVal は 0 のままです。
		sumVal += arg ;
	}
	return retArg = sumVal / arguments.length ; // arguments オブジェクトを持たないため計算結果は NaN となります。
}
console.log( ave( 2 , 3 ) ) ; // NaN
console.log( ave( 2 , 3 , 4 , 5 ) ) ; // NaN

arguments オブジェクトは残余引数構文 … を使って代用することができます。
こちらのほうがスマートな印象ですね。

// 平均値を求める関数
const ave = function( ...args ) { // 残余引数構文を使って引数を取得。
	let sumVal = 0;
	for( arg of args ) {
		sumVal += arg ;
	}
	return retArg = sumVal / args.length ;
}
console.log( ave( 2 , 3 ) ) ; // 2.5
console.log( ave( 2 , 3 , 4 , 5 ) ) ; // 3.5

残余引数構文 … を使えばアロー関数でも同じ処理が実現できます。

// 平均値を求める関数
const ave = ( ...args ) => { // 残余引数構文を使って引数を取得。
	let sumVal = 0;
	for( arg of args ) {
		sumVal += arg ;
	}
	return retArg = sumVal / args.length ;
}
console.log( ave( 2 , 3 ) ) ; // 2.5
console.log( ave( 2 , 3 , 4 , 5 ) ) ; // 3.5

function を使った関数とアロー関数の this

アロー関数では this を持ちません。

下の例では、functionで宣言した obj.funcFunction の this値は、ローカルオブジェクトである obj を指していますが、アロー関数で宣言した obj.funcArrow はグローバルオブジェクトを指しています。
これは、アロー関数が this値を持たないからです。アロー関数の this値は実行モードにより変わります。non-strict モードではグローバルオブジェクトになりますが、strict モードでは undefined になります。
this値を使う処理にはアロー関数を使用しない方が this値が明確になります。

this.val = '車寅次郎' ;
var obj = {
	val : '渥美清' ,
	funcFunction : function() { return this.val ; } ,
	funcArrow : () => { return this.val ; } ,
}
console.log( obj.funcFunction() ) ; // 渥美清
console.log( obj.funcArrow() ) ; // 車寅次郎

bind / call / apply

functionで宣言した関数で this値を変更したい場合は bind / call / apply を使用します。
bind / call / apply は this値を設定するのは同じですが、bind は指定された this値で設定された新しい関数を生成しますが、他の call / apply は新しい関数を生成せずに即時に実行します。
bind は新しい関数を生成するので処理速度は遅くなりますが、this値を指定した関数を変数に保存して繰り返し使うことができます。イベントのコールバック関数への登録時に this値を変更する場合は bind でおこない、新しく作成された関数をコールバック関数へ登録すようにします。

this.val = '車寅次郎' ; // グローバルオブジェクトの val
var obj = {
	val : '渥美清' , // ローカルオブジェクトの val
	func : function() { return this.val ; } ,
}
console.log( obj.func() ) ; // 渥美清
console.log( obj.func.bind( this )() ) ; // 車寅次郎
const funcX = obj.func.bind( this ) ; // this値を指定した新たな関数を作成
console.log( funcX ) ; // 車寅次郎
console.log( obj.func.call( this ) ) ; // 車寅次郎
console.log( obj.func.apply( this ) ) ; // 車寅次郎
this.role = '御前様' ; // グローバルオブジェクト
this.actor = '笠智衆' ;
var tora = {
	role : '車寅次郎' ,
	actor : '渥美清' ,
}
var gen = {
	role : '源吉' ,
	actor : '佐藤蛾次郎' ,
}
var obj = {
	role : 'タコ社長' ,
	actor : '太宰久雄' ,
	func : function() { return this.role + ':' + this.actor ; } ,
}
console.log( obj.func() ) ; // タコ社長:太宰久雄
console.log( obj.func.call( this ) ) ; // 御前様:笠智衆
console.log( obj.func.call( tora ) ) ; // 車寅次郎:渥美清
console.log( obj.func.call( gen ) ) ; // 源吉:佐藤蛾次郎
// HTML のボタン定義
// <button id="test-button">テストボタン</button>
// ボタンをクリックした回数を表示する。
const proc = {
	counter : 0 , // カウンター
	clickCount :
		function(e) {
			++this.counter ; // カウンターを加算
			console.log(this.counter) ; // クリックした回数を表示
		} ,
}
// test-button のタグ要素を取得
let testBtn = document.getElementById("test-button") ;
// ボタンのイベントリスナー click に proc オブジェクトの clickCount プロパティの関数を登録する。
testBtn.addEventListener('click',proc.clickCount.bind(proc)); // <--- bind することでコールバック時の this を procオブジェクトに指定

bind / call / apply は引数の渡し方が違うので注意が必要です。
apply は配列を渡します。

this.val = '佐藤蛾次郎' ;
var obj = {
	val : '渥美清' ,
	func : function( arg ) { return [ arg ,this.val ] ; } ,
}
console.log( obj.func('源吉') ) ; // [ '源吉' , '渥美清' ]
// bind
console.log( obj.func.bind( this )( '源吉' ) ) ; // [ '源吉' , '佐藤蛾次郎' ]
const thisFunc = obj.func.bind( this ) ; // this値を指定した新たな関数を作成
console.log( thisFunc('源吉') ) ; // [ '源吉' , '佐藤蛾次郎' ]
// call
console.log( obj.func.call( this , '源吉' ) ) ; // [ '源吉' , '佐藤蛾次郎' ]
// apply
console.log( obj.func.apply( this , ['源吉'] ) ) ; // [ '源吉' , '佐藤蛾次郎' ]

アロー関数で宣言された関数は this値を持たないので bind / call / apply を使用しても this値は変わりません。

this.val = '佐藤蛾次郎' ;
var obj = {
	val : '渥美清' ,
	funcArrow : () => { return this.val ; } ,
}
console.log( obj.funcArrow() ) ; // 佐藤蛾次郎
console.log( obj.funcArrow.bind(obj)() ) ; // 佐藤蛾次郎 <--- bind しても this値は変わらない。

アロー関数で this は使わないほうが本当に良いのか?

アロー関数は this を持たないなら、アロー関数内の this の値はどこからくるのか。
アロー関数以外の関数では、this は呼び出し元に依存するため関数の実行時に動的に決まります。
アロー関数の this は、アロー関数内で定義されていないので、常に外側のスコープ(関数)へ this の定義を探しに行きます。

アロー関数以外のコールバック関数は、呼び出されるときのベースオブジェクトは動的に割り当てられます。
次の例では onclick イベントリスナーに、proc オブジェクトのプロパティ clickCount をコールバック関数に登録して this の値を表示しています。

const proc = {
	counter : 0 , // カウンター
	clickCount :
		function clickCount(e) {
			console.log(this) ; // <button id="test-button">テストボタン</button> <--- this の値は testBtn
		} ,
}
// test-button のタグ要素を取得
let testBtn = document.getElementById("test-button") ;
// ボタンのイベントリスナー click に proc オブジェクトの clickCount プロパティの関数を登録する。
testBtn.addEventListener('click',proc.clickCount);

コールバックされた proc.clickCounter の this の値は testBtn となっています。
コールバック関数 proc.clickCounter のベースオブジェクトは動的に割り当てられて testBtn となっています。
proc.clickCounter のベースオブジェクトを proc にするには、bind句で割り当てます。

testBtn.addEventListener('click',proc.clickCount.bind(proc));

これを、アロー関数で書き換えてみます。

const proc = {
	counter : 0 , // カウンター
	clickCount :
		function clickCount(e) {
			console.log(this) ; // <--- this の値は proc
		} ,
}
console.log(proc) ;
// test-button のタグ要素を取得
let testBtn = document.getElementById("test-button") ;
// ボタンのイベントリスナー click に proc オブジェクトの clickCount プロパティの関数を登録する。
testBtn.addEventListener('click', (e) => { proc.clickCount(e) } );

アロー関数で呼び出されたコールバック関数は this を持たないので、その外側のスコープの this を参照しますので proc となります。

アロー関数における this はアロー関数自身の外側に定義されたもっとも近い this となります。

これは、オブジェクト指向のコールバックには理想的な動作をします。
オブジェクトやクラスを使った、オブジェクト指向のコールバックには積極的に使うだけの意味はありそうです。

変数のスコープ

関数は外部変数にアクセス可能で変更も可能です。

role = '車寅次郎' ;
const func =  function() {
	actor = ( role == '車寅次郎' ) ? '渥美清' : '佐藤蛾次郎' ; // role 外部変数
	return role + ' : ' + actor ;
}
console.log( func() ) ; // 車寅次郎 : 渥美清
role = '車寅次郎' ;
const func =  function() {
	role = '源吉' ; // 外部変数を変更
	actor = ( role == '車寅次郎' ) ? '渥美清' : '佐藤蛾次郎' ;
	return role + ' : ' + actor ;
}
console.log( func() ) ; // 源吉 : 佐藤蛾次郎
console.log( role ) ; // 源吉 --> 外部変数が変更された
role = '車寅次郎' ;
const func =  function() {
	let role = '源吉' ; // ローカル変数 role を定義
	actor = ( role == '車寅次郎' ) ? '渥美清' : '佐藤蛾次郎' ;
	return role + ' : ' + actor ;
}
console.log( func() ) ; // 源吉 : 佐藤蛾次郎
console.log( role ) ; // 車寅次郎

仮引数のデフォルト値

関数の仮引数にはデフォルト値が設定できます。
デフォルト値が設定されずに関数に引数が渡されなかった場合は undefined が渡されます。
JavaScript は引数と仮引数の数が違っていてもエラーにはなりません。

const func = function ( arg ) { // 関数の仮引数は1つ
	return arg ;
}
console.log( func() ) ; // undefined --> 引数がない デフォルト値指定なしは undefined
console.log( func( '車寅次郎', 'タコ社長', '御前様' ) ) ; // 車寅次郎 --> 引数が仮引数よりも多い
const func = function ( arg = '源吉' ) { // 仮引数のデフォルト値
	return arg ;
}
console.log( func() ) ; // '源吉' --> 仮引数のデフォルト値
console.log( func( '車寅次郎' ) ) ; // 車寅次郎

仮引数のデフォルト値は関数でも構いません。

const argFunc = function() { return '源吉' ; }
const func = function ( arg = argFunc() ) {
	return arg ;
}
console.log( func() ) ; // '源吉'
console.log( func( '車寅次郎' ) ) ; // 車寅次郎

デフォルト値に、複雑なオブジェクトや配列などを設定する場合や、デフォルト値を再利用する必要がある場合は、関数化するほうが可読性が良くなります。

const argFunc = () => { // 仮引数の初期値を返します
	return (
		[
			{ role: '車寅次郎' , actor: '渥美清' } ,
			{ role: '諏訪さくら' , actor: '倍賞千恵子' } ,
			{ role: '諏訪博' , actor: '前田吟' } ,
			{ role: '諏訪満男' , actor: '吉岡秀隆' } ,
			{ role: 'タコ社長' , actor: '太宰久雄' } ,
			{ role: '御前様' , actor: '笠智衆' } ,
			{ role: '源吉' , actor: '佐藤蛾次郎' } ,
		]
	)
}
const argFuncX = () => { // 仮引数の初期値を返します
	return (
		{
			title: '男はつらいよ' ,
			director: '山田洋次' ,
			distributor: '松竹株式会社' ,
		}
	)
}
const func = function ( arg = argFunc() , argX = argFuncX() ) {
	return arg , argX ;
}

参考:
MDN 開発者向けのウェブ技術 > 関数宣言
MDN 開発者向けのウェブ技術 > 関数式
MDN 開発者向けのウェブ技術 > Function
MDN 開発者向けのウェブ技術 > アロー関数式
MDN 開発者向けのウェブ技術 > arguments
MDN 開発者向けのウェブ技術 > 残余引数
MDN 開発者向けのウェブ技術 > Strict モード
MDN 開発者向けのウェブ技術 > Function.prototype.bind()
MDN 開発者向けのウェブ技術 > Function.prototype.call()
MDN 開発者向けのウェブ技術 > Function.prototype.apply()
MDN 開発者向けのウェブ技術 > JavaScript「再」入門