嫌ならあっち行け。
というわけで、関数に関してもう少し基礎をおさらいしていこうと思う。
おさらいだけで終わらないように祈っていてもらえると嬉しいかもね。
関数はいくつか書き方があって、前回では関数リテラル、オブジェクトリテラルな書き方をした。typeof()関数で型を調べた結果は、関数リテラルがfunction、オブジェクトリテラルがobjectだった。まぁ名前からしてあたりまえっちゃ当たり前なんだけどね。
実は関数リテラルにはもう一つの書き方が有るんだ。今回はそれをおさらいしておきたい。
関数を変数に代入できる
そう、JavaScriptは関数を変数に代入することができる。今までどおり関数リテラルで普通に書くならこうなる。
function myFunc() { console.log('this is myFunc.'); }
変数に代入する方法で、myFuncを変数として関数を定義することもできる。
var myFunc = function() { console.log('this is myFunc.'); }
よく見ると関数名が無い。あるのは変数だけだ。こういう関数を無名関数と呼ぶ。
オモロイよな、JavaScriptって。
で、これらはなにが違うのか。
まずは型を見てみよう。
function Foo() { console.log('this is Foo.'); } var Bar = function() { console.log('this is Bar.'); } console.log(typeof(Foo)); console.log(typeof(Bar));
Foo()は関数リテラル、Barは関数リテラルを代入した変数。つまりどっちも関数ってことになる。
というわけで両方共、型はfunctionな関数だ。
さて、前者の関数宣言と、後者の代入式の違いはなんだろうか。
ためしに、prototypeという方法で、両者に新たに関数を追加してみたいと思う。
追加する関数名はshowという名前にする。
その際、関数の定義より先に追加する、というやり方をしてみよう。
Foo.prototype.show = function() { // ← 追記 console.log('show'); } function Foo() { console.log('this is Foo.'); } Bar.prototype.show = function() { // ← 追記 console.log('show'); } var Bar = function() { console.log('this is Bar.'); }
さぁ、どうだろうか。
エラーになる。
これ、実はエラーになったのは2つめの関数Barなんだ。Fooはエラーになってない。
答えは簡単で、Barのように変数に関数を代入した場合、その変数は、関数の参照になるからだ。
つまり代入された変数は、関数が格納されたメモリ上のアドレスの先頭部分だけが代入されることになる。
JavaScriptは関数宣言時、宣言された関数は実行時になると、親関数の先頭に巻き上げられるので、まだ存在していないはずの関数に対して処理を書いてもエラーにならない。Fooがエラーにならない理由だ。
しかし参照であるためには、その関数がすでに存在してなければならない。
そして関数を変数に代入するという方法は、実際には無名関数を変数に代入しただけなので、宣言と代入が同時になっている。
つまり宣言だけ先にすることができるFooは問題なく巻き上げられるが、宣言と代入が同時の場合、関数のアドレスが決まってないというか、そもそもそんな関数ないですよ状態になるので、その未定義な関数の参照に対して関数を追加しようとし、エラーになるというわけだ。
他の言語と比べると、巻き上げられるという仕組みが逆に興味深いと思う。
Foo()に対して、宣言する前に関数を追加しているけど、他の言語などならこういう書き方はしないと思う。
ココらへんはJavaScriptの利点でもあるし、わかりにくいという点では欠点でもある。
変数、関数の巻き上げ
さて、実は巻き上げは関数だけではなく、変数にも行われる。
変数は、宣言だけ、宣言後に代入、宣言時に代入、なんていくつかの方法がある。
var hoge; // 宣言のみ hoge = 'HOGE'; // 宣言後に代入 var fuga = 'FUGA'; // 宣言と代入
この内、宣言のみの場合は関数と同じく巻き上げられるので、関数内のどの位置で宣言しても、その前で参照することが可能だ。
しかし無名関数の代入と同じく、代入してしまうと巻き上げられないため、その位置から関数内の最後までが有効範囲となり、代入前では参照できなくなる。
実際には変数に代入したBarは、関数の宣言ではなく、変数の宣言と代入なんだよね。代入されるのが関数というだけで。
別の言い方をすれば、undefined状態の変数は実行時に巻き上げられるので宣言前に使用可能、とでも言おうか。
これは注意が必要だ。
プロトタイプ
上記関数の巻き上げでさり気なく使ってるprototypeだけど、案外知らない人多いのかもしれない。俺もJavaScriptはプロトタイプベースの言語だ!みたいな記事を読んだ時に、へ?って思った。クラスベースとどう違うの?と。
試しに先ほどの関数の宣言後、コンソールでprototeypを見てみよう。
console.log(Foo.prototype);
こんな風になるはずだ。
展開用の三角アイコンをクリックしてツリーを展開してみよう。
コンストラクタとしてFooが設定されていて、あとから追加したshow()関数も含まれている。いろいろ覗いていると面白いので、だいたい10分位はここを見まくるといいかも。
で、prototypeってなんぞや?と言われたら、俺はこう答える。
関数作ると勝手に作られる、子離れできてない親
だ。いちいちどこに行くのにも後ろをついてくる親がいたらうざいけど、JavaScriptではそれがありがたかったりする。
そう、Javaとかのクラスが使える言語やってる人なら、ただの親クラスといったほうが早いかもしれない。
親クラスなので、自分自信にメソッドとかプロパティをじゃんじゃん追加するより、親に入れておけば同じメソッドが1個ですむし、無駄がなくなってメモリの節約になる。
具体的に見てみよう。
function Animal(voice) { this.bark = function() { console.log(voice); } } var cats = new Animal('mew'); var dogs = new Animal('bow'); cats.bark(); // mew を返す dogs.bark(); // bow を返す
このAnimal関数は、与えられた声を、barkメソッドで発する事ができる。
catsオブジェクトでmewを指定し、dogsオブジェクトでbowを指定してオブジェクト化してある。
それぞれのbarkメソッドを実行してみると、コンソールにはちゃんとmew、bowが表示される。
しかしこの場合、catsオブジェクトとdogsオブジェクトそれぞれに、同じ役割を持つbarklメソッドが作られてしまうんだ。
試しにオブジェクトをコンソールで表示してみると分かる。
console.log(cats); console.log(dogs);
ほらこの通り。それぞれにbarkメソッドが作られている。これでは無駄だ。メモリの無駄遣い。
次はprototypeをつかって、親にbarkメソッドを指定してみよう。親なので、newで作られたオブジェクトは全部子供だ。そして親の中身を子供のオブジェクトに完全に継承してくれる。
function Animal(voice) { this.voice = voice; } Animal.prototype.bark = function() { // ← prototypeでメソッド定義 console.log(this.voice); } var cats = new Animal('mew'); var dogs = new Animal('bow'); cats.bark(); // mew を返す dogs.bark(); // bow を返す
これもオブジェクトをコンソールで表示してみるとその違いがよく分かる。
console.log(cats); console.log(dogs);
barkメソッドはここには表示されていない。
しかし__proto__を展開してみると、その中にbarkメソッドがいる。
__proto__というのはプロパティで、この場合、prototypeで宣言されたメソッドが、catsの__proto__プロパティと、dogsの__proto__プロパティに代入されるわけで、prototypeの参照受付みたいな感じで捉えておいて構わない。
実際のbarkメソッドの実態はprototypeに一つだけあり、その参照が__proto__に用意される、というイメージだ。これなら実態が1個なので、無駄がない。
汎用的な関数を作っておき、その関数のprototypeに更に汎用的なメソッドをたくさん入れておき、何か別の関数のprototypeへ継承する、なんてライブラリ的な使い方もできる。
もう少し続きそうだ
というわけで続きは次回。
facebook
twitter
google+
fb share