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
結果は以下のとおり
解説
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
言葉の意味やいろんな情報を参考に、以下のようなソースコードを書いてみました。
<!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>
カリー化した関数に部分適用をしています。
// ====カリー化された関数 ====//
/**
* 要素の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)("明日"));
解説
解説にあたり、先立って言っておかないといけない事があります。
このカリー化ですが、「関数の一つ目の引数を与えて、残りの引数を持った関数を返す」という風に作りました。Wikipediaには「もとの関数の残りの引数を取り結果を返す関数」とあり、結果を返す関数としては最後の引数まで呼ばないとそうはならないので、カリー化としてはどうなのかの判断ができていません。
なのですが、本来のカリー化の意味と合っているのか確証がもてませんが、きっとこういうハズ!と思って解説を進めていきます。
2016/6/6 追記
コメント欄にて情報を頂きまして、やっと理解できました!
複数の引数を取る関数を以下のように変換するまでがカリー化です。
すべての引数を適用せずに途中で止めるのが部分適用です。
コメント欄のrryu様が書いて下さったソースを参照していただければ一目瞭然です!
私が「わからない」と思っていた事は、「部分適用まで含めたものがカリー化」と思ってしまい「部分適用とカリー化の差はapplyとかcallとかが絡むのが違いなのかな?え?どういうこと?」と勝手に意味の分からない所にハマってしまいました。
コメントにて「明確に別」という旨を仰って頂けたことで脳内がとてもスッキリしました!ありがとうございました!
関数の連続実行
大元となる関数は3つの引数をもっています。細かい内容はソースコードを参照して頂くとします。
一気に呼び出すには以下のように行います。
var result = searchTextByElements(true)(tgt)("明日");
()
が着くと関数が実行されます。
まずはsearchTextByElements(true)
が実行され、その戻り値に()
が付いて、引数tgt
がセットされた「今まさに返って来た関数」が実行されます。その戻り値にまた( )
が付くことで同様に「今まさに返って来た関数」の実行が起きます。その際に引数に"明日"
がセットされたものが実行される、という風に、関数が次々に実行されていく事となります。
「今まさに返って来た関数」はクロージャですので、一番初めで呼んだ関数function searchTextByElements(elem, target, str)
のパラメーターであるelem
とtarget
とstr
は参照され続けていますので、子スコープである次の「今まさに返って来た関数」で同じ値を参照できるという事です。
新たな関数の作成(部分適用)
でのそのような便利なしくみを用いて新たな関数を作ってみます。
var getElementsBySearch = searchTextByElements(true);
var getElementsByBoxes = getElementsBySearch(tgt);
var result = getElementsByBoxes("明日");
var resultToday = getElementsByBoxes("今日"));
1行目で、新たにgetElementsBySearch
という関数名でelem
の値をtrue
にした結果の関数を変数に代入しております。
これは、elem
で指定する値を 決め打ちしたものを関数化したもので、この関数を使うことで、本来の関数searchTextByElements
の引数を1つ無くす事に成功しています。
今作成した関数は、関数名で示した通り、「要素を検索によって取り出す」といった目的に使うための関数を新たに作った事になります。
ただこの関数は、そのまま使用できず、この関数自体もまた次の関数を返します。
続けて、2行目を見てみましょう。
新たな関数getElementsByBoxes
は、検索対象のターゲットの要素も決めうちしている関数となります。
今回の検索対象はHTMLがタグ名がcontainer
のdiv
要素の中にあるクラス名がbox
のdiv
要素なので、新たな関数名をgetElementsByBoxes
としました。
あくまで検索用に使う関数なので、ボックスという名前の要素をゲットする関数として混同してしまう怖れがあります。なので明確な名前の方がいいかもしれません。(getElementsByBoxesForSeachTextなど)
そして3行目を見てみましょう。
ようやく目的の値を取得するための所まで来ました。getElementsByBoxes
に引数"明日"
を渡して呼び出すと、配列が返ってきます。
4行目は、引数を"今日"
とする事で、今日という文字が入ったものを返してくれる、という使い方ができるわけです。
こういう風にすると、途中でパラメーターの変更をして扱いたくなった時に、組み合わせを自由に変えられるので、便利だなという事ですね。
今回、途中で関数としては中途半端なものができてしまったので、実際は以下のように書くとよりスマートかなと追われます。
var getBoxesForSearchText = searchTextByElements(true)(tgt);
var result = getElementsByBoxesForSearchText("今日");
余計な関数オブジェクトが作成されず、サクッと使いたい時に使えますね。
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("今日"));
bind
は新たに関数を作成する関数で、第一引数は関数内で使用するthisの束縛、第二引数以降は基となる関数の引数の並びと対応しています。
感想
どんどんややこしい!どんどん鈍足!
そしてカリー化ですが、部分適用と明確な違いがあまり理解出来ていません。ひょっとしたら自分が書いたソースは部分適用なのかもしれませんし、まだまだ勉強不足だと思います。
カリー化と部分適用を書いていました。コメントのおかげでスッキリしました!ありがとうございました!
とりあえず関数編はここまでとして、次に進んでいこうと思います。
リンク
前回はこちら
オブジェクト指向初心者の私がJavaScriptにも再入門 「関数・初期化関連編」 〜JavaScriptパターン 学習7日目【逃げメモ】〜
カリー化にあたって参考にさせて頂いたサイト