LoginSignup
64
82

More than 3 years have passed since last update.

JavaScriptの基礎を押さえて、中級JS使いを目指す

Last updated at Posted at 2019-11-07

前回の記事の続き。JavaScriptのわかりづらい部分をまとめてJS使いを目指す記事です。
中級者ぐらいになって、こなれた感じでJavaScriptのことJSって略して呼びたいです。
この記事では、this巻き上げ即時関数を主に扱います。

※ VCSのdebugで検証しているので、グローバルオブジェクトはglobalになっています。
ブラウザで実行した場合はwindowになるので検証時は注意してください。

thisについて

thisはruby等でいうselfのようなものだが、JavaScriptでのthisは、呼び出し元の呼び出し方によって値が変動する。
主に4つのパターン(メソッドから、関数から、コンストラクタから、apply/callから)がある。
また、この本によると、上記四つに加えて、bindした時、アロー関数から呼び出した時との記載がある。
種類も多く利用するタイミングがまだわからないものもあるので、今回は関数から呼び出した時、メソッドから呼び出した時、コンストラクタから呼び出した時の三点に絞ってまとめる。1

thisの前にちょっと関数について補足

書き方が同じで紛らわしいが、functionで定義された関数はメソッドにもコンストラクタ2にも関数にもなりえる。
1. メソッド: オブジェクトのプロパティに関数を定義したもの。
2. コンストラクタ: newを使って呼び出したもの。
3. 関数: 通常の関数。


// 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はグローバルオブジェクトglobalwindowが入る。


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と一致している。
関数内でthisglobalのプロパティとして定義した値は、グローバルオブジェクトからグローバル変数と見なされていることが確認できる。

メソッドから呼び出した時の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.propobjに定義されている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にはインスタンス自身の値が入るため、instance1instance2どちらのインスタンスにも別の値が入っている。

巻き上げについて

関数の巻き上げと、変数の巻き上げの二つがある。英語では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の前にあり、かなりグロテスクな見た目で、ちょっとタイポを疑うレベルなので使用しないほうが良いと思うが、この書き方は、即時関数の一般的な書き方の(関数)(引数)の理解の助けになると思われる。

参照サイトなど


  1. いつか理解して利用価値がわかればまとめ直したい。 

  2. コンストラクタ関数と呼ぶのか、newがコンストラクタ呼び出しで呼ばれる関数なのか、コンストラクタって呼ばれる部分の言葉の定義がよくわからないので、だれか知っている方いれば教えて欲しいです! 

64
82
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
64
82