Chapter 4. Functions
2011/01/03(月) 16:53 Javascript親記事へこのエントリーをはてなブックマークに追加

function expressions("関数式") and function declarations("関数宣言").

Background

Functions(関数)はオブジェクト
functionがオブジェクトであることは次のことからも見えると思います。
new Function()コンストラクタを使うことで関数オブジェクトを作成できるが、
これはeval的な感じ(Avoiding eval())なので避けるべきであるし、何より書きにくいです。
// antipattern
// for demo purposes only
var add = new Function('a, b', 'return a + b');
add(1, 2); // returns 3
もう一つ関数で重要なことはスコープを提供するということです。
JavaScriptはifやforなどでブロックスコープができるわけではない。(ブロックはスコープを作らない)

Disambiguation of Terminology

"名前付き関数式"と"関数式"
次のような関数定義についてみてみましょう。
// 名前付き関数式
var add = function add(a, b) {
    return a + b;
};
これような関数式を一般的には関数式に出てくる2回目のaddをなくして無名関数で書くことが多いと思います。
// 関数式 a.k.a. 無名関数
var add = function (a, b) {
    return a + b;
};
"名前付き関数式"はnameプロパティを定義してくれるため、あるとデバッガーの役に立ちます。
var add = function hoge(a, b) {
    return a + b;
};
add.name;// hoge
ただし、IEではhogeがスコープ漏れするので使いにくい。
ナビ子記法のような書き方をするとデバッガーにも優しい書き方ができる

Function’s name Property

関数オブジェクトはreadonlyなnameプロパティを持っている。
無名関数の場合のみundefined(IE),""(Firefox,Webkit)になる。
function foo() {} // declaration
var bar = function () {}; // expression
var baz = function baz() {}; // named expression
foo.name; // "foo"
bar.name; // ""
baz.name; // "baz"
さっきから言ってるけどデバッガーにとってはnameプロパティがあるとうれしい。
関数内でエラーが起きたときにnameプロパティを参照して利用しているからだ。
まだnameプロパティは関数内で自信を再帰呼び出しする際にも利用できる。

Function Hoisting

関数式でもHoistingは同様に起こります。
以下のようなコードをみてみるとわかりやすいでしょう
// antipattern
// global functions
function foo() {
    alert('global foo');
}
function bar() {
    alert('global bar');
}
function hoistMe() {
    console.log(typeof foo); // "function"
    console.log(typeof bar); // "undefined"
    foo(); // "local foo"
    bar(); // TypeError: bar is not a function
    // 関数宣言
    function foo() {
        alert('local foo');
    }
    // 関数式 => Hoisting
    var bar = function () {
        alert('local bar');
    };
}
hoistMe();

Callback Pattern

引数として関数を渡して実行させる方法
dosomething(function(){
    // callback code
});

Callbacks and Scope

callbackとして呼ばれる関数がthisに依存した書き方をされている予期せぬ動作になったりします。
たとえば下のような時にfindNodes(myapp.paint)とmyapp.paintをコードバックとして渡すと、その中でthisを参照しているためwindow.colorをみてしまいます。
var myapp = {};
myapp.color = "green";
myapp.paint = function (node) {
    node.style.color = this.color;
};
var findNodes = function (callback) {
    // ...
    if (typeof callback === "function") {
        callback(found);
    }
    // ...
};
このような場合はthisを指定できる、call()やapply()を使いcallback関数を呼び出すことで解決できます。
findNodes(myapp.paint, myapp);と呼ぶとき。
var findNodes = function (callback, callback_obj) {
    //...
    if (typeof callback === "function") {
        callback.call(callback_obj, found);
    }
   // ...
};

Asynchronous Event Listeners

callbackパターンは非同期イベントでよく見かけると思います。
// Clickイベント
document.addEventListener("click", console.log, false);

Returning Functions

functionはfunctionを返すこともできる。
いわゆるクロージャー
var setup = function () {
    var count = 0;
    return function () {
        return (count += 1);
    };
};
// usage
var next = setup();
next(); // returns 1
next(); // 2
next(); // 3

Self-Defining Functions

関数内で自分自身を書き換えるパターン
このパターンは初回のみの動作と次からの動作を変えることができる。
"lazy function definition"とも言われます。
var scareMe = function () {
    alert("Boo!");
    scareMe = function () {
        alert("Double boo!");
    };
};
// using the self-defining function
scareMe(); // Boo!
scareMe(); // Double boo!
このパターンの欠点はオリジナルの関数に上書きして作るため、オリジナルの関数に加えたものはどこか言ってしまう。
var scareMe = function () {
    alert("Boo!");
    scareMe = function () {
        alert("Double boo!");
    };
};
// 1. プロパティを加える
scareMe.property = "properly";

// using the self-defining function
console.log(scareMe.property);// "properly"
scareMe(); // Boo!
console.log(scareMe.property);// undefined
scareMe(); // Double boo!

Immediate Functions

即時実行のパターン。
(function () {
    alert('watch out!');
}());

Parameters of an Immediate Function

即時実行にパラメーターを渡してやるとより便利になる。
globalとかwindowとかundefinedなどを受け取ったりするのがスマートに書ける。
(function (global, undefined) {
    // access the global object via `global`
}(this));

Returned Values from Immediate Functions

即時実行の結果を変数に入れルこともできます。
var result = (function () {
    return 2 + 2;
})();
(個人的にはこの書き方でtrim関数定義するのスキ
var trim = (function(regexp){
    return function(str){
        return str.replace(regexp, '');
    };
})(/^\s+|\s+$/g);
この書き方を利用してオブジェクトでプロパティ名:プロパティ値の形式で書くのにも利用できます。
var o = {
    message: (function () {
        var who = "me",
            what = "call";
        return what + " " + who;
    }()),
    getMsg: function () {
        return this.message;
    }
};

Benefits and Usage

即時実行のパターンはグローバル汚染を心配しなくてもいいなどの理由から広く使われています。
またどんなページで実行するかわからないブックマークレットも、グローバル名前空間をきれいに保つためにも利用するべきです。
一つ一つが分離できるため、module的に一つのファイルにして使う場合にも使いやすいです。
// module1 defined in module1.js
(function () {
  // all the module 1 code ...
}());

Immediate Object Initialization

即時実行のパターンは関数オブジェクトでしたが、同様にinit()を持ったオブジェクトを使って初期化を行うパターン。
({
    // この辺にいろいろ値を定義
    // a.k.a. configuration constants
    maxwidth: 600,
    maxheight: 400,
    // you can also define utility methods
    gimmeMax: function () {
        return this.maxwidth + "x" + this.maxheight;
    },
    // initialize
    init: function () {
        console.log(this.gimmeMax());
        // more init tasks...
    }
}).init();
このパターンは次のようにどちらの書き方でも書けます
({...}).init();
({...}.init());
この書き方も即時実行のパターンと同様にグローバル名前空間を汚染せずに初期化処理を書くことができます。必要ならヘルパー関数をこのオブジェクトに定義したりして利用できる。
このパターンの欠点はJavaScriptの圧縮などを行うツールがこのパターンを効率よく圧縮してくれないことがあります。具体的にはプライベートなプロパティやメソッドをsafetyではないと判断して短い名前に変えてくれないことがあります。
Google's Closure Compilerで“advanced” modeを使う事でプライベートなものも短い名前に圧縮できます。
({d:600,c:400,a:function(){return this.d+"x"+this.c},b:function(){console.log(this.
a())}}).b();

Init-Time Branching

またの名をload-time branching。
この最適化のパターンをよく見かけるのはイベントリスナーをつける関数などです。
イベントをつける関数やXHRはブラウザ間で違いが存在するためラップする関数を作成すると思います。
下のように関数内で場合分けした時は呼び出すたびにその判定を行ってしまいます。
// BEFORE
var utils = {
    addListener: function (el, type, fn) {
        if (typeof window.addEventListener === 'function') {
            el.addEventListener(type, fn, false);
        } else if (typeof document.attachEvent === 'function') { // IE
            el.attachEvent('on' + type, fn);
        } else { // older browsers
            el['on' + type] = fn;
        }
    },
    removeListener: function (el, type, fn) {
        // pretty much the same...
    }
};
呼び足すたびに毎回判定してはあまり効率よくないので、これを変えて最初の一度だけ判定して次からは判定なしに利用できるようにするパターンをInit-Time Branchingといいます。
ただ関数宣言とは違って、利用するより前の位置に定義しないといけない。
// AFTER
// the interface
var utils = {
    addListener: null,
    removeListener: null
};
// the implementation
if (typeof window.addEventListener === 'function') {
    utils.addListener = function (el, type, fn) {
        el.addEventListener(type, fn, false);
    };
    utils.removeListener = function (el, type, fn) {
        el.removeEventListener(type, fn, false);
    };
} else if (typeof document.attachEvent === 'function') { // IE
    utils.addListener = function (el, type, fn) {
        el.attachEvent('on' + type, fn);
    };
    utils.removeListener = function (el, type, fn) {
        el.detachEvent('on' + type, fn);
    };
} else { // older browsers
    utils.addListener = function (el, type, fn) {
        el['on' + type] = fn;
    };
    utils.removeListener = function (el, type, fn) {
        el['on' + type] = null;
    };
}
このパターンはXMLHttpRequestやaddEventListenerなどでよく見られる。

Function Properties—A Memoization Pattern

関数の実行結果をキャッシュしておくパターンはメモ化として知られています。
var myFunc = function (param) {
    if (!myFunc.cache[param]) {// キャッシュがないなら
        var result = {};
        // ... expensive operation ...
        myFunc.cache[param] = result;
    }
    return myFunc.cache[param];
};
// キャッシュを入れておく場所
myFunc.cache = {};
上のmyFuncだと引数にstringが必要ですが、JSON.stringifyを使ってシリアライズしたものをキーに使用することで引数がオブジェクトでも対応できます。
var myFunc = function () {
    // シリアライズ
    var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)),
    result;
    if (!myFunc.cache[cachekey]) {
        result = {};
        myFunc.cache[cachekey] = result;
    }
    return myFunc.cache[cachekey];
};
// cache storage
myFunc.cache = {};
ただしこの方法は同一のプロパティを持ち異なるオブジェクトを引数としたときでも同じキャッシュが使われてしまう。
var foo = {};
foo.hoge = "new property";
foo.baz = 3;

var bar = {};
bar.hoge = "new property";
bar.baz = 3;
(foo == bar);// false
(JSON.stringify(foo) === JSON.stringify(bar));// true

Configuration Objects

引数がたくさん必要な関数を作った場合、呼び出す際に煩わしいことになるため、オブジェクトを引数にして渡すパターン
var conf = {
    username: "batman",
    first: "Bruce",
    last: "Wayne"
};
addPerson(conf);
このパターンを使えば、引数をオブジェクトにまとめて関数に渡せるため、引数の順番は関係ないし、オプション的な引数も簡単にスキップできる。
引数が複雑すぎる関数自体あまり作るべきではないけど、どうしても複雑になる場合はオブジェクトでやりとりするのがよい。

Curry

いわゆるカリー化

Partial Application

x+yという足し算をやりたいときにadd(x,y);みたいに一度にではなくてxは何,yは何とステップごとにやりたいときにカリー化のパターンが出てくる。add(x)(y)
下のように分割して計算を行えるpartialApply()について考えていく
// partial application
var newadd = add.partialApply(null, [5]);
// applying an argument to the new function
newadd.apply(null, [4]); // 9

Currying

上のようなadd関数を単純に実現するには関数を返す関数を定義すればいい。
// accepts partial list of arguments
function add(x, y) {
    if (typeof y === "undefined") { // partial
        return function (y) {
            return x + y;
        };
    }
    // full application
    return x + y;
}
ただあまり汎用的ではないので、argumentsを使いもっと一般化する
function schonfinkelize(fn) {
    var slice = Array.prototype.slice,
        stored_args = slice.call(arguments, 1);
    return function () {
        var new_args = slice.call(arguments),// 2回目の引数
            args = stored_args.concat(new_args);// 1回目と2回目の引数を結合
        return fn.apply(null, args);
    };
}
// 普通のadd関数
function add(x, y) {
    return x + y;
}
// カリー化したadd関数を作る
var newadd = schonfinkelize(add, 5);
newadd(4); // 9

When to Use Currying

これってどんな時に使うの?
同じ関数を何回も呼んでいる時に引数の大部分が一緒だった場合カリー化を使うチャンスです。
カリー化してできた関数は最初の引数を保存してるので、引数の一部を省略できます。

Summary

いろんな関数呼び出しのパターンをやった
カリー化以外は知らなくても意外とやったことがある人多そう。

名前:  非公開コメント   

  • TB-URL  http://efcl.info/adiary/091/tb/