JavaScriptの関数定義

こんばんは、south37です。

最近はEffective JavaScriptを読んでるのですが、知らなかった事がポロポロ出てくるので、とても勉強になっています。これからはちょいちょい、そーやって勉強した事をまとめていけたらいいなと思います。

って事で、今日は関数定義の話をしたいと思います。

関数宣言と関数式

JavaScriptにおける関数定義には、大きく分けて3つの方法があります。

  1. function文を使う方法(関数宣言)
  2. function演算子を使う方法(関数式)
  3. Functionコンストラクタを使う方法

1と2が一般的によく使われる方法ですね。

// 方法1
function multi(x, y) {
  return x * y;
}

// 方法2
var multi = function(x, y) {
  return x * y;
};
// これも方法2
var multi = function multiPly(x, y) {
  return x * y;
};

1と2はパッと見だと似た感じですが、意味としては違ってきます。1のfunction文を使う方法は関数宣言と呼ばれ、関数定義用に特別に用意された構文です。普通の文とは違い文末にセミコロン(;)がいらない事からも特別扱いなのが分かると思います。関数宣言で定義された関数はコンパイル時に生成される為、宣言前からでも使えます。

console.log(multi(6, 7));
function multi(x, y) {
  return x * y;
}
// 42が出力される

一方、function演算子を用いた関数定義では、演算子式の実行時に関数が生成されます。その為、演算子式より前では関数を使う事が出来ません。

console.log(multi(6, 7));
var multi = function(x, y) {
  return x * y;
}
// エラー!!!(まだ未定義の変数であるmultiを使用した為)

このfunction演算子を用いた式を関数式と呼びます。1のfunction文を用いた「関数宣言」と、2のfunction演算子を用いた「関数式」の違いは、意識しておいた方がいいかと思います。

ちなみに、関数もオブジェクトなので3の様にコンストラクタを使って生成出来る訳ですが、あまり見かけない構文になります。

var multiply = new Function("x", "y", "return x * y");
console.log(multiply(7, 6)); // 42

文字列で関数を定義出来る為、evalなんかと同じで強力な力を持ちます。多分、メタプロとかも出来ます。ただ、強力過ぎて扱いが難しい気もしますね。

名前付き関数式のスコープに注意

以下、名前付き関数式を使う時は注意しようって話です。細かい話なので読む必要はそんなに無いかもです。

関数式には名前をつける事も出来ます。その際つけた名前は、グローバルには存在しませんが関数定義の中では使う事が出来ます。例えば次の用に再帰的な関数を定義出来ます。

// factという名前付きの関数式
var f = function fact(n) {
  if (typeof n !== 'number' || isNaN(n) || n < 0) {
    return 0;
  } else if (n === 1) {
    return 1;
  } else {
    return n * fact(n - 1);
  }
};
console.log(f(4)); // 12
console.log(fact(4)); // エラー!!factという変数は存在しない。

ただ、これは大したメリットにはなりません。何故なら、名前無し関数式や関数宣言を使っても同じ事は出来るからです。

// 名前無し関数式
var fact = function(n) {
  if (typeof n !== 'number' || isNaN(n) || n < 0) {
    return 0;
  } else if (n === 1) {
    return 1;
  } else {
    return n * fact(n - 1);
  }
};

// 関数宣言
function fact(n) {
  if (typeof n !== 'number' || isNaN(n) || n < 0) {
    return 0;
  } else if (n === 1) {
    return 1;
  } else {
    return n * fact(n - 1);
  }
}

それどころか、名前無し関数式にはバージョンの違いによる仕様の違いという大きな問題があります。ECMAScript3では、名前付き関数式において関数名を変数として使える様にする為に、関数名をプロパティとして持つオブジェクトを、スコープとして使う事にしていました。その際、Object.prototypeからもプロパティを継承する仕様となっていた為に、関数定義内にObject.prototypeのプロパティも変数として存在する事になってしまっていました。

var constructor = function() { return null; };
var example = function f() {
  return constructor();
};
example(); // nullが返ってくる事を期待するが、{}が返される。

Object.prototypeにはconstructorプロパティがメソッドとして存在する為、外部で定義したconstructor関数は見えなくなり、nullでは無く空のオブジェクトが返される事になります。 現在はこの間違った仕様は修正され、多くのJavaScript環境であるべき振る舞いをするようになっています。

ただ、標準に準拠していない間違ったJavaScriptエンジンでは、名前付き関数式を関数宣言のように解釈してしまうものもあります。

var f = function g() { return 17; };
g(); // 17 (標準に準拠しない環境)

Effective JavaScriptでは、名前付き関数式の使用を開発中のデバッグ用途にしぼり、production版ではプリプロセッサで無名化する事を勧めていました。

argumentsを書き換えない

argumentオブジェクトは、JavaScriptがすべての関数に対して暗黙のうちに提供するローカル変数で、引数が配列状に格納されています。一般に可変数の引数を受け取る関数を定義したい時は、argumentsオブジェクトを使用する事になると思います。

function average() {
  for (var i = 0, sum = 0, n = arguments.length; i < n; i++) {
    sum += arguments[i];
  }
}

このargumentsオブジェクト、とても便利なのですが、書き換えようとすると途端にまずい事になります。

function callMethod(obj, method) {
  var shift = Array.prototype.shift;
  shift.call(arguments);
  shift.call(arguments);
  return obj[method].apply(obj, arguments);
}

shitメソッドをargumentsに対して呼び出す事でargumentsの1つ目と2つ目の要素を削除して左詰めし、残った要素を引数としてobj[method]を呼び出そうとしています。

var obj = {
  add: function(x, y) { return x + y; }
};
callMethod(obj, "add", 17, 25);
// エラー!! undefinedの"apply"プロパティを読み出せない

これは、argumentsオブジェクトが関数の引数のコピーでは無い為に起こります。実は、objやmethodはarguments[0]やarguments[1]の別名(alias)に過ぎません。argumentsオブジェクトが大元なのです。その為、上記の例では2回のshiftを経てarguments[0]に17が、arguments[1]に25が格納された状態となり、その為におかしな事になったのでした。

JavaScriptのstrictモードでは、また違った振る舞いとなります。すなわち、関数の名前付きパラメータがargumentsオブジェクトのaliasとならないのです。

function strict(x) {
  "use strict";
  arguments[0] = "modified";
  return x === arguments[0];
}
strict("unmodified"); // false
function unstrict(x) {
  arguments[0] = "modified";
  return x === arguments[0];
}
unstrict("unmodified"); // true

Effective JavaScriptでは、argumentsを書き換えた際の複雑な挙動を避ける為に、argumentsを決して書き換えないようにした方が良いと主張しています。変数を取り扱う為に変わりにしばしば使われるのが、次のようなイディオムです。

var args = Array.prototype.slice.call(arguments);

sliceはArrayオブジェクトを返すため、これで引数の内容を要素として持つarrayを生成する事が出来ます。argumentsに変更を加えずに済むだけでなく、Arrayのメソッドを使える為に取り扱いがぐっと楽になります。最初の例のcallMethod関数も簡単に定義出来ます。

function callMethod(obj, method) {
  var args = Array.prototype.slice.call(arguments, 2);
  return obj[method].apply(obj, args);
}

とりあえず今日はこんな感じです。

Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方

Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方