今回は、「クロージャーの使い方」ということで、クロージャー というのはどうのような機能なのかということと、
クロージャーを利用したカプセル化やメモ化、カリー化といった技術について学習したいと思います。
さっそく、クロージャーって何?って話なのですが、
クロージャーというのは関数型プログラミングといわれる近年注目を集めているプログラミングの手法が用いられているので、
関数型プログラミングというものについて少しお話したいと思います。
まずは、関数型プログラミングではない通常の書き方から見てみたいと思います。
const array = [];
for(let n=1;n<101;n+=1){
if(n%6===0){
array.push(n);
}
}
console.log(array);
// [6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72, 78, 84, 90, 96]
関数型プログラミングではない通常の書き方を、手続き型プログラミングといったりするんですが、
手続き型プログラミングというのはこのようなコードになります。
このように、
・array配列を作る
・1から100までの中で6の倍数の物を配列に入れていく
・出力する
といったように、複数の命令文によって構成されていて、処理が上から順番に実行されていきます。
いっぽうで、関数型プログラミングで書いた場合はどのようになるかというと
const range = function(start, end) {
return [...new Array(end - start).keys()]
.map(function(n) {
return n + start;
});
}
console.log(range(1, 101).filter(function(n) {
return n % 6 === 0
}));
// [6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72, 78, 84, 90, 96]
このようなコードになるんですが、
関数型プログラミングっていうのは、「プログラムを関数の集合体」として考えられていて、式によって構成されているので、左から右に評価していく事で値が得られるようになっています。
この場合は、
・引数を入れる事で1~100の配列を作るrange 関数を事前に作っておき、
・range の結果から filter を使って、6の倍数だけ抜き出す。
といった流れで処理をしています。
この関数型プログラミングは、関数の中に、別の関数がネストしたような形になっているので、
関数型プログラミングにあまり慣れていないエンジニアからすると気持ち悪いように感じるかもしれませんが、
関数型プログラミングは、「関数を値のように扱う事ができる」という特徴があって、
関数を変数に代入したり、別の関数の引数に渡したり、戻り値として関数を返却したりして扱うことが出来ます。
// 関数は変数に値として代入できる
const add = function(a, b) {
return a + b;
}
console.log(add(1, 2)); // 3
// 関数を他の関数の引数として渡す
function apply(func, a, b) {
return func(a, b);
}
console.log(apply(add, 3, 4)); // 7
// 関数を戻り値として返す
function createAddFunction () {
return function(a, b) {
return a + b;
}
}
const add2 = createAddFunction();
console.log(add2(5, 6)); // 11
例えば、このようなコードになるんですが、
この一番上の関数は、定義した関数を add という変数 に代入しています。
この add という変数は 関数が代入されているので、addという変数を実行することでこのように関数を実行することが出来ます。
そして、その次にある apply という関数 ですが、この関数は1つ目の引数に関数を渡せるようになっています。
このように、apply関数を実行する際に add 関数 を引数として渡すことで、apply関数の中で引数で渡したadd関数が実行されるようになっています。
そして、最後の createAddFunction という関数ですが、この関数を実行すると 戻り値として別の関数が返却されるようになっています。
なので、この場合は、createAddFunction を実行して戻り値をadd2 という変数に代入していますが、
そうすると add2 には、createAddFunction の中で定義されている関数が代入されることになりますので、
add2 を実行することで このように、createAddFunctionの中の関数が処理されることになります。
このように、Javascript は、関数をまるで値のように、変数に代入したり、引数で渡したり、
また、実行結果として戻り値で返却することが出来ようになっています。
そして、クロージャーは、この「関数を戻り値として返却する」という仕組みが利用されています。
カプセル化
function outer() {
var count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
var closureExample = outer();
closureExample(); // 1
closureExample(); // 2
closureExample(); // 3
では、実際に、クロージャーを利用したコードの例をみてみたいと思います。
この例では、outer 関数の中で、変数に count が宣言されていて0で初期化されています。
そして、その中にある inner 関数の中で count を参照していて、inner関数を実行する度にカウントを1づつ増やしていくようになっています。
そして、outer関数は 戻り値として inner関数を返却するようになっています。
このouter関数を実行するとどのようになるかというと、
このように、outer 関数を実行すると変数のclosureExample に countが0で初期化された inner関数が代入されます。
そうすることで、closureExample 変数 を実行する度に count の値が1づつカウントアップされるようになっています。
このクロージャーを使う一番の利点としては、変数の countが 内部的に保持されて、この関数を利用する側からは、inner() 関数にだけアクセスさせるように制限させています。
もし、変数のcountの値が外部から書き換えできてしまうと「値を1づつ増やす」という機能に不具合が起こる可能性が出てきてしまいます。
このように関数を利用する側に、「必要最低限な機能だけを公開」しておいて、内部的なコードについて中に隠蔽して外から変更できないようにすることを「カプセル化」と呼ばれていて、外部の影響を受けないように機能を纏めることが出来るので、保守性や再利用性を高くすることが出来ます。
function createPerson(name) {
let age = 0;
return {
getName: function() {
return name;
},
getAge: function() {
return age;
},
setAge: function(newAge) {
age = newAge;
},
}
}
const person = createPerson('Alice');
person.setAge(30);
console.log(person.getName()); // "Alice"
console.log(person.getAge()); // 30
次に、こちらのコードは、オブジェクト指向プログラミングにおける、プライベート変数を実現する場合の例になります。
この person オブジェクトには、getName と getAge、そして、setAge の3つの関数が含まれていますが、
age については、取得も変更も出来るようになっていますが、name については、初期化した後は変更させないように隠蔽しています。
このように、クロージャーは、関数を利用する側に、アクセスを許可するものと制限するものを区別して、必要最低限のものだけを公開することが出来るという特徴があるので覚えておきましょう。
メモ化
function fibonacci(n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(10)); // 55
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
console.log("cached");
return cache[key];
}
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
const memoizedFibonacci = memoize(fibonacci);
console.log(memoizedFibonacci(10)); // 結果: 55
console.log(memoizedFibonacci(10)); // 結果: "cached" 55
次に、メモ化 についてお話します。
メモ化 というのは計算量の多い処理なんかをキャッシュして高速化する際に利用されるものになるんですけども、クロージャーを使用することでメモ化を実現することが出来ます。
例えば、この例はフィボナッチ数列と呼ばれる、計算量の非常に大きなプログラムになっていて引数で渡した数字が大きければ大きいいほど高負荷となるようなプログラムになっています。
このように計算量が大きい処理をする場合、再帰的なアルゴリズムで書くと同じ値が何度も計算されてしまって処理が遅くなってしまいます。
もし、この関数で計算した結果を引数で渡した値をキーにしてキャッシュしておいて、2回目からはキャッシュしておいた値を返却することで、同じ計算処理を何度も実行する必要がなくなるので効率的にすることが出来ます。
では、どのようにメモ化を実現するかというと、このように記述することが出来ます。
この例では、memoize (メモイズ)という関数を定義していて、渡された関数をメモ化する新しい関数を返却しています。
また、この関数は内部にcacheオブジェクトを持っていて、関数に渡された引数の値をキーにして、計算結果を保持しています。
そして、再度、同じ引数を渡して関数を実行した場合は、内部に保持していた値を返すようにしています。
このように、メモ化 というキャッシュを利用する方法としても、クロージャーが利用されていますので覚えておきましょう。
そして、次に、カリー化についてお話します。
カリー化
// カリー化を利用しない書き方
const add = function(x, y) {
return x + y;
}
console.log(add(1, 2)); // 3
console.log(add(1, 3)); // 4
// カリー化を利用した書き方
const add = function(x) {
return function(y) {
return x + y;
}
}
const add1 = add(1)
console.log(add1(2)); // 3
console.log(add1(3)); // 4
カリー化は、部分適用 なんて言われることもあるのですが、このようなコードになります。
上のコードは、カリー化を利用しない書き方になるんですが、
もし、このコードの第一引数には、必ず1を渡すといった場合に、このように、毎回第一引数に1を代入して実行しなければなりません。
それに対して、下の方で記述したカリー化を利用した書き方の場合はどのようになるかというと、
引数に1を渡した場合の処理を初めに実行しておいて、
2回目以降はクロージャーを利用して残りの処理を実行するようになっています。
このように、処理を細かく分割することで、それぞれの処理を独立させて効率的に処理させるようにする書き方のことをカリー化とよばれていて、ここでもクロージャーが活かされていますので、あわせて覚えておきましょう。
チャンネル登録 よろしくお願いします
こんにちわ!JS太郎です‼
このチャンネルでは、はじめてプログラミングをする人はもちろん、小学生からでも理解出来るように判りやすく解説しています。
是非、一緒にプログラミングを学んでご自身の付加価値を高めていきましょう。