メインコンテンツまでスキップ

JavaScript

はじめに

Webの発展とともにJavaScriptは広く普及しました。
ブラウザで実行できる動的型付け言語といった手軽さの反面、少々クセのある言語でもあります。

今回はJavaScriptプログラミングを行う上で、特に重要だと思う3つのキーワード

  • this
  • クロージャ
  • イベントループ

について紹介していきます。

this

thisとは 実行コンテキストのオブジェクトへの参照 です。
同じようなキーワードはJavaScript以外の言語でも実装されているため、馴染み深いと思います。
(PHPでは $this、Python ではselfなど)

しかしJavaScriptのthisは、他の言語と少し異なり、thisが呼ばれた時のコンテキストによって参照先のオブジェクトが変わる 特徴があります。

thisが呼び出されたコンテキストによって、どのように参照が変わるのか見ていきます。

グーローバルコンテキスト​

グローバルコンテキストで呼び出した this は、グローバルオブジェクトを参照します。

// Webブラウザ
console.log(this === window); // ✅ true

// Node.js
console.log(this === global); // ✅ true

※ 以後Webブラウザで実行している前提とします。

クラス​

クラス内の this はそのクラスのインスタンスを参照します。
これは他の言語のthisと同じですね。

class Person {
constructor(name) {
this.name = name;
}

sayHello() {
console.log('Hello, ' + this.name); // Hello, Alice
}
}

const person = new Person('Alice');
person.sayHello();

関数内​

関数内の this は呼び出し元のオブジェクトを参照しますが、
アロー関数内では、関数が宣言された時点でthisの参照先が確定する という特徴があります。
以下の例では arrow関数 が宣言された時点で参照先がグローバルオブジェクトに確定します。

function func() {
console.log(this === window); // ✅ true
}

const arrow = () => {
console.log(this === window); // ✅ true
};

const obj = {
func,
arrow,
};

obj.func(); // ❌ false (通常関数のthisはobjに変わる)
obj.arrow(); // ✅ true (アロー関数のthisはwindowのまま)

特にアロー関数内でのthis呼び出しで、意図せずグローバルオブジェクトを書き換えてしまう(または意図したオブジェクトを参照出来ていない)のはよくあるケースかと思いますので、
thisの挙動を正しく理解して使用することはとても重要です。

クロージャ

クロージャは関数とその関数が作られた環境という 2 つのものの組み合わせです。

出典: https://developer.mozilla.org/ja/docs/Web/JavaScript/Closures

言葉では分かりづらいので、例を見てみます。
次の createCounter() は関数内で宣言されたcount変数をインクリメントする関数 coutUp() を返す関数です。

function createCounter() {
let count = 0;
return function countUp() {
count++;
console.log(count);
};
}

const counterA = createCounter();
const counterB = createCounter();

counterA(); // 1
counterA(); // 2
counterA(); // 3

counterB(); // 1
counterB(); // 2
counterB(); // 3

この例では coutUp() がクロージャとなります。
クロージャは、自身が作成された環境(ここではcreateCounter関数)のcount変数を保持し続けるため、
関数を作成するごとに、別々のcount変数を保持することができます。

一見、何が便利なの?と思うかも知れません。
カウンタ変数を受け取り、1を足して返す関数でも同じことが実現できますよね。

let countA = 0;
let countB = 0;

function counter(count) {
return (count += 1);
}

countA = counter(countA); // 1
countA = counter(countA); // 2
countA = counter(countA); // 3

countB = counter(countB); // 1
countB = counter(countB); // 2
countB = counter(countB); // 3

クロージャを使った場合と見比べてみてください。
カウンタ変数が、グローバルスコープに定義されてしまっています。
クロージャのテクニックを使うと、カウンタ変数をプラーベート変数として閉じ込めることがきます。
これはグローバルスコープを汚染せず、外部からカウンタ変数へアクセスできなくなるため、カウンタ変数が誤って変更されることを防げるという大きな利点があります。

イベントループ

これまで紹介したthis、クロージャはいずれも文法の話でした。
次に紹介するイベントループは、実行環境(ランタイム)の話になります。 
そして、イベントループは非同期処理を理解する上で非常に重要な概念となります。
非同期処理は、Ajaxを利用してサーバーと通信を行い画面を更新するSPAが普及した現在のWebフロントエンド開発において欠かせない存在ですので是非理解しておきたいところです。

説明を始める前に、次のコードの実行結果を考えてみてください。

function main() {
console.log(1);

setTimeout(() => {
console.log(2);
}, 0);

console.log(3);
}

main();

setTimeout の delay は 0 です。
直ちに実行されるため次の結果を予想されるかもしれません。

1
2
3

しかしこれは誤りで、正解は

1
3
2

となります。
最後まで読んでいただければ、setTimeoutのコールバック関数の実行が遅れる原因が理解できると思います。

JavaScript のランタイムは

  • Call Stack
  • WebAPI
  • Callback Queue

と呼ばれる仕組みによって実行されていて、それぞれ次の関係性があります。

詳しくみていきます。

Call Stack​

関数の呼び出し履歴を管理するスタックです。
関数が呼び出されると、その関数がスタックの一番上に追加されます。
関数の実行が完了するとスタックから削除されます。

呼び出し毎、1 度に一つの処理しか実行できません。

Web API​

ブラウザが提供する API と、その実行環境です。
(fetch, DOM, History API などです。)

JavaScript のランタイムとは別のランタイムで実行されています。

Callback Queue​

Web API の処理が完了すると、Callback Queue にコールバック関数が追加されます。
Call Stack が空になったとき、Callback Queue の先頭から Call Stack に追加され実行されます。

JavaScript はこれらの仕組みを利用して、非同期処理を実現しています。

そしてこれらの繰り返しがまさにイベントループです。

改めて冒頭の問題の処理を追ってみます。

const main = () => {
console.log(1);

setTimeout(() => {
console.log(2);
}, 0);

console.log(3);
};

main();
  1. Call Stack に main() が追加されます。
  1. 次に、main関数内の1番目の処理、console.log(1) が Call Stack に追加されます
  1. console.log(1) が実行され Stack から削除されます
  1. 次の処理に移ります。setTimeout() が Stack に追加されます
  1. setTimeout() はWeb APIのため Web API に渡されます
  1. Web API は 0 秒後、コールバック関数を Callback Queue に追加します

このとき、CallStack は main() の処理が終わっていないため空ではありません。
そのため Callback Queue から Call Stack へ追加はできず、待機します。

  1. 一方、Call Stack では次の処理 console.log(3) が追加&実行されています
  1. console.log(3) が Stack から削除され、ようやく main() の処理が終わり Call Stack が空になります
  1. Call Stack が空になったので Callback Queue から待機していたコールバック関数が Stack に追加されます
  1. console.log(2) が最後に実行され、再度 Call Stack は空になります。

これが実際に関数を実行した際にJavaScriptが行なう処理の流れになります。

setTimeout()のような非同期処理は、CallStack に追加された時点で、Web API に渡されます。
そのため、たとえ delay が 0 のタイマーを作成しても、コールバック関数の実行は後に回される訳です。
setTimeout() の delay は、
指定秒数後にコールバック関数の実行を保証するものでは無く、
指定秒数後にコールバック関数を Callback Queue に追加する
と言い換える事ができると思います。

今回の例は簡略化しており、実際にコールバックキューはタスクキューとマイクロタスクキューの2つがあったりします。
マイクロタスクキューは優先度の高いキューです。Promiseを使う時などに登場するキューです。(今回は詳しい説明は省略します🙇‍♂️)

実際の開発では、API通信等でリソースの取得が終わってから次の処理に進ませる等、非同期が絡む処理の実行順を考慮する必要がある場面が往々にして発生します。
最低限の仕組みを知っているだけでも、ロジックの矛盾にいち早く気づくことができるようになると思います。

文章では少々わかりづらかったと思いますが、
JSConf EU でのイベントループに関する発表の動画がアニメーション付きで、日本語字幕も用意されているのでおすすめです。
イベントループとは一体何ですか? | Philip Roberts | JSConf EU


以上が、JavaScriptの基礎知識についての紹介でした!
拙い文章ではありましたが、少しでも参考になれば幸いです。
間違った情報があればご指摘、修正いただけると助かります...!