LoginSignup
17
19

More than 5 years have passed since last update.

オブジェクト指向初心者の私がJavaScriptにも再入門 「関数・メモ化・カリー化・bind編」 〜JavaScriptパターン 学習8日目【逃げメモ】〜

Last updated at Posted at 2016-06-05

2016/6/6改訂

関数

メモ化

関数はオブジェクトなのでプロパティを持てます。そのプロパティにその関数の結果を格納しておけば、いちいち毎回計算しなくても値として格納しているものを読み込むだけなので、速度が速くなりますよね、というものです。

計算が難しく、重たいものであればあるほど効力を発揮します。今回のソースコードでは難しい計算はさせていませんが、どういうものかイメージはつけられるかと思います。

メモ化
//関数式定義
var fsFunc = function(val) {

    // 変数宣言と引数の値をキャッシュのキーとして使う
    var cacheKey = val,
        result;

    //この関数オブジェクトのcacheプロパティにキーがない場合
    if (!fsFunc.cache[cacheKey]) {

        //計算結果を入れるオブジェクト生成
        result = {};

        //0-1000まで繰り返す
        for (var i = 0; i <= 1000; i += 1) {

            //謎の計算式(なんでも良い)を行いオブジェクトにキー:i、値:計算結果として格納
            result[i] = i * val * 26 * 234 / 47 + 3; 

        }

        //この関数オブジェクトのcacheプロパティに計算結果を追加
        fsFunc.cache[cacheKey] = result;

        //コンソールにcacheに新たに追加されたよと通知
        console.log(cacheKey + ":オブジェクト追加");

    }

    //今から返すよとコンソールに出力
    console.log(cacheKey + ":のオブジェクトを返す")

    //関数の戻り値にキー:cacheKeyを持つオブジェクトを返す
    return fsFunc.cache[cacheKey];
};

console.log("ここからはじまる");
//キャッシュの記憶領域
fsFunc.cache = {};

//1をキーとしたオブジェクト生成
console.log(fsFunc(1)); //{0: 3, 1: 132.4468085106383, 2: 261.8936170212766...

//1の返って来たオブジェクトにインデックスを与えて値を取得
console.log(fsFunc(1)[1]); // 132.4468085106383
console.log(fsFunc(1)[2]); // 261.8936170212766

//5をキーとしたオブジェクト生成してそのままインデックスを与えて値を取得
console.log(fsFunc(5)[1]); // 650.2340425531914

//1を再度呼び出してみる
console.log(fsFunc(1)[1]); // 132.4468085106383

結果は以下のとおり

スクリーンショット 2016-06-04 10.30.49.png

解説

1度だけ生成、次からは再利用

関数オブジェクト内では、関数が呼ばれた際にその引数をキャッシュのキーにしています。

その後、関数オブジェクトのcacheに先ほどの文字列をキーに持つオブジェクトが無いかを探し、無ければresultオブジェクトを生成し、計算結果をresultオブジェクトに入れていき、その結果を先ほどの文字列をキーとしてcacheにオブジェクトを追加します。

この際にコンソールに追加した旨を伝えるメッセージを書いています。これは、出力結果でどういう動きになっているかを見るためのものです。

続けて、cacheオブジェクトから先ほどの文字列をキーに持つオブジェクトをこの関数の戻り値として指定しています。ここにも先ほどの文字列に対するオブジェクトを返す旨をコンソールに出力しています。

この関数を呼び出した時、cache内に引数の文字列をキーに持つオブジェクトが存在した場合は、そちらを返すだけという処理となっています。

この部分がこのメモ化の一番の利点で、生成は1度だけ、使用は変数に格納されたものを使うだけ、という仕組みの醍醐味だと言えるでしょう。

流れを読む

関数外のコードの部分を見てみましょう。

結果の画像が示すように呼び出しの流れとしては、

  • 関数を初めて呼び出したらオブジェクトの追加とオブジェクトを返す事が行われる
  • 2度めの呼び出しでは追加は行われず、オブジェクトが返ってくるだけとなっている
  • 新しい別の引数名で関数を呼び出せば、新たにオブジェクトの追加とオブジェクトを返すという事が行われている
  • またはじめの引数で呼び出したらはじめのオブジェクトが返されている

といった点が挙げられます。

引数に値を与えてその値に対応した計算が行われて結果をそれぞれ格納しておくという原理にすれば素敵でしょう。

設定オブジェクト

関数の引数がはじめは少なめで作っていたものの、あれやこれや実装していくうちに「この値も使いたい、このオブジェクトも欲しい」といったような事があると思います。その際に、設定オブジェクトを作成して、それを引数に渡すことで、わざわざ引数の構成を変えなくてもいいよね、って話ですね。

設定オブジェクトを使用しない関数の例
function addPerson(name,age,gender,address){
    // ↑引数の数が多い
}

設定オブジェクト使用した一例
//Personオブジェクトを定義
var Person = function (conf){

  //newし忘れ防止
  if(!(this instanceof Person)){
    return new Person(conf);
  };

  //プロパティの設定
    this.name = conf.name || "名無し";
    this.age = conf.age || 0;
    this.gender = conf.gender || "未回答";
    this.address = conf.address || "未回答";
}


//新しく生成するPersonオブジェクトの初期値を設定
var conf = {
    name:"Taro",
    age:20,
    gender:"man"
}

//新しいpersonオブジェクト生成
var taro = Person(conf)
;
console.dir(taro);
//Person {name: "Taro", age: 20, gender: "man", address: "未回答"}

ポイントは、conf設定オブジェクト内で、addressを設定していない点です。Personオブジェクト内のプロパティの設定で、addressがある場合とない場合で処理を書いておけば、引数の数は気にせずに増やしたり消したり出来ます。

関数の適用

今まで関数は「呼び出すもの」と考えてきましたが、関数プログラミング言語(これが何なんだろう)の場合は「適用するもの」として扱われます。

関数の適用
//関数の定義
var sayHi = function(who,what) {
    return "Hello" + (who ? "," + who : "") + (what ? "'s '" + what : "") + "!";

};

//関数を呼び出し
console.log(sayHi()); // Hello
console.log(sayHi("Taro")); // Hello,Taro!
console.log(sayHi("Taro","pen"));// Hello,Taro's pen!

//関数を適用 applyの場合
console.log(sayHi.apply(null, ["Taro"])); //Hello,Taro!
console.log(sayHi.apply(null, ["Taro","pen"])); //Hello,Taro's pen!

//関数を適用 callの場合
console.log(sayHi.call(null, "Taro")); // Hello,Taro!
console.log(sayHi.call(null, "Taro","world")); // Hello,Taro!

apply()

apply()は関数を適用させる為に使う関数で、引数が2つあります。

1つめは関数内のthisが指すものとなります。nullが指定されると関数内のthisはグローバルオブジェクトを指す事となります。

2つ目は関数内で使用できる引数の配列となります。引数の配列は、引数が指定された順番に適用されます。

call()

apply()関数のシンタックスシュガー(別の書き方ができるもの)であり、2つ目の引数が配列でなく、値やオブジェクトそのものを指定するものとなります。実質、できることは applyと同じです。

適用とは

他人の持っているプロパティやメソッドを自分のものとし利用できるメソッドという事になります。この説明はまた後の章に出てくるので、ここではこれ以上の深掘りはしないこととします(終わらないので(´ε`;))

適用を使った例
//名前プロパティだけを持つオブジェクト
var charaName = function(name){
  this.name = name;
}

//年齢プロパティと年齢を返すメソッドを持つオブジェクト
var charaAge = function(age){
  this.age = age;

  this.getAge = function (){
    return (this.name ? this.name + " / " : "") + this.age;
  };
};

//charaNameを基としたオブジェクト生成
var onchan = new charaName("onchan");

//charaAgeを基としたオブジェクト生成
var nochan = new charaAge(10);

console.dir(onchan); //charaName {name: "onchan"}

//charaAgeをcharaNameオブジェクトであるonchanに適用
charaAge.call(onchan,10);

//適用後はcharaAgeの持つプロパティとメソッドが追加される
console.dir(onchan); //charaName {name: "onchan", age: 10, getAge: function}

//charaAgeオブジェクトが基のnochanは名前が無いまま
console.dir(nochan); //charaAge {age: 10, getAge: function}

//onchanには年齢が出力できる
console.log(onchan.getAge()); //onchan / 10

という事で、関数は適用するもの、という話でした。よくよくカリー化と話が一緒になってしまいがちなので、分けて考えることにしましょう。

カリー化

カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。

出典:Wikipedia

言葉の意味やいろんな情報を参考に、以下のようなソースコードを書いてみました。

カリー化用HTML
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <title>js</title>
    <style>
        div#container:after {
            clear: both;
        }

        div#container div.box {
            float: left;
        }
    </style>
</head>

<body>
    <div id="container">
        <div class="box">今日の天気は晴れでしょう。</div>
        <div class="box">明日の天気は曇りでしょう。</div>
        <div class="box">今日の運勢は吉でしょう。</div>
        <div class="box">明日の運勢は明日発表されます。</div>
        <div class="box">クラッカーだけじゃ流石にお腹が空く</div>
        <div class="box">明日はパンを食べよう。</div>
    </div>
</body>
<script type="text/javascript" src="js-currytest.js">
</script>

</html>

カリー化した関数に部分適用をしています。

js-currytest.js
// ====カリー化された関数 ====//

/**
 * 要素のHTML内に指定文字列が含まれる時に要素かそのhtml自体を返す関数(カリー化用)
 * @param {boolean} elem - 返ってくる配列の中身を指定。 true - 要素自体, false - HTML自体
 * @param {NodeList} targets - 検索対象のオブジェクト配列
 * @param {string} str - 検索文字列
 */
function searchTextByElements(elem, targets, str) {

    //searchTextByElementsの戻り値に設定される関数
    return function(targets) {

        //searchTextByElementsの戻り値に設定された関数を呼んだ時の戻り値に返される関数
        return function(str) {

            //返す配列を初期化
            var results = [];

            //ターゲットの数だけ繰り返す
            for (var i = 0, max = targets.length; i < max; i += 1) {

                //要素内の文字列に検索文字があるかどうか
                if (targets[i].innerHTML.indexOf(str) > -1) {

                    //elemの値を見て条件分岐
                    if (elem) {
                        results[results.length] = targets[i]; //要素を返す
                    } else {
                        results[results.length] = targets[i].innerHTML; //HTML内の文字を返す
                    }
                }
            }
            //作成した配列を返す
            return results;
        }
    }
}

//ターゲットの要素を取得
var tgt = document.querySelectorAll("div#container div.box");

//全ての引数で適用
console.dir(searchTextByElements(true)(tgt)("明日"));


// ==== ここから部分適用 ==== //


/* elemのみを適用した新たな関数getElementsBySearchを生成。
   引数がtrueなら要素そのものを返すように、falseなら要素のHTMLの文が返されるように設定される */
var getElementsBySearch = searchTextByElements(true);


/* 対象要素を指定した新たな関数getElementsByBoxesを生成
   この引数を自由に変える事で検索対象を変えることが出来る */
var getElementsByBoxes = getElementsBySearch(tgt);

//検索キーワードをgetElementsByBoxesに与えて検索結果をコンソールに表示
console.dir(getElementsByBoxes("明日"));
console.dir(getElementsByBoxes("今日"));

//ちなみにelemをfalseにした時の結果
console.dir(searchTextByElements(false)(tgt)("明日"));

スクリーンショット 2016-06-05 12.16.41.png

解説

解説にあたり、先立って言っておかないといけない事があります。

このカリー化ですが、「関数の一つ目の引数を与えて、残りの引数を持った関数を返す」という風に作りました。Wikipediaには「もとの関数の残りの引数を取り結果を返す関数」とあり、結果を返す関数としては最後の引数まで呼ばないとそうはならないので、カリー化としてはどうなのかの判断ができていません。

なのですが、本来のカリー化の意味と合っているのか確証がもてませんが、きっとこういうハズ!と思って解説を進めていきます。

2016/6/6 追記

コメント欄にて情報を頂きまして、やっと理解できました!

複数の引数を取る関数を以下のように変換するまでがカリー化です。
すべての引数を適用せずに途中で止めるのが部分適用です。

コメント欄のrryu様が書いて下さったソースを参照していただければ一目瞭然です!

私が「わからない」と思っていた事は、「部分適用まで含めたものがカリー化」と思ってしまい「部分適用とカリー化の差はapplyとかcallとかが絡むのが違いなのかな?え?どういうこと?」と勝手に意味の分からない所にハマってしまいました。

コメントにて「明確に別」という旨を仰って頂けたことで脳内がとてもスッキリしました!ありがとうございました!

関数の連続実行

大元となる関数は3つの引数をもっています。細かい内容はソースコードを参照して頂くとします。

一気に呼び出すには以下のように行います。

全ての引数に適用
var result = searchTextByElements(true)(tgt)("明日");

()が着くと関数が実行されます。

まずはsearchTextByElements(true)が実行され、その戻り値に()が付いて、引数tgtがセットされた「今まさに返って来た関数」が実行されます。その戻り値にまた( )が付くことで同様に「今まさに返って来た関数」の実行が起きます。その際に引数に"明日"がセットされたものが実行される、という風に、関数が次々に実行されていく事となります。

「今まさに返って来た関数」はクロージャですので、一番初めで呼んだ関数function searchTextByElements(elem, target, str)のパラメーターであるelemtargetstrは参照され続けていますので、子スコープである次の「今まさに返って来た関数」で同じ値を参照できるという事です。

新たな関数の作成(部分適用)

でのそのような便利なしくみを用いて新たな関数を作ってみます。

新たな関数の作成と実行
var getElementsBySearch = searchTextByElements(true);
var getElementsByBoxes = getElementsBySearch(tgt);
var result = getElementsByBoxes("明日");
var resultToday = getElementsByBoxes("今日"));

1行目で、新たにgetElementsBySearchという関数名でelemの値をtrueにした結果の関数を変数に代入しております。

これは、elemで指定する値を 決め打ちしたものを関数化したもので、この関数を使うことで、本来の関数searchTextByElementsの引数を1つ無くす事に成功しています。

今作成した関数は、関数名で示した通り、「要素を検索によって取り出す」といった目的に使うための関数を新たに作った事になります。

ただこの関数は、そのまま使用できず、この関数自体もまた次の関数を返します。

-

続けて、2行目を見てみましょう。

新たな関数getElementsByBoxesは、検索対象のターゲットの要素も決めうちしている関数となります。

今回の検索対象はHTMLがタグ名がcontainerdiv要素の中にあるクラス名がboxdiv要素なので、新たな関数名をgetElementsByBoxesとしました。

あくまで検索用に使う関数なので、ボックスという名前の要素をゲットする関数として混同してしまう怖れがあります。なので明確な名前の方がいいかもしれません。(getElementsByBoxesForSeachTextなど)

そして3行目を見てみましょう。

ようやく目的の値を取得するための所まで来ました。getElementsByBoxesに引数"明日"を渡して呼び出すと、配列が返ってきます。

4行目は、引数を"今日"とする事で、今日という文字が入ったものを返してくれる、という使い方ができるわけです。

こういう風にすると、途中でパラメーターの変更をして扱いたくなった時に、組み合わせを自由に変えられるので、便利だなという事ですね。

今回、途中で関数としては中途半端なものができてしまったので、実際は以下のように書くとよりスマートかなと追われます。

実際はこっちが良いかも
var getBoxesForSearchText = searchTextByElements(true)(tgt);
var result = getElementsByBoxesForSearchText("今日");

余計な関数オブジェクトが作成されず、サクッと使いたい時に使えますね。

bind()

カリー化、実に強力ですが、実際カリー化用に関数を書くのは大変、って時は組み込みのbindを使うと良いようです。

bindを用いた新たな関数作成
/**
 * 要素のHTML内に指定文字列が含まれる時に要素かそのhtml自体を返す関数
 * @param {boolean} elem - 返ってくる配列の中身を指定。 true - 要素自体, false - HTML自体
 * @param {NodeList} targets - 検索対象のオブジェクト配列
 * @param {string} str - 検索文字列
 */
function searchTextByElements(elem, targets, str) {

    //返す配列を初期化
    var results = [];

    //ターゲットの数だけ繰り返す
    for (var i = 0, max = targets.length; i < max; i += 1) {

        //要素内の文字列に検索文字があるかどうか
        if (targets[i].innerHTML.indexOf(str) > -1) {

            //elemの値を見て条件分岐
            if (elem) {
                results[results.length] = targets[i]; //要素を返す
            } else {
                results[results.length] = targets[i].innerHTML; //HTML内の文字を返す
            }
        }
    }
    //作成した配列を返す
    return results;

}

//ターゲットの要素を取得
var tgt = document.querySelectorAll("div#container div.box");

//コンソールで一旦確認
console.dir(searchTextByElements(true, tgt, "明日"));

//新しい関数をbindを使って作成
var getElementsByBoxesForSearchText = searchTextByElements.bind(null,true,tgt);

//コンソールに出力
console.dir(getElementsByBoxesForSearchText("明日"));
console.dir(getElementsByBoxesForSearchText("今日"));

スクリーンショット 2016-06-05 13.29.17.png

bindは新たに関数を作成する関数で、第一引数は関数内で使用するthisの束縛、第二引数以降は基となる関数の引数の並びと対応しています。

感想

どんどんややこしい!どんどん鈍足!

そしてカリー化ですが、部分適用と明確な違いがあまり理解出来ていません。ひょっとしたら自分が書いたソースは部分適用なのかもしれませんし、まだまだ勉強不足だと思います。

カリー化と部分適用を書いていました。コメントのおかげでスッキリしました!ありがとうございました!

とりあえず関数編はここまでとして、次に進んでいこうと思います。

リンク

前回はこちら

オブジェクト指向初心者の私がJavaScriptにも再入門 「関数・初期化関連編」 〜JavaScriptパターン 学習7日目【逃げメモ】〜

カリー化にあたって参考にさせて頂いたサイト

javascript で遊ぶラムダ式、クロージャ、カリー化

17
19
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
19