前回の記事の続き。JavaScriptのわかりづらい部分をまとめてJS使いを目指す記事です。
中級者ぐらいになって、こなれた感じでJavaScriptのことJSって略して呼びたいです。
この記事では、this・巻き上げ・即時関数を主に扱います。
※ VCSのdebugで検証しているので、グローバルオブジェクトはglobal
になっています。
ブラウザで実行した場合はwindow
になるので検証時は注意してください。
thisについて
this
はruby等でいうself
のようなものだが、JavaScriptでのthis
は、呼び出し元の呼び出し方によって値が変動する。
主に4つのパターン(メソッドから、関数から、コンストラクタから、apply/callから)がある。
また、この本によると、上記四つに加えて、bindした時、アロー関数から呼び出した時との記載がある。
種類も多く利用するタイミングがまだわからないものもあるので、今回は関数から呼び出した時、メソッドから呼び出した時、コンストラクタから呼び出した時の三点に絞ってまとめる。1
this
の前にちょっと関数について補足
書き方が同じで紛らわしいが、function
で定義された関数はメソッドにもコンストラクタ2にも関数にもなりえる。
- メソッド: オブジェクトのプロパティに関数を定義したもの。
-
コンストラクタ:
new
を使って呼び出したもの。 - 関数: 通常の関数。
// 1. メソッド プロパティに関数を入れる。fooが出力される。
var obj = { foo: function() { console.log('foo'); } }
obj.foo();
// 2. コンストラクタ newで呼び出されたFoo関数はコンストラクタ。頭文字を大文字にする慣習があるらしい。Foooが出力される。
function Foo() { console.log('Fooo'); }
var fooInstance = new Foo();
// 3. 通常の関数 foooが出力される。
function foo(){ console.log('fooo'); }
foo();
関数から呼び出した時のthis
通常の関数として呼び出した場合、this
はグローバルオブジェクトglobal
やwindow
が入る。
var testFunc = function() {
this.foo = 'fooo';
global.bar = 'barrrrr';
console.log(this === global); // true
}
testFunc();
console.log(foo); // fooo
console.log(bar); // barrrrr
this自体がglobal
と一致している。
関数内でthis
やglobal
のプロパティとして定義した値は、グローバルオブジェクトからグローバル変数と見なされていることが確認できる。
メソッドから呼び出した時のthis
メソッドから呼び出した場合、this
は自身が所属するオブジェクトを指している。
var obj = {
prop: 'foo',
testMethod: function() {
console.log(this); // Object {val: "foo", testMethod: }
this.prop = 'bar';
}
}
obj.testMethod();
console.log(obj.prop); // bar
testMethod
メソッド内のthis
は、自身が所属するtestMethod
メソッドのobj
オブジェクトが入る。よって、this.prop
はobj
に定義されているprop
プロパティにアクセスしていることになり、値はfoo
からbar
に上書きされている。
コンストラクタから呼び出した時の、this
コンストラクタから呼び出した場合、コンストラクタ呼び出しをしたインスタンスがthis
になる。
new
を使用して呼び出した関数がこれに相当する。new
を使用して関数を呼び出す場合、暗黙的にthis
オブジェクトを定義してreturn
してくれる。
var TestFunc = function(foo) {
this.prop = foo;
}
var instance1 = new TestFunc('bar1');
var instance2 = new TestFunc('bar2');
console.log(instance1.prop); // bar1
console.log(instance2.prop); // bar2
関数は一つだが、this
にはインスタンス自身の値が入るため、instance1
・instance2
どちらのインスタンスにも別の値が入っている。
巻き上げについて
関数の巻き上げと、変数の巻き上げの二つがある。英語ではhoistingというらしい。響きがかわいい。
変数の巻き上げ
関数内で宣言されたローカル変数は、すべて関数内の先頭で宣言されたものとみなされる。
var global = 'global';
function func() {
// var global; がここで暗黙的に宣言されている。これが変数巻き上げの正体
console.log(global); // undefined
var global = 'local';
console.log(global); // local
}
func();
func
内の一度目のconsole.log
でグローバル変数のglobal
が呼び出されそうに見えるが、実際はundefined
になる。これは関数内でvar global = 'local'
と変数宣言をしている行があるため、関数内の先頭でvar global;
が暗黙的に宣言されていることによる挙動。
関数の巻き上げ
関数宣言で定義された関数は、コード実行時にスコープの先頭まで巻き上げられる。
var foo = sum(1, 2);
function sum(val1, val2) {
return val1 + val2;
}
console.log(foo); // 3
sum
関数定義前に、sum(1, 2)
と関数を利用しているがエラーにならずに3
が出力される。JavaScriptを含めて一般的な言語は上から下に実行されるので、本来は何かしらのエラーになってしかるべきだが、関数は巻き上げによってスコープの先頭(ここではvar foo
の前)で宣言されるので正常に動作する。
関数の巻き上げの注意点
関数の巻き上げは、関数式と関数宣言で挙動が変わるので注意が必要。
関数式と関数宣言は以下のコードを参照。
var func = function() {return 0;} // 関数式
function func() {return 0;} // 関数宣言
関数の巻き上げのサンプルコードは関数宣言を用いたが、次は関数式で同じことを実行する。
var foo = sum(1, 2);
var sum = function(val1, val2) {
return val1 + val2;
}
console.log(foo); // TypeError: sum is not a function
関数宣言の場合は関数名が決まっているが、関数式の場合は関数名が決まっていなく、変数名を通して実行されるためエラーが発生する。
即時関数について
個人的にJavaScriptをちょっと嫌煙してまじめに勉強しなかった主な理由の一つが、この即時関数の書き方だった。パッと見の違和感が強い。だが、グローバルオブジェクトを汚さないためにも大事。
// 即時関数宣言方法1
(function(val1, val2) {
console.log(val1 + val2); // 3
})(1, 2);
// 即時関数宣言方法2
(function(val1, val2) {
console.log(val1 + val2); // 3
}(1, 2));
書き方は上記の二通りがあり、同じように動作する。
()
が多すぎて分かりづらいが、冷静に考えると通常の関数の呼び出しも()
を付けて実行しているだけである。
念のための確認だが、関数宣言後に()
を付けずに実行すると、当然宣言内容を参照するのみで関数は実行されない。
function testFunc() { console.log('foo'); }
// ()なしで実行
console.log(testFunc); // function testFunc() { … }
// ()ありで実行
testFunc(); // foo
関数宣言の場合、省略して書くと関数名(引数)
で実行している。ということは、即時関数は(関数宣言)(引数)
で実行していると考えられる。こうして見ると、ある程度即時関数の書き方も腑に落ちる。
()
演算子自体は、(1 + 2) / 2
等で使用するように、グループ化演算子と呼ばれ優先順位を制御している。この場合(1 + 2)
が優先して評価され、3 / 2
になる。
即時関数の(関数宣言)(引数)
は、(関数宣言)
を優先して扱い、関数(引数)
の形で実行されているということになる。これは通常の関数呼び出し方の、関数名(引数)
とほぼ同じである。
関数を式として評価し即実行するということなので、関数宣言を囲う()
は、()
だけでなく他の演算子でも同様の挙動が再現可能である。
// 関数宣言を囲う()を+や!に置き換えている
+function(val1, val2) {
console.log(val1 + val2); // 3
}(1, 2);
!function(val1, val2) {
console.log(val1 + val2); // 3
}(1, 2);
+
や!
がfunction
の前にあり、かなりグロテスクな見た目で、ちょっとタイポを疑うレベルなので使用しないほうが良いと思うが、この書き方は、即時関数の一般的な書き方の(関数)(引数)
の理解の助けになると思われる。
参照サイトなど
- http://bonsaiden.github.io/JavaScript-Garden/#function.scopes
- JavaScriptの「this」は「4種類」??
- JavaScriptの理解を深めた人がさらにもう一歩先に進むための本