とっても短いJavaScript入門
- Javascriptのローカルスコープは関数スコープだ。
http://www.nttpc.co.jp/yougo/ローカルスコープ.html
ローカルスコープ ... コンピュータ・プログラムの中では、変数や関数といった機能が使われている。この変数や関数を参照できる範囲が、プログラムの一部に絞られた状態をローカルスコープ(local scope)という。
Javascriptは結構異質で、関数ブロックの範囲がローカルスコープになる。ブロックスコープ (if文やfor文)の範囲とローカルスコープとは(ES2015(JavaScriptの中核仕様を抜き出して標準化した規格)以前は)全く何の関係もない1。
注) ES2015では、const, let宣言が登場した。これは、ブロックスコープで有効な変数宣言。varはいらない子。なので、constかletを使おう。
- コードがだいたい読める!
ほぼ終わり2。
補足
var周りの挙動
ただ、コードを読む際にvarは出てくるので、補足3。
//i, jはvar宣言
for(var i = 1; i < 11; i++)
{
var j = i+1;
if(j%3 === 0)
{
console.log(i*j);
}
}
//ブロックスコープの外でも参照できる。
console.log(i,j);
このコードを見ると、JavaScriptをあんまり知らない他言語修得者(CとかJavaとかC#とか)はこういう風に見るに違いない:
//iの宣言
for(var i = 1; i < 11; i++)
{
//jの宣言(ループごとに行っている)
var j = i+1;
// "==="..? 「厳密等価演算子」っていうんか..
if(j%3 === 0)
{
console.log(i*j);
}
}
//i,jのスコープの終わり
//11 11..? あれ(・・) もう、iとjは使えんやろ??なんでや??
console.log(i,j);
実はJavaScriptは、変数の巻き上げ(hoisting)を内部で行っている。
//宣言部分を暗黙のうちに関数の冒頭(JSのローカルスコープは関数スコープ)に巻き上げた。
var i;
var j;
for(i = 1; i < 11; i++)
{
//代入の部分はその場所に残る
j = i+1;
console.log(i*j);
}
//ブロックスコープの外でも参照できる。<= i, jのスコープが意図しているより広すぎる
console.log(i,j);
ここからわかるショッキングなこと4:
- for文で変数宣言したのが、意図した通りにならない
- jは1回しか宣言されていない(つまり、一回宣言した後はループを繰り返すたび、上書きされる)
関数プログラミングの入り口
for文の変数を何とかスコープ内に閉じ込めたい。でも閉じ込める方法が関数ブロックしかないorz
=> じゃあ、関数使うしかないっしょ。
まずは単純にローカルスコープの手段が関数ブロックしかないのだから、functionをただの箱だと思って使う方法。
//functionに閉じ込めたら問題解決!
//var func = () => //ES6からはこの記法でもOK
var func = function()
{
var i;
var j;
for(i = 1; i < 11; i++)
{
j = i+1;
if(j%3 === 0)
{
console.log(i*j);
}
}
}
//console.log(i*j);
func();
どうせ実行するのだから、すぐに実行してしまえ!5
//functionの前の丸括弧は必要
var func = (function()
{
var i;
var j;
for(i = 1; i < 11; i++)
{
j = i+1;
if(j%3 === 0)
{
console.log(i*j);
}
}
})(); //6 30 72
// '})'までが関数の終わり、()部分が引数(今回は引数を取らないので、なにもない)
function(関数リテラル表現)を用いれば、定義した変数は関数スコープ内部で閉じ込めることができます(上の例だと、var i, var j
とか)
という感じで、スコープを覆い隠すという点では、このまま同じようなやり方でやっても、これ以上前に進めないので、違う観点でのアプローチも見ていきます。ここから、関数型プログラミングに足を突っ込み始めます。
発想としては、
「一つの関数に複数の処理(ロジック)をひとまとめにせず、ロジック(生成、選別, 実行の部分)を分離することができれば、1つ1つの関数ブロックが狭くなって、(変数のスコープが狭くなって)うれしいのでないか」
と考えます。
//配列生成部分 "..."はSpread operator
var arr = [...Array(10).keys()].map(c => c+1);
console.log(arr) //[1,2,3,4,5,6,7,8,9,10]
//配列処理部分
//引数が匿名関数になっている
//elemのスコープがfunction内に閉じ込められている
console.log(
arr
.filter(c => (c+1)%3===0) //条件文に相当
.map(c => c*(c+1))
); //[6, 30, 72]
one sentenceで繋げてしまって、
//もしくは、メソッドチェーンで書けちゃいます。
[...Array(10).keys()]
.map(c => c+1) //ここまで、配列生成部分
.filter(c => (c+1)%3===0) //ここまで条件分岐
.forEach(c => console.log(c*(c+1)) ); //配列の個々の要素を出力
とも書けます。
ここで、匿名関数の表記についてちょっと補足。
例えば、
map(c => c+1);
は以下と同じです6。(ES2015のアロー演算子を用いています)
map(function(c) { return c+1; });
//or
map((c) => {return c+1;}); //アロー関数記法
//or
map(c => {return c+1;}); //引数が一つだけならカッコを省略できる
//またreturn文しかない場合はreturnも省略できるので、最終形が上記のようになる:
map(c => c+1);
mapの引数は匿名関数となります。意味合いとしては、要素ごとに+1の演算を行ってくださいね〜、という感じです。
再掲しておきます。
[...Array(10).keys()]
.map(c => c+1) //ここまで、配列生成
.filter(c => (c+1)%3===0) //ここまで条件分岐
.forEach(c => console.log(c*(c+1)) ); //配列の個々の要素を出力
最初パッと見ると複雑そうですが、メソッドごとに役割分担がきちんとなされているのが見えれば、for文内部の条件分岐などの構造がないという点で、ロジックがかなりわかりやすくなります7
このように、より小さい単位で独立性を保ったままモジュール化でき、それでいて、変数のスコープの範囲を最小限に抑える(filter, map, foreachの引数cは関数のスコープに閉じ込められているでしょ。)ことができているということは、関数型言語の大きな利点です。
遅延評価
ここで、疑問に思う人はいると思います。
「そもそも、要素一つづつに対して、最後まで処理を行うfor文と、先に配列を生成してしまう上記のコードは全然違うよね」
全くもってその通りです。
では、関数型プログラミングではどう対応するかというと、値が必要になったとき(評価したい時)に初めて計算して取ってくるという手段をとります。この、計算を後回しにすることを遅延評価と言います。
なのですが、正直JavaScriptでは、荷が重いです8。そこで、最近登場したScalaとかでは、値を使用する段になって初めて評価する(遅延評価)が文法の中に組み込まれています9...
こうしてみていけば、
JavaScriptの一見して不可解な?文法規則10
=> 関数主体で考えざるをえない
=> 関数型プログラミングのパラダイムでコーディング
=> ロジックの分離ができ、開発効率化につながる
=> 関数型プログラミングって実は便利じゃね?
=> Scala, Swift, Kotlin..11
みたいな感じで、なにが言いたいかっていうとJavaScriptってなんだかんだ面白い言語だな、と思いました:)12
-
JavaScriptのスコープはあとは、evalスコープがあるが、eval関数を使わない限りは出てこないので、放っておこう。 ↩
-
宣伝ですが、JavaScriptにおけるthisの挙動とかは書いたので、よかったら。JavaScriptの文法は一見すると単純に見えますが、奥深くやろうとすると意外と大変だと気付きました(-_-) ↩
-
ちょっと触ってみようかなと思う人には、http://runstant.com とかが便利です。 ↩
-
最初に理解した時はなんやこれ..って思いました。こんなの絶対間違えるよ>< ↩
-
即時関数(IIFE:Immediately Invoked Function Expressions)といいます。 ↩
-
C#のLINQを使ったことのある人は同じように書けます。ちなみに自分はC#のLINQではじめて、関数型プログラミングに触れました。 ↩
-
全然分かりやすく見えないかもしれませんが、ぶっちゃけ慣れです。一度見えてしまえば、
for(i=0; i<=10; i++)
にはもう戻れません... ↩ -
できなくはないです。引数のない関数で値をラッピングすることで評価を後回しにすることができます(この関数の構造をサンクと言います。) 例えば、関数型プログラミングの基礎 JavaScriptを使って学ぶ では、わりと本格的に遅延評価をJavaScriptで実装しています。 ↩
-
実はScalaは自分自身まだ手をつけていないので(Haskellはやってる最中!)、他の人にバトンタッチ。 ↩
-
いや、まぁ実際不可解な規則も少なからずあるんですが... ↩
-
haskellもあるよ! ↩
-
ちなみに私はC++er(初心者)です。 ↩