【Udemy】JavaScriptメカニズムの学習メモ

JavaScriptの本質を理解する JavaScript

この記事は Udemy のコース「【JS】初級者から中級者になるためのJavaScriptメカニズム」で学習した内容を、自分の言葉で記録したものになっている。

【JS】初級者から中級者になるためのJavaScriptメカニズム

このコースは初心者用の書籍では語られることのないJavaScripの持つ面倒な性質の説明に正面から挑み、そして非常に分かりやすく解説してくれる稀有なコースとなっている。そのため、JavaScript を本質から理解したいと望んでいる人には、このコースの受講を自信を持っておすすめできる。

なお、この記事では、私が学習した内容を確実に理解するために、自分の言葉で置き換えて記述しているため、コースの内容と表現が異なっていたり、誤りを含んでいる部分があると思うので注意されたし。

  1. 環境設定(VSCode)
    1. Live Server
    2. Live Server で HTML を開く
  2. JavaScript と ECMAScript
    1. ECMAScript
    2. JavaScript
      1. ブラウザの JavaScript
      2. Node.js の JavaScript
  3. ブラウザの JavaScript の実行環境
    1. ブラウザの構成要素
    2. JavaScript エンジンの種類
    3. JavaScript エンジン
    4. JavaScript コード実行前
      1. コード実行前に用意されるもの
      2. グローバルオブジェクト
      3. Window オブジェクト
    5. 実行コンテキスト
      1. グローバルコンテキスト
      2. 関数コンテキスト
    6. コールスタック(Call Stack)
    7. ホイスティング(Hoisting)
      1. ホイスティングとは
      2. function 宣言の巻き上げ
      3. Var 宣言の巻き上げ
      4. let, const 宣言の巻き上げ
      5. 関数式では巻き上げしない
  4. スコープ
    1. スコープとは
    2. Global スコープ、Script スコープ
      1. Global スコープ
      2. Script スコープ
    3. 関数スコープ
    4. ブロックスコープ
      1. ブロックスコープとは
      2. ブロック内の let, const 命令
      3. ブロック内の var 命令
      4. ブロック内の function 命令
      5. ブロック内の関数式
      6. if 文によるブロックスコープ
    5. レキシカル(Lexical)スコープ
    6. スコープチェーン
      1. スコープチェーンとは
      2. スコープチェーンによる変数参照の仕組み
      3. Global スコープ が一番外側
      4. 間違いやすい例
    7. クロージャー(Closure)
      1. クロージャーとは
      2. 値を保持できる変数を持った関数
      3. 動的な関数の生成
    8. 即時関数(IIFE)
      1. 関数呼び出し演算子 ( )
      2. グループ化演算子 ( )
      3. 即時関数
      4. 即時関数はどこで使うのか
        1. 即時関数で変数・関数のスコープを制限
        2. 即時関数の外でも使いたいなら return する
  5. 変数
    1. let, const, var の機能の違い
      1. 変数の再宣言
      2. 変数への再代入
      3. ブロックスコープの有効性
      4. 関数スコープの有効性
      5. ホイスティングによる初期値の設定
    2. データ型
    3. 暗黙的な型変換
      1. JavaScript は動的型付け言語
      2. 暗黙的な型変換
      3. typeof 演算子
      4. 文字列型 + 数値型
      5. 文字列型 - 数値型
      6. 数値型 + null 型、数値型 - null 型
      7. 数値型 + 真偽型、数値型 - 真偽型
    4. 厳格な等価性と抽象的な等価性
    5. falsy と truthy
    6. AND 演算と OR 演算
      1. AND 演算子(&&)
      2. OR 演算子(||)
      3. OR 演算の応用(初期値の設定)
    7. プリミティブ型とオブジェクト
      1. データ型
      2. プリミティブ型(Primitive Type)
      3. オブジェクト
    8. オブジェクトの分割代入
    9. 関数の引数におけるオブジェクトの分割代入
  6. 関数
    1. JavaScript の関数の特徴
      1. 関数呼び出しの際の実引数を省略できる
      2. 関数名が重複してもエラーにならない
      3. 引数の個数は関数が異なる理由にならない
      4. 関数式の場合は、関数名の重複でエラーになる
      5. 引数には初期値を設定できる。(ES6 以降)
      6. arguments で実引数を受け取れる
      7. 戻り値を定義しない場合は undefined が返る
      8. 関数は実行可能なオブジェクトである
    2. コールバック関数
      1. コールバック関数とは
      2. コールバック関数として無名関数も渡せる
      3. コールバック関数の実例:setTimeout
    3. this キーワード
      1. オブジェクトのメソッドにおける this
      2. 関数における this
      3. コールバック関数における this
      4. bind メソッド
        1. bind メソッドで this を束縛する
        2. bind メソッドは引数も束縛できる
      5. call メソッド、apply メソッド
        1. bind は関数を生成、call / apply は関数を実行
        2. call と apply の違いは引数の指定方法
        3. apply メソッドの実践的な利用例
      6. アロー関数
        1. アロー関数とは
        2. 無名関数とアロー関数の違い
        3. アロー関数における this
    4. コンストラクタ関数とオブジェクト
      1. コンストラクタ関数とは
      2. prototype
        1. 関数オブジェクトの prototype プロパティ
        2. オブジェクトの [[Prototype]] 内部プロパティ
        3. [[Prototype]] は __proto__ プロパティからもアクセスできる
        4. インスタンス化の際に prototype が [[Prototype]] に設定されている
        5. prototype プロパティの暗黙的な参照
        6. メモリ消費の観点から、メソッドは prototype に定義する
        7. Object.create() によるオブジェクトの生成
      3. new 演算子
        1. new 演算子とは
        2. new 演算子の処理の詳細
        3. new 演算子を自作してみる
      4. instanceof 演算子
      5. Function コンストラクタ
        1. Function コンストラクタとは
        2. 関数の __proto__ は Function.prototype
        3. new Function(…) はグローバルスコープの関数を生成
      6. プロトタイプチェーン
      7. hasOwnProperty メソッド と in 演算子
      8. プロトタイプ継承
        1. [[Prototype]] 内部プロパティによる継承
        2. コンストラクタ関数の prototype による継承
      9. コンストラクタ関数の継承
        1. Prototype を継承する
        2. プロパティを継承する
      10. クラス(Class)
      11. クラス継承
      12. super
        1. 継承元の constructor を呼び出す
        2. 継承元のクラスのメソッドを呼び出す
        3. 継承元のオブジェクトのメソッドを呼び出す
      13. ビルトインオブジェクト
        1. ビルトインオブジェクトとは
        2. ビルトインコンストラクタ
        3. 配列もオブジェクトである
      14. ラッパーオブジェクト
      15. Symbol
        1. Symbol とは
        2. Symbol() 関数の引数
        3. シンボルはオブジェクトのプロパティとして利用できる
      16. オブジェクトのプロパティとディスクリプタ―
        1. ディスクリプタ―とは
        2. ディスクリプタ―の取得
        3. プロパティの追加・変更
      17. setter と getter
        1. getter とは
        2. setter とは
        3. Object.defineProperty メソッドによる定義
        4. コンストラクタ関数における getter, setter
        5. class における getter, setter
      18. 静的メソッド(static メソッド)
        1. 静的メソッド(static メソッド)とは
        2. コンストラクタ関数における静的メソッド
        3. class における静的メソッド
      19. チェーンメソッド
  7. 反復処理
    1. 演算子と優先順位
      1. 演算子とは
      2. 演算子の優先順位
      3. 演算子の結合性
      4. インクリメント演算子・デクリメント演算子
    2. ループ文とプロックスコープ
    3. for … in と列挙可能性
      1. 列挙可能性とは
      2. for … in 文
        1. 列挙可能プロパティのみ列挙する
        2. 列挙する順序は保証されていない
        3. プロトタイプチェーン内も列挙対象となる
        4. Symbol で定義したプロパティは列挙されない
    4. for … of と反復可能性
      1. イテレーター(Iterator)とは
        1. IteratorResult オブジェクト
        2. イテレーター(Iterator)の定義
        3. イテレーターを用いた反復処理
        4. イテレーターを関数から生成する
        5. イテレーターによる反復処理を関数化してみる
      2. 反復可能(Iterable)オブジェクト
        1. 反復可能オブジェクトとは
        2. String, Array, Map などのオブジェクトは反復可能
        3. Object のオブジェクトは反復可能ではない
      3. for … of 文
      4. ジェネレーター(Generator)
        1. ジェネレーター関数
        2. yield キーワード
        3. ジェネレーターを用いた反復処理
        4. ジェネレーターを使用した反復オブジェクト
      5. スプレッド演算子(Spread Operator)
        1. スプレッド演算子とは
        2. 反復可能オブジェクトの展開
        3. 列挙可能なプロパティの展開
      6. 関数の残余引数(Rest Parameter)
        1. 残余引数とは
        2. 可変長引数の関数を作成
    5. Map と Set
      1. Map オブジェクト
        1. Map とは
        2. 任意の型をキーとして利用できる
        3. for … in 文でキーを列挙することはできない
        4. for … of 文で反復処理を行うことができる
      2. Set オブジェクト
        1. Set とは
        2. 重複する値を格納することはできない
        3. インデックスやキーがないため、for … in で列挙できない
        4. for … of 文で反復処理を行うことができる
        5. インデックスやキーなどで要素にアクセスできない
  8. 非同期処理
    1. ブラウザとスレッド
      1. スレッドとは
      2. ブラウザに存在するスレッド
      3. メインスレッド(Main Thread)とは
      4. FPS(Frames Per Second)
    2. 同期処理と非同期処理
      1. 同期処理とは
      2. 非同期処理とは
      3. コールスタック(Call Stack)
      4. タスクキュー(Task Queue)
      5. イベントループ(Event Loop)
      6. Web APIs
      7. 非同期処理の実行の流れ
      8. 非同期処理のチェーン
      9. Promise
      10. Macrotasks と Microtasks
      11. async と await
      12. Fetch API による非同期ネットワーク通信
      13. 例外処理とエラー
  9. モジュール構文(import, export)
  10. Strict モード
  11. Proxy オブジェクト
  12. Reflect オブジェクト
  13. WeakMap オブジェクト
  14. JSON 形式と JSON オブジェクト
  15. localStorage, sessionStorage

環境設定(VSCode)

Live Server

Live Server というプラグインで機能拡張すると、HTMLをブラウザ上で簡単に確認できるようになる。

エディタ左端の Extensions(拡張機能)を選択し、検索窓から「Live Server」で検索する。表示された一覧から「Live Server」を選択し、install(インストール)を実行する。

Live Server で立ち上げるブラウザの設定は、VSCode の左下にある設定(Settings)から「Browser」で検索し、「Live Server > Settings: Custom Browser」で行うことができる。

設定後は、念のため VSCode を再起動する。

Live Server で HTML を開く

VSCode のプロジェクトツリーから .htmlファイルを右クリックし、「Open with Live Server」を選択する。

JavaScript と ECMAScript

ECMAScript

JavaScript は、Netscape Communications社のブラウザ「Netscape Navigator」で使われる言語として誕生した。

その後、Microsoft 社のブラウザ「Internet Explorer」でも同様の機能が JScript という名前でサーポートされるなど、様々なブラウザで JavaScript がサポートされるようになったが、ブラウザ間の JavaScript の違い(非互換性)が問題になった。

そのため、JavaScript の標準化が行われ、JavaScript のコアの部分を仕様として定めることになった。その仕様が ECMAScript と呼ばれている。

ECMAScript は、国際的な標準化団体である Ecma International により、現在も標準化が進められている。

JavaScript

ECMAScript は言語の仕様を定めたものであり、その仕様に則って実装されたプログラミング言語が現在の JavaScript ということ。

ただし、ECMAScript は JavaScript のコア部分のみを定める仕様であり、我々が JavaScript と呼んでいるものは、ECMAScript で定めているもの以外の機能も持っている。そして JavaScript の実行環境によって、持っている機能は異なっている。

ブラウザの JavaScript

ブラウザに実装されている JavaScript の場合、JavaScript のコアである ECMAScript を実装した部分の他に、Web APIs と呼ばれる JavaScript からブラウザを操作するための機能などを実装した部分を持っている。

ブラウザの JavaScript

Node.js の JavaScript

Node.js というソフトウェアをパソコン(PC)にインストールすることで、ブラウザとは関係なく、PC上で JavaScript を動作させることができる。

Node.js の場合、JavaScript のコアである ECMAScript を実装した部分の他に、CommonJS と呼ばれる ECMAScript とは異なる JavaScript の仕様を実装した部分を持っている。

CommonJS では、ブラウザ環境外での JavaScript の仕様を定めている。

Node.js の JavaScript

ブラウザの JavaScript の実行環境

ブラウザの構成要素

ブラウザは、主に次のような要素から構成されている。

  • ブラウザエンジン
    アドレスバー、戻る/進むボタン、ブックマーク メニューなど
  • ブラウザエンジン
    ユーザーインタフェースとレンダリング エンジンの間の処理を整理する
  • レンダリングエンジン
    HTMLとCSSを解析し、解析した内容を画面に表示する
  • ネットワーキング
    サーバーとの通信を担当する
  • UIバックエンド
    セレクトボックスやウィンドウなどの基本的なウィジェットの描画を担当
  • JavaScript エンジン
    JavaScript コードの解析と実行
  • データストレージ
    Cookie やストレージ

参考:ブラウザの仕組み: 最新ウェブブラウザの内部構造

これらの要素が連携することで、HTTP(HTTPS)リクエストによるリソースの要求からレンダリングまでの一連の処理が行われる。

そしてこの中の「JavaScript エンジン」こそが、JavaScript の解析・実行を司る要素であり、JavaScript エンジンが組み込まれているからこそ、ブラウザで JavaScript を実行させることができる。

JavaScript エンジンの種類

ただし、この JavaScript エンジンはブラウザごとに異なるものが使用されている。

ブラウザJavaScript エンジン
ChromeGoogle V8 JavaScript Engine
SafariJavaScriptCore
FireFoxSpiderMonkey
Edge昔は Chakra、現在は Google V8 JavaScript Engine
OperaGoogle V8 JavaScript Engine

参考:Webブラウザ、レンダリングエンジン、JavaScriptエンジンを整理して図視化してみた

ブラウザごとに JavaScript エンジンが異なるとは言っても、ほとんど Google の V8 JavaScript Engine が使用されていることが分かる。

この V8 はオープンソースで開発されている JavaScript エンジンであり、様々なソフトウェアに組み込むことで JavaScript を実行できる環境を作り上げることができる。Node.js でもこの V8 が採用されている。

JavaScript エンジン

この JavaScript エンジン上で、ECMAScript で定められている JavaScript の機能と、Web APIs と呼ばれる JavaScript からブラウザを操作するための機能などが提供されている。

この Web APIs の中では、例えば、ブラウザで読み込んだ HTML や CSS を操作するための DOM API や、サーバーからデータを取得するための XMLHttpRequest、Fetch API などが提供されている。

参考:Web API の紹介

参考:Web API

ブラウザの JavaScript エンジンの構成

JavaScript コード実行前

コード実行前に用意されるもの

JavaScript エンジンでは、JavaScript のコードの実行前に、次の2つを予め用意している。

  • グローバルオブジェクト
  • this キーワード

グローバルオブジェクト

JavaScript では、コードの実行前に「グローバルオブジェクト」と呼ばれるオブジェクトが、JavaScript エンジンによって一つ生成される。

このグローバルオブジェクトは、JavaScript の実行環境によってその実体が異なり、ブラウザの JavaScript の場合は Window オブジェクト、Node.js の JavaScript では global という名前のオブジェクトがそれに相当する。

実行環境グローバルオブジェクト
ブラウザWindow
Node.jsglobal

また、このグローバルオブジェクトにはコードのどこからでもアクセスすることができる。

Window オブジェクト

Window オブジェクトは、ブラウザ環境でのグローバルオブジェクトであり、ブラウザを操作するための機能は、すべてこの Window オブジェクトを通してアクセスする。つまり、この Window オブジェクトに Web APIs が含まれている。

Window オブジェクトは WebAPIs を含む

Window オブジェクトにコードから直接アクセスすることはできないが、Window オブジェクトは自分自身を参照する window プロパティを持っているため、window プロパティを介して Window オブジェクトにアクセスすることはできる。

window プロパティにアクセスする

したがって、Window オブジェクトに含まれているブラウザを操作するための機能(Web APIs)は、window プロパティを介してアクセスすることができる。

実行コンテキスト

実行コンテキストとは、コードを実行する際の文脈・状況のことを表し、次の3種類の実行コンテキストが存在する。

  • グローバルコンテキスト
  • 関数コンテキスト
  • eval コンテキスト

グローバルコンテキスト

グローバルコンテキスト内でコードを実行しているときには、次のものを使用することができる。

  • 実行中のコンテキスト内の変数・関数
  • グローバルオブジェクト
  • this キーワード

html ファイルから

<script src="main.js"></script>

のように main.js を読み込むとき、main.js の直下に書かれたコードが実行される環境がグローバルコンテキストとなる。そして main.js には次のようなコードを書いて実行させることができる。

let a = 0;  // 変数 a を宣言
function b() {}  // 関数 b を宣言

console.log(a);
b();

このように、グローバルコンテキスト内では、そのなかで宣言した変数 a・関数 b を使用することができる。

関数コンテキスト

関数コンテキストでは、次のものを使用することができる。

  • 実行中のコンテキスト内の変数・関数
  • arguments
  • super(特殊な環境でのみ使用可能)
  • this キーワード
  • 外部変数

関数コンテキストとは、関数が実行されているときに呼び出される環境のこと。例えば、次のようにグローバルコンテキスト内で関数 b を呼び出すとき、関数 b の { } 内が関数コンテキストとなる。

let a = 0;
function b() {
  console.log(arguments); // arguments を使用できる。
  console.log(this); // this を使用できる。
  console.log(a);  // 外部変数(関数 b の外で宣言された変数)a を使用することができる。
  // ここでは super は使用できない。
}

console.log(a);
b();

コールスタック(Call Stack)

スタック(stack)とは「積み重ね」という意味の言葉であり、コールスタックでは「実行中のコードが辿ってきたコンテキスト」を積み重ねている。

例えは、html ファイルから読み込まれる JavaScript のコードが次のようなものだった場合を考える。

function a() {
}
function b() {
  a();
}
function c() {
  b();
}
c();

まず、コードが開始されるとグローバルコンテキストが生成され、コールスタックに積まれる。下の画像は、コードの9行目にブレークポイントを設置し、関数 c が呼び出される直前の状態を表している。

コールスタックにグローバルコンテキストが積まれる

Call Stack にある (anonymous) がグローバルコンテキストを表している。

ひとつ Step を進めると関数 c の呼び出しが実行され、関数 c の中に処理が移るとともに、c の関数コンテキストが生成され Call Stack に積まれる。

コールスタックに関数 c のコンテキストが積まれる

さらに Step を進めると関数 b についても同様に関数コンテキストが生成されて Call Stack に積まれ、関数 b に処理が移る。

コールスタックに関数 b のコンテキストが積まれる

さらに Step を進めると関数 a についても同様に関数コンテキストが生成されて Call Stack に積まれ、関数 a に処理が移る。

コールスタックに関数 a のコンテキストが積まれる

ここから Step を進めると関数 a の呼び出しが終了し、Call Stack に積まれた a の関数コンテキストは削除され、関数 b の処理の続きが開始される。

コールスタックから関数 a のコンテキストが削除される

さらに Step を進めると関数 b の呼び出しが終了し、Call Stack に積まれた b の関数コンテキストは削除され、関数 c の処理の続きが開始される。

コールスタックから関数 b のコンテキストが削除される

さらに Step を進めると関数 c の呼び出しが終了し、Call Stack に積まれた c の関数コンテキストは削除され、関数 c の呼び出し以降の処理が実行されることになる。

コールスタックから関数 c のコンテキストが削除される

この様に、コードの文脈(コンテキスト)が変化する度に新しいコンテキストが生成されて Call Stack の一番上に追加されたり、実行済みのコンテキストが Call Stack から削除されたりする。

常に実行中のコンテキストが Call Stack の一番上に積まれている状態になっていることが分かる。

そして、このようにその時々の状況(コンテキスト)を Call Stack に積んでいく仕組みを JavaScript エンジンはもっている。

ホイスティング(Hoisting)

ホイスティングとは

ホイスティング(宣言の巻き上げ)とは、コンテキスト生成時に、そのコンテキスト内で宣言した変数や関数定義をメモリに配置することを指す。

したがって、コンテキスト内で let/const/var 命令で宣言された変数や function 命令で宣言された関数は、コード実行前には既にメモリに配置されている。

function 宣言の巻き上げ

例えは、html ファイルから読み込まれる JavaScript のコードが次のようなものだった場合、

my_func();

function my_func() {
  console.log('Hello World');
}

関数 my_func の呼び出しより後に定義が記述されていても、正しく実行できる。

関数宣言の巻き上げ

これは my_func(); の実行前、つまりグローバルコンテキストが生成される段階で、function 命令による関数宣言が巻き上げられてメモリに配置されるため。

Var 宣言の巻き上げ

Var 命令で宣言された変数の場合、コンテキスト生成時の巻き上げによりメモリに変数のための領域が取られ、JavaScript エンジンにより undefined が設定(初期化)される。

console.log('グローバルコンテキストの a の値:' + a);
var a = 10;
console.log('グローバルコンテキストの a の値:' + a);

myfunc();

function myfunc() {
  console.log('関数コンテキストの a の値:' + a);
  var a = 100;
  console.log('関数コンテキストの a の値:' + a);
}
var 宣言の巻き上げ

let, const 宣言の巻き上げ

let 命令や const 命令で宣言された変数の場合、コンテキスト生成時の巻き上げによりメモリに変数のための領域が取られるが、var 命令の場合とは異なり、undefined は設定(初期化)されない

console.log('グローバルコンテキストの a の値:' + a);
let a = 10; // let 命令による宣言
let 宣言の巻き上げ
console.log('グローバルコンテキストの b の値:' + b);
const b = 10; // const 命令による宣言
const 宣言の巻き上げ

関数式では巻き上げしない

関数式構文による関数の定義では、関数定義のホイスティングが行われない。

my_func();

const my_func = function() {
  console.log('Hello Worls');
}
関数リテラルは巻き上げられない

スコープ

スコープとは

実行中のコードから値と式が参照できる範囲のことを指す。つまり実行中のコードから変数や関数が見える(アクセスできる)かどうかを決める範囲のこと。

JavaScript には次の5種類のスコープが存在する。

  • Global スコープ
  • Script スコープ
  • 関数スコープ
  • ブロックスコープ
  • モジュールスコープ

Global スコープ、Script スコープ

グローバルコンテキスト(つまり関数内ではない)で宣言した変数や関数のスクープは次のようになる。

命令スコープ
let 命令Script スコープ
const 命令Script スコープ
var 命令Global スコープ
function 命令Global スコープ

これは次のサンプルで確かめることができる。

function a() {}
var b = 0;
let c = 0;
const d = 0;
debugger; // 開発者ツールを開いていると、ここで停止する。
Global スコープと Script スコープ

ただし、Global スコープと Script スコープは、一般的には使い勝手は変わらないため、まとめて Global スコープと呼ばれる。

Global スコープ

グローバルスコープ(Global Scope)とは、厳密には Window オブジェクトのことを指す。コードのどこからでもアクセスすることができる。

グローバルコンテキスト(つまり関数内ではない)において、var 命令で宣言された変数や、function 命令で宣言された関数はグローバルスコープ(Window オブジェクト)に所属する。

Window オブジェクトのプロパティになるので、Window オブジェクトの window プロパティを介してアクセスすることもできる。

function a() {
  console.log('Hello World')
;
}
var b = 100;

a();
window.a();
console.log(b);
console.log(window.b);
debugger;
window プロパティを介してアクセスできる
Global スコープ

Script スコープ

スクリプトスコープ(Script Scope)とは、グローバルコンテキスト(つまり関数内ではない)において let 命令const 命令で宣言された変数・定数が所属するスコープのこと。

Global スコープとは異なり Window オブジェクトのプロパティにはならないが、ほとんど使い勝手は変わらないので、Script スコープもまとめて Global スコープと呼ばれる。

let c = 10;
const d = 100;

console.log('c: ' + c);
console.log('window.c: ' + window.c);
console.log('d: ' + d);
console.log('window.d: ' + window.d);
debugger;
window プロパティを介してアクセスできない
Script スコープ

関数スコープ

関数スコープとは、function f() { … } の波括弧 { … } で囲まれた部分が形成する範囲(スコープ)のこと。

function f() {
  var a = 10;
  console.log(a);
}

f();  // 10
console.log(a); // Error
関数スコープ内での var 宣言
function f() {
  let b = 100;
  console.log(b);
}

f();  // 100
console.log(b); // Error
関数スコープ内での let 宣言

このように関数スコープ内において var, let, const 命令で宣言された変数は、関数の外部からはアクセス(参照)することができない。

これは、関数スコープ内において function 命令で宣言された関数も同様。

function f() {
  function ff() {
    console.log('Hello World');
  }
  ff();
}

f();  // Hello World
ff(); // Error
関数スコープ内での function 宣言

つまり function 命令により関数の宣言を行うと、1つの閉じた(外から参照できない)スコープが形成される。

ブロックスコープ

ブロックスコープとは

ブロックとは波括弧 { } のことを表し、ブロックスコープとは、波括弧 { … } で囲まれた部分が形成する範囲(スコープ)のこと。

if () { … } や for () { … } などの波括弧 { } もブロックスコープを形成する。

ただし、関数スコープ function f() { … } も波括弧 { } を含んではいるが、ブロックスコープとは一部の性質が異なるので注意。

また、ブロック内で var 命令で宣言された変数function 命令で宣言された関数については、ブロックスコープが無視されるので注意が必要。

ブロック内の let, const 命令

ブロック内において let, const 命令で宣言された変数は、ブロックの外部からはアクセス(参照)することができない。

{
  let a = 100;
  console.log(a); // 100
}

console.log(a); // Error
ブロックスコープ内の let 宣言
{
  const b = 999;
  console.log(b); // 999
}

console.log(b); // Error
ブロックスコープ内の const 宣言

ブロック内の var 命令

ブロック内において var 命令で宣言された変数は、ブロックの外部からアクセス(参照)できてしまう。つまりブロックスコープは形成されない

{
  var a = 10;
  console.log(a); // 10
}

console.log(a); // 10
ブロックスコープ内の var 宣言

関数スコープでは、var 命令による宣言であっても参照できなかった。したがって、関数スコープとブロックスコープは異なる性質をもっていることが分かる。

ブロック内の function 命令

ブロック内において function 命令で宣言された関数は、ブロックの外部からアクセス(参照)できてしまう。つまりブロックスコープは形成されない

{
  function f() {
    console.log('Hello World');
  }
}

f();  // Hello World
ブロックスコープ内の function 宣言

ブロック内の関数式

ブロック内で関数リテラル形式アロー関数で定義された関数については、それを代入した変数を介してアクセスするため、変数が var, let , const のいずれで宣言されているかによって、ブロックの外部からアクセス(参照)できるかどうかどうかが決まる。

{
  var vf = function() {
    console.log('var');
  };
  let lf = function() {
    console.log('let')
  }
}

vf(); // var
lf(); // Error
ブロックスコープ内の関数リテラル
{
  var vf = () => console.log('var');
  let lf = () => console.log('let');
}

vf(); // var
lf(); // Error
ブロックスコープ内のアロー関数

if 文によるブロックスコープ

if () { … } や for () { … } などの波括弧 { } もブロックスコープを形成する。

if (true) {
  var a = 10;
  let b = 100;
}
console.log(a)  // 10
console.log(b); // Error
if 文によるブロックスコープ

レキシカル(Lexical)スコープ

コードを書く場所によって、参照できる変数が変わるスコープのことをレキシカルスコープ(Lexical Scope)という。

レキシカルスコープは、コードを記述した時点で決定するため、静的スコープとも呼ばれる。

次のサンプルで考える。

let x = 1;
function f() {
  let y = 2;
  function g() {
    let z = 3;
    console.log(x, y, z)
  }
  g();
}
f();

このコードの実行を「console.log(x, y, z)」の直前で停止させると、この時点では次の4つのスコープ(Scope)が存在していて、アクセス可能なことが分かる。

  • Local スコープ(関数 g の関数スコープ)
  • Closure スコープ(関数 f の関数スコープ)
  • Script スコープ
  • Global スコープ
Closure スコープが存在する

したがって、これらのスコープに存在している変数 x, y, z にアクセスすることができる。

x, y, z の全てを参照できる

この場合の Local スコープ(関数 g の関数スコープ)のように、実行中のコード行のスコープからは、その外部のスコープ(外部スコープ)を参照することができる。

一方で、先ほどのコードを次のように書き替える。

let x = 1;
function f() {
  let y = 2;
  // function g() {
  //   let z = 3;
  //   console.log(x, y, z)
  // }
  g();
}
f();

function g() {
  let z = 3;
  console.log(x, y, z)
}

関数 g の定義を関数 f の外側に移した。

このコードの実行を「console.log(x, y, z)」の直前で停止させてみると、実行中のコード行のスコープには、先ほどの Closure スコープ(関数 f の関数スコープ)が存在しない。

関数 f のスコープは存在しない

そのため、Closure スコープ(関数 f の関数スコープ)に含まれる変数 y にアクセスすることはできない。

y を参照できない

このように、外部スコープを参照できるという仕組みのため、コードを配置する位置によって参照できる変数(参照できるスコープ)が変化する。

この実行中のコードから見た外部スコープのことをレキシカルスコープと呼ぶ。

また、コードを配置する位置によって参照できるスコープが変化するという言語の仕様を指してレキシカルスコープ、あるいは静的スコープと呼ぶこともある。

スコープチェーン

スコープチェーンとは

スコープチェーン(Scope Chain)とは、スコープが複数の階層状に連なっている状態のことをいう。つまり、あるスコープが他のスコープを含んでいる状態をスコープチェーンと呼んでいる。

// <-- Global スコープ or Script スコープ
let x = 1;
{ // <------ Block スコープ
  let y = 2;
  function f() {  // <-- 関数スコープ(f)
    let z = 3;
    function g() {  // <---- 関数スコープ(g)
      let w = 4;
      console.log('Hello World');
    } // <------------------ 関数スコープ(g)
    g();
  } // <---------------- 関数スコープ(f)
  f();
} // <------ Block スコープ

// <-- Global スコープ or Script スコープ

スコープチェーンによる変数参照の仕組み

次のサンプルで考える。

let x = 1;
let y = 2;
let z = 3;
function f() {
  let x = 10;
  let y = 20;
  function g() {
    let x = 100;
    console.log(x, y, z)
  }
  g();
}
f();

このコードの実行を「console.log(x, y, z)」の直前で停止させると、次のように内側のスコープから変数を参照していることが分かる。

スコープチェーン

このようにスコープが複数階層になって連なっている状態では、一番内側となる現在のスコープから変数を探し始め、無ければ一つずつ外側に同じ変数名がないかを探しに行く。

Global スコープ が一番外側

Global スコープと Script スコープに同じ変数がある場合を考える。

let a = 'Script Scope';
window.a = 'Global Scope';
console.log(a);

これを実行すると「Scipt Scope」が優先される。

Global スコープより Script スコープが優先される

このことから Global スコープは Script スコープよりも外側のスコープであることが分かる。

JavaScript では多階層のスコープ内で変数を見つける際には、スコープチェーンを現在のスコープから順番に外側へ向かって検索し、最終的に一番外側の Global スコープまで探して、無かった場合にはエラーを発生させる。

間違いやすい例

let a = 1;
function f() {
  console.log(a); // ホイスティングにより後ろの a の宣言が巻き上げられる。
  // ... 処理 ...
  // ... 処理 ...
  // ... 処理 ...
  // ... 処理 ...
  let a = 10;
}
f();

クロージャー(Closure)

クロージャーとは

クロージャー(Closure)とは、レキシカルスコープ(外部スコープ)の変数を関数が使用している状態のことを指す。

このクロージャーという状態を使用することによって、次のような特殊な機能を持った関数を実装することができる。

  • 値を保持できる変数を持った関数
  • 動的な関数の生成

値を保持できる変数を持った関数

次のようなコードを考える。

let count = 0;

function increment() {
  console.log(++count);
}

increment();  // 1
increment();  // 2
increment();  // 3

このコードで定義された関数 increment() は、呼び出す度に count の値を +1 してコンソールに出力することができる。

ただし、ここで使用している変数 count は Global スコープ(Script スコープ)にあるため、コードのどこからでも変更できてしまう。つまり、誤って変更してしまう可能性がある。

そこで、この変数 count を関数 increment() の内部に記述することで関数スコープ内に閉じ込めてしまい、外部からはアクセスできないようにしたい。

function increment() {
  let count = 0;  // ここに移した。
  console.log(++count);
}

increment();  // 1
increment();  // 1
increment();  // 1

しかし、単純にこのように関数内に count の宣言を移動しただけでは、関数を呼び出す度に count の値が 0 で初期化されてしまい、上手く行かない。

そこでクロージャーと呼ばれる、レキシカルスコープ(外部スコープ)の変数を関数が使用(参照)している状態を作り出してみる。

function increment() {
  let count = 0;
  function f() {
    console.log(++count);
  }
  f();
}

increment();  // 1
increment();  // 1
increment();  // 1

このコードでは関数 increment の内部で、外部スコープの変数 count を参照する関数 f を宣言して呼び出すようにしてみた。この関数 f は外側にある変数 count を参照している、つまりクロージャーだ。

しかし、結果は前のコードと変わらない。関数 increment を呼び出す度に count が初期化されてしまうからだ。

そこでもう一つ工夫してみる。このクロージャーな関数を外に取り出してみることを考える。

function increment() {
  let count = 0;
  function f() {
    console.log(++count);
  }
  return f;
}

const _f = increment();
_f();  // 1
_f();  // 2
_f();  // 3

このコードでは関数 increment の呼び出しにより、クロージャー関数 f が返されるので、返された f を変数 _f に格納して3回呼び出している。するとカウント値が 1, 2, 3 とカウントアップされる結果が得られた。

このように、関数の内部で定義したクロージャーな関数を外部に取り出すと、取り出したクロージャーな関数が参照している変数は消滅せずに保持されたままになる。

つまり、このクロージャーの性質を利用することで、「関数の内部でのみ使用できて、かつ値を保持し続ける変数」を持つ関数を作成することができる。

function specialFuncFactory(init) {
  let x = init;
  return function() {
    // 変数 x を参照した処理
  }
}

const specialFunc = specialFuncFactory(100);
specialFunc();
specialFunc();

function incrementFactory(init) {
  let count = init;
  return function() {
    console.log(++count);
  }
}

const increment = incrementFactory(10);
increment();  // 11
increment();  // 12
increment();  // 13

動的な関数の生成

ここでいう「動的な関数の生成」とは、プログラムのコード中でそのときの必要性に応じた設定の関数を生成できることを指す。

function animalBarkFactory(animal) {
  function bark(voice) {
    console.log(animal + 'が' +  voice + 'と鳴いた。');
  }
  return bark;
}

const dogBark = animalBarkFactory('犬');
const catBark = animalBarkFactory('猫');

dogBark('わんわん');      // 犬がわんわんと鳴いた。
dogBark('キャンキャン');  // 犬がキャンキャンと鳴いた。
catBark('にゃんにゃん');  // 猫がにゃんにゃんと鳴いた。
catBark('ミャー');        // 猫がミャーと鳴いた。

このコードでは、関数 animalBarkFactory の中で宣言した関数 bark を雛形として、dogBark, catBark という2つの異なる関数を生成している。

関数 bark はレキシカルスコープ(外部スコープ)の引数 animal を参照しているので、クロージャー関数となり、したがってその複製である2つの関数 dogBark, catBark のなかで参照されている引数 animal の値は、animalBarkFactory の実行後も保持されたままになる。

上のコードの実行結果から、dogBark, catBark が内部で参照している animal の値は異なっている、つまりそれぞれ別々のメモリを割り当てられて記憶されていることが分かる。

このように、1つの関数 animalBarkFactory から、その中に定義されている関数を雛形として複数の異なる関数を生成することができた。引数に与える値を変更することで、必要な関数を自由に作り出すことができる。つまり、クロージャーを使うことで動的に関数を生成できたことになる。

即時関数(IIFE)

即時関数とは、関数定義と同時に一度だけ実行される関数のこと。即時関数は IIFE(Immediately Invoked Function Expression) という略称で呼ばれることもある。

次のように記述する。

let result = (function(仮引数) {
  ...
  return 戻り値;
})(実引数);

この書き方は、丸括弧 ( ) が持つ演算子としての異なる2つの機能を組み合わせて使用している。丸括弧 ( ) には次の2つの演算子としての役割がある。

  • 関数呼び出し演算子
  • グループ化演算子

関数呼び出し演算子 ( )

関数名の直後に丸括弧を書くことで、関数を(引数を指定して)呼び出すことができる。

function my_func(s) {
  console.log('Hello ' + s);
}

my_func('Taro'); // Hello Taro

関数 my_func を次のように定義しても、同様に丸括弧 ( ) で呼び出して実行することができる。

const my_func = function(s) {
  console.log('Hello ' + s);
}

my_func('Taro'); // Hello Taro

つまり、Function オブジェクトの直後に丸括弧 ( ) を書くと、関数の処理が呼び出されて実行される。

Functionオブジェクト(実引数);

グループ化演算子 ( )

グループ化演算子は四則演算でよく利用されるお馴染みのもの。例えば、次のように式の計算の優先順位を変えることができる。

console.log(1 + 2 * 3); // 1 + 6
console.log(1 + (2 * 3)); // 1 + 6
console.log((1 + 2) * 3); // 3 * 3

即時関数

Function オブジェクトの直後に丸括弧 ( ) を書けば関数呼び出しを実行できるのだから、次のように関数を定義して実行させてみる。

function(s) {console.log('Hello ' + s);}('Taro');
// Uncaught SyntaxError: Function statements require a function name

残念ながら構文エラーが発生する。関数名が必要だと言われている。つまり、無名関数(匿名関数)の定義が上手く解釈されていないようだ。

そこでグループ化演算子 ( ) で無名関数の部分を明示的に一つのまとまりとして表現してみる。

(function(s) {console.log('Hello ' + s);})('Taro'); // Hello Taro

これは成功した。つまり、関数を定義しつつ、その関数を実行してしまう記法(即時関数)が手に入った。

(function(仮引数) { ... })(実引数);

(function(仮引数) {
  ...
})(実引数);

関数に戻り値も定義すれば、関数の呼び出しの記述は戻り値で置き換わるので、変数に代入して受け取ることもできる。

let result = (function(仮引数) {
  ...
  return 戻り値;
})(実引数);

let result = (function(s) {
  return 'Hello ' + s;
})('Taro');

console.log(result);  // Hello Taro

即時関数はどこで使うのか

関数スコープ内の変数や関数は、関数の外からはアクセスできないのだった。

そのため、ある処理の中でしか使用できない変数・関数と、その処理の外側でも使用できる変数・関数を区別したいときに、即時関数を使って区別することができる。

即時関数で変数・関数のスコープを制限

ある処理を即時関数でくくることで、その処理中の変数名が他の処理の変数と競合することを心配しなくて済む。

例えば、次のような3つのファイル index.html, app.js, app2.js を作成してブラウザで index.html を開いてみる。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <script src="app.js"></script>
  <script src="app2.js"></script>
</body>
</html>

app.js

let a = 1;
let b = 2;
console.log('app.js', a, b);

app2.js

let a = 100;
let b = 200;
console.log('app2.js', a, b);

開発者ツールでコンソールに吐き出される内容を確認すると、エラーが出力されている。

変数が競合する

app.js の処理は無事に実行されているが、app2.js の変数 a の宣言でエラーが発生している。変数名が競合しているためだ。

そこで2つの JavaScript ファイルをそれぞれ即時関数でくくってみる。

app.js

(function() {
  let a = 1;
  let b = 2;
  console.log('app.js', a, b);
})();

app2.js

(function() {
  let a = 100;
  let b = 200;
  console.log('app2.js', a, b);
})();

結果

変数の競合が解消された

このように即時関数というテクニックをスコープを制限する目的で使用することにより、その中の変数や関数の名前が他と競合する可能性を排除することができる。

即時関数の外でも使いたいなら return する

即時関数の中で宣言した変数や関数で、即時関数の外でも使用したいものがあるなら、即時関数の戻り値をオブジェクトリテラルにして、その中に外に出したいものを含めればよい。

let result = (function() {
  let privateVal = 0;
  let publicVal = 10;

  function privateFunc() {
    console.log('this is private');
  }
  function publicFunc() {
    console.log('this is public');
  }

  return {
    publicVal,
    publicFunc
  }
})();

console.log(result.publicVal);  // 10
result.publicFunc();  // this is public

変数

let, const, var の機能の違い

let, cost, var を次の機能で比べてみる。

  • 変数の再宣言
  • 変数への再代入
  • ブロックスコープの有効性
  • 関数スコープの有効性
  • ホイスティングによる初期値の設定

変数の再宣言

let により同じ変数を再度宣言するとエラーが発生する。

let a;
let a; //  Uncaught SyntaxError: Identifier 'a' has already been declared

const により同じ定数を再度宣言するとエラーが発生する。

const c = 0;
const c = 0;  // Uncaught SyntaxError: Identifier 'c' has already been declared

var により同じ変数を再宣言してもエラーが発生しない

var a;
var a;  // エラーは発生しない

変数への再代入

let で宣言した変数は値を再代入できる。

let a = 0;
a = 1;  // 値を再び代入できる

const で宣言した定数は値を再代入するとエラーが発生する

const c = 0;
c = 1;  // Uncaught TypeError: Assignment to constant variable.

var で宣言した変数は値を再代入できる。

var a = 0;
a = 1;  // 値を再代入できる

ブロックスコープの有効性

let で宣言した変数はブロックスコープが有効。つまりブロック内で let で宣言した変数の参照可能な範囲は、ブロック内に限定される。

{
  let a = 100;
  console.log(a); // 100
}
console.log(a); // Uncaught ReferenceError: a is not defined

const で宣言した定数はブロックスコープが有効。つまりブロック内で const で宣言した定数の参照可能な範囲は、ブロック内に限定される。

{
  const c = 100;
  console.log(c); // 100
}
console.log(c); // Uncaught ReferenceError: c is not defined

var で宣言した変数はブロックスコープが無効。つまりブロック内で var で宣言した変数は、ブロック外からでもアクセスできてしまう。

{
  var a = 100;
  console.log(a); // 100;
}
console.log(a); // 100

関数スコープの有効性

let, const, var のいずれで宣言しても、関数スコープは有効。つまり関数内で let, const, var で宣言した変数・定数の参照可能な範囲は、関数内に限定される。

function f() {
  let a = 100;
  console.log(a); // 100
}

f();
console.log(a); // Uncaught ReferenceError: a is not defined
function f() {
  const c = 100;
  console.log(c); // 100
}

f();
console.log(c); // Uncaught ReferenceError: c is not defined
function f() {
  var a = 100;
  console.log(a); // 100
}

f();
console.log(a); // Uncaught ReferenceError: a is not defined

ホイスティングによる初期値の設定

let で宣言した変数の場合、宣言の前に値を取得しようとすると、エラーが発生する。

console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 100;

const で宣言した定数の場合、宣言の前に値を取得しようとすると、エラーが発生する。

console.log(c); // Uncaught ReferenceError: Cannot access 'c' before initialization
const c = 100;

var で宣言した変数の場合、宣言の前に値を取得しようとすると、ホイスティングにより undefined が初期値に設定されているため、undefined が取得される

console.log(a); // undefined
var a = 100;

データ型

JavaScript(ECMAScript)では、次の8種類のデータ型が存在する。

データ型補足
数値型(number)10, 3.14
文字列型(string)“JavaScript”
真偽型(boolean)true / false
undefined 型undefined値が未定義であることを表す。
null 型null参照を保持していない、空であることを表す。
シンボル型(symbol)ES2015から追加された一意で不変な値のデータ型
bigint 型12n任意の巨大な整数に使用することができる。
オブジェクト(object){ “key”: “value” }

暗黙的な型変換

JavaScript は動的型付け言語

JavaScript は動的型付け言語であり、したがって次のような特徴をもつ。

  • 変数宣言時に型の宣言を行わない
  • 変数を使用する状況によって変数の型が変更される

そのため、「let a = 0;」のような変数への代入が実行されたときに、その代入された値で一時的に型が決定されることになるが、この変数の型は使用される状況によって変更される。

一方でC言語やJava言語のような静的型付け言語では、

  • 変数宣言時に型の宣言を行う
  • 変数を使用する状況に依らず、常に同じ型を保持する

という特徴がある。

暗黙的な型変換

暗黙的な型変換(implicit conversion)とは、変数が呼ばれた状況によって、変数のデータ型が自動的に変換されること。

typeof 演算子

変数のデータ型の確認には typeof 演算子を利用する。

let a = 0;
console.log(typeof a);  // number

文字列型 + 数値型

文字列と数値を + 演算子で演算すると結果は文字列になる。これは異なるデータ型を演算する際には、どちかのデータ型に合わせてから演算を行うが、「文字列+数値」では数値を暗黙的な型変換により文字列型に変換するため。

let b = '1' + 0;
console.log(typeof b, b); // string 10

b = 3.14 + '1';
console.log(typeof b, b); // string 3.141

文字列型 - 数値型

文字列と数値を-演算子で演算すると結果は数値になる。これは異なるデータ型を演算する際には、どちかのデータ型に合わせてから演算を行うが、-演算子は数値の計算にしか使用しないことから、文字列が暗黙的な型変換により数値型に変換されるため。

let c = 15 - '10';
console.log(typeof c, c); // number 5

c = '12' - 2;
console.log(typeof c, c); // number 10

数値型 + null 型、数値型 - null 型

この場合は、null が数値の 0 に暗黙的に変換される。

let d = 10 - null;
console.log(typeof d, d); // number 10

d = 10 + null;
console.log(typeof d, d); // number 10

数値型 + 真偽型、数値型 - 真偽型

この場合は、真偽値が数値に変換される。

let e = 10 - true;
console.log(typeof e, e); // number 9

e = 10 + true;
console.log(typeof e, e); // number 11

e = 10 - false;
console.log(typeof e, e); // number 10

e = 10 + false;
console.log(typeof e, e); // number 10

厳格な等価性と抽象的な等価性

2つの値が等しいかどうかを比較するには、「厳格な等価性(===)」と「抽象的な等価性(==)」という2つの比較の仕方(演算子)がある。

この2つの違いはデータ型の比較を行うかどうか(暗黙的な型変換を行うかどうか)という点にある。

  • 厳格な等価性(===)では、型の比較を行う。
  • 抽象的な等価性(==)では、型の比較を行わない。

例えば、次の例の場合、10 は数値型であり、’10’ は文字列型のため、型が異なるので厳格な等価性(===)では等しくない(false)という結果になる。

一方で抽象的な等価性(==)では、比較する2つのデータの型が異なるときは暗黙的な型変換により型を合わせる作業が行われ、その後で厳格な等価性で比較するということが行われる。この例では文字列の ’10’ が数値の 10 に暗黙的に変換される。その結果等しいと見なせるので true を返している。

console.log(10 === '10'); // false
console.log(10 == '10');  // true  <-- 文字列の '10' が数値の 10 に暗黙的に変換される

抽象的な等価性(==)は、暗黙的な型変換により型を揃える作業が行われるため、思わぬバグを生む原因にもなるので、基本的に厳格な等価性(===)を使うようにする。

なお、抽象的な等価性に(==)ついては、ECMAScript の仕様に比較のルールが示されているので参考になる。

falsy と truthy

falsy な値」とは、Boolean 型に変換したときに false になる値のこと。true になる値は「truthy な値」と呼ばれる。

falsy な値には次の8つが該当する。

  • false
  • 0
  • -0(数値のマイナスゼロ)
  • 0n(BigInt 型のゼロ)
  • “”(空文字列)
  • null
  • undefined
  • NaN(数値を期待したが、結果を数値で表現できないことを表す。0/0 など)

これ以外のものは全て truthy な値ということになる。

console.log(Boolean(0)); // false
console.log(Boolean(-0));  // false
console.log(Boolean(0n));  // false
console.log(Boolean(""));  // false
console.log(Boolean(null));  // false
console.log(Boolean(undefined)); // false
console.log(Boolean(NaN)); // false
console.log(Boolean(10));   // true
console.log(Boolean(-20));  // true
console.log(Boolean(3.14)); // true
console.log(Boolean("0"));  // true
console.log(Boolean("null")); // true
console.log(Boolean([]));   // true
console.log(Boolean({}));   // true

AND 演算と OR 演算

JavaScript では Boolean 型でない値も truthy な値は true に、falsy な値は false に変換できるため、AND 演算や OR 演算を行うことができる。

AND 演算子(&&)

変数 a と変数 b の AND 演算の結果 a && b は次のように定められている。

aa && b
falsya が返される
truthyb が返される
console.log(false && 100);      // false
console.log(undefined && 100);  // undefined
console.log(0 && 100);          // 0
console.log(NaN && 100);        // NaN

console.log(1 && false);          // false
console.log({} && NaN);           // NaN
console.log([] && undefined);     // undefined
console.log(3.14 && {prop: 100}); // {prop: 100}

この定義は初見では不安を感じるが、a, b の値そのものよりも truthy, falsy としての性質に注目すれば、一般的なよくある AND 演算と同じであることが分かる。

aba && b
falsyfalsya(falsy)
falsytruthya(falsy)
truthyfalsyb(falsy)
truthytruthyb(truthy)

OR 演算子(||)

変数 a と変数 b の OR 演算の結果 a || b は次のように定められている。

aa || b
falsyb が返される
truthya が返される
console.log(false || 100);      // 100
console.log(undefined || NaN);  // NaN
console.log(0 || {});           // {}
console.log(NaN || [1, 2]);     // [1, 2]

console.log(1 || false);          // 1
console.log({} || NaN);           // {}
console.log([] || 100);           // []
console.log(3.14 || {prop: 100}); // 3.14

AND 演算と同様に、a, b の値そのものよりも truthy, falsy としての性質に注目すれば、一般的なよくある OR 演算と同じであることが分かる。

aba && b
falsyfalsyb(falsy)
falsytruthyb(truthy)
truthyfalsya(truthy)
truthytruthya(truthy)

OR 演算の応用(初期値の設定)

変数が undefined, null, “”(空文字列)のような falsy の値であるときは初期値で置き換えたいという場面で、OR 演算(||)を使用することができる。

function getUserInfos() {
  return {
    name: '',
    id: 1234
  };
}
let userInfos = getUserInfos();

// name プロパティが空文字列なら「名無しさん」としたい。
let userName = userInfos.name || '名無しさん';
console.log(userName);  // 名無しさん

// age プロパティが存在しない(undefined)なら「年齢不詳」としたい。
console.log(userInfos.age); // undefined
let userAge = userInfos.age || '年齢不詳'
console.log(userAge); // 年齢不詳

プリミティブ型とオブジェクト

データ型

JavaScript(ECMAScript) には、8種類のデータ型が存在する。

データ型補足
数値型(number)10, 3.14
文字列型(string)“JavaScript”
真偽型(boolean)true / false
undefined 型undefined値が未定義であることを表す。
null 型null参照を保持していない、空であることを表す。
シンボル型(symbol)ES2015から追加された一意で不変な値のデータ型
bigint 型12n任意の巨大な整数に使用することができる。
オブジェクト(object){ “key”: “value” }

そしてこの8種類のデータ型は、「プリミティブ型(Primitive Type)」と「オブジェクト」に分けることができる。

  • 数値型(number)
  • 文字列型(string)
  • 真偽型(boolean)
  • undefined 型
  • null 型
  • シンボル型(symbol)
  • bigint 型

これらはプリミティブ型であり、これらプリミティブ型以外はすべてオブジェクトとして扱われる。

プリミティブ型(Primitive Type)

全てのプリミティブ型の値は、一度作成すると変更できない。この性質を immutable(不変)と呼んでいる。

文字列もプリミティブ型であり、一度作成した文字列は変更することができない。

オブジェクト

オブジェクトを変数に代入する場合、そのオブジェクトの参照(アドレス)が格納される。

オブジェクトは変更することが可能。この性質を mutable(可変) と呼んでいる。

オブジェクトの分割代入

オブジェクトの分割代入とは、ES6 から導入された構文であり、オブジェクトから特定のプロパティを抽出して、個々の変数に分解して宣言を行うことができるもの。

const obj = {
  a: 123,
  b: 'apple',
  c: true
};

let {a, b, x} = obj;
console.log(a, b, x);  // 123 "apple" undefined

このように新しく宣言する変数名と抽出元のオブジェクトのプロパティ名は一致させる必要があるが、次のようにして名前を変えることはできる。

const obj = {
  a: 123,
  b: 'apple',
  c: true
};

let {a, b: fruite, x} = obj;
console.log(a, fruite, x);  // 123 "apple" undefined

console.log(b); // Uncaught ReferenceError: b is not defined

関数の引数におけるオブジェクトの分割代入

関数の引数においてもオブジェクトの分割代入を利用することができる。

let obj = {
  a: 123,
  b: 'apple',
  c: true
};

function f({a, b}) {
  console.log(a, b);
}

f(obj); // 123 "apple"

関数

JavaScript の関数の特徴

JavaScript における関数には、次のような特徴がある。

  • 関数呼び出しの際の実引数を省略できる。
  • 関数名が重複してもエラーにならない。
  • 引数の個数は関数が異なる理由にならない。
  • 関数式の場合は、関数名の重複でエラーになる。
  • 引数には初期値を設定できる。(ES6 以降)
  • arguments で実引数を受け取れる。
  • 戻り値を定義しない場合は undefined が返る。
  • 関数は実行可能なオブジェクトである。

関数呼び出しの際の実引数を省略できる

実引数を省略した場合、後ろ側の引数が省略されたと見なされ、その引数には undefined が設定される。

function f(a, b, c, d) {
  console.log('a:', a);
  console.log('b:', b);
  console.log('c:', c);
  console.log('d:', d);
}

f(1, 2);
// a: 1
// b: 2
// c: undefined
// d: undefined

関数名が重複してもエラーにならない

関数名が重複した場合は、後から宣言された関数が実行される。

function f(name) {
  console.log('Hello ' + name);
}
function f() {
  console.log('Hello World');
};
f('Taro');  // Hello World

引数の個数は関数が異なる理由にならない

引数が省略できることから、引数の個数が違う同名の関数を作っても、後に宣言した関数が実行される。

function f(a, b, c) {
  console.log(a, b, c);
}
function f() {
  console.log('Hello World');
};
f(1, 2, 3);  // Hello World

関数式の場合は、関数名の重複でエラーになる

let, cont による宣言を使用した関数式なら、関数名の重複時にエラーが発生する。

const f = function (a, b, c) {
  console.log(a, b, c);
}
function f() {  // Uncaught SyntaxError: Identifier 'f' has already been declared
  console.log('Hello World');
};
f(1, 2, 3);

引数には初期値を設定できる。(ES6 以降)

ES6 以降であれば、引数に初期値を設定することができる。

function f(name = 'World') {
  console.log('Hello ' + name);
}

f('Taro');    // Hello Taro
f();          // Hello World
f(null);      // Hello null
f(undefined); // Hello World

このサンプルのように、null を渡した場合は初期値が適用されないが、undefined を渡した場合は引数を省略した場合と同様に初期値が適用される。

arguments で実引数を受け取れる

arguments は関数コンテキストにおいて自動的に生成されるオブジェクトであり、関数呼び出し時に渡された全ての実引数が保持されている

function f(a, b, c) {
  console.log(arguments);
}

f(100, 'apple', true);

関数の仮引数を経由しないで、arguments 経由で実引数を受け取ることができる。

function f(a, b) {
  console.log(arguments[0]);
  console.log(arguments[1]);
  console.log(arguments[2]);
  console.log(arguments[3]);
  console.log(arguments[4]);
}

f(100, 'apple', 'orange', 'banana', true);
// 100
// apple
// orange
// banana
// true

戻り値を定義しない場合は undefined が返る

retrun 文で返却する値を書かなかった場合や、return 文そのものを省略した場合は、undefined が返却される。

function f() {
  return;
}
console.log(f()); // undefined

function g() {
  // return 文を省略
}
console.log(g()); // undefined

関数は実行可能なオブジェクトである

関数はオブジェクトであり、実行可能である点だけが他のオブジェクトと異なる。つまり、実行可能である点を除けば、それ以外は他のオブジェクトと同じ挙動をする

// 関数fの宣言
function f() {
  console.log('f was called');
}

// fは関数呼び出し演算子()を使用して実行することができる。
f();  // f was called

// f はオブジェクトであるから、プロパティを追加できる。
f.prop1 = 'apple';
f['prop2'] = 'orange';
console.log(f.prop1, f.prop2);  // apple orange

// メソッドも追加できる。
f.method = function() {
  console.log('method was called');
}
f.method(); // method was called

コールバック関数

コールバック関数とは

関数もオブジェクトであることから、他の関数に引数として渡すことができるし、渡した先で呼び出してもらうことができる。

// 渡される関数を定義
function f(name) {
  console.log('Hello ' + name);
}

// 引数で関数を受け取り、実行する関数
function g(func) {
  func('World');
  func('Taro');
}

g(f);
// Hello World
// Hello Taro

このサンプルの関数 f のような、関数の引数として渡し、その処理の中で実行してもらう関数のことを「あとで呼び出してもらう」という意味でコールバック関数(callback function)と呼ぶ。

コールバック関数として無名関数も渡せる

無名関数も当然オブジェクトであるので、コールバック関数として関数に渡して実行してもらうことができる。

function my_func(cb) {
  cb('World');
}

const callback = function(name) {
  console.log('Hello ' + name);
}
my_func(callback);  // Hello World

// 次のように書いても同じ
my_func(function(name) {
  console.log('Hello ' + name);
}); // Hello World

コールバック関数の実例:setTimeout

setTimeout 関数は、JavaScript エンジンによって用意される API の一つであり、第一引数にコールバック関数を受け取り、第二引数で指定した経過時間後に、渡されたコールバック関数を実行してくれる。

function greet(name) {
  console.log('Hello ' + name);
}

setTimeout(greet, 2000);
// 2000ms 後に greet が呼び出されるが、
// 引数なしの greet() という形で呼び出されるため、
// 結果は Hello undefinde となる。

this キーワード

実行コンテキストによって、this キーワードの参照先は変化する。

オブジェクトのメソッドにおける this

オブジェクトのメソッドとして呼び出された関数内での this キーワードは、その呼び出し元のオブジェクトを指す。

const obj = {
  name: 'Taro',
  hello: function() {
    console.log('this: ', this);
    console.log('Hello ' + this.name);
  }
}

obj.hello();

このサンプルの場合、obj.hello という形で obj を経由して hello メソッドを呼び出しているので、この時の hello メソッド内の this キーワードは、obj を参照するようになる。

オブジェクトのメソッドにおける this キーワード

関数における this

オブジェクトのメソッドとしてではなく、単なる関数として実行された場合、その関数内での this キーワードはグローバルオブジェクトを指す。

const obj = {
  name: 'Taro',
  hello: function() {
    console.log('this: ', this);
    console.log('Hello ' + this.name);
  }
}

// obj.hello();
const func = obj.hello;
func();

obj の hello メソッド(への参照)を変数 func にコピーして func を実行すると、obj を経由せずに hello メソッドをただの関数として実行することができる。

この場合、関数 func、つまり関数 hello 内での this キーワードは、グローバルオブジェクト(Window オブジェクト)を指すようになる。

関数における this キーワード

余談だが、この場合、this は Window オブジェクトを指すので this.name は undefined になり、Hello undefined と表示されるかと思いきや、そうはならない。Window オブジェクトには既定で name プロパティが存在するらしいため。

今度は逆の例を見てみる。つまり関数 func を普通の関数として定義し、それをオブジェクト obj のメソッドとして登録して呼び出してみる。

function func() {
  console.log('this:', this);
  console.log('Hello ' + this.name);
}

const obj = {
  name: 'Taro',
  method: func
}

func();
obj.method();
今度は関数をオブジェクトのメソッドとして登録

やはり、関数として実行した場合の this キーワードは Window オブジェクトを参照し、オブジェクトのメソッドとして呼ばれた時はそのオブジェクトを参照するようになることが分かる。

コールバック関数における this

コールバック関数として実行される関数内での this キーワードが参照するものは何か?

これはコールバック関数を引数として受け取って実行する側の関数が、どのように渡されたコールバック関数を実行するのかに依存する。

const obj = {
  name: 'Taro',
  hello: function() {
    console.log(this);
    console.log('Hello ' + this.name);
  }
}

function func(callback) {
  callback();
}

func(obj.hello);
渡されたコールバック関数をそのまま実行

このサンプルの様に、コールバック関数としてオブジェクトのメソッド obj.hello を渡しても、引数 callback で受け取るのは関数への参照(関数のアドレス)だけであり、obj の情報が渡されるわけではない。

そのため、callback をそのまま実行すれば、ただの関数呼び出しとなり、this キーワードはグローバルオブジェクト(Window オブジェクト)を参照する。

もちろん、上記コードを修正して、callback をオブジェクトのメソッドとして登録し、メソッドとして実行するような構造にすれば、this キーワードは呼び出し元のオブジェクトを参照するようになる。

const obj = {
  name: 'Taro',
  hello: function() {
    console.log(this);
    console.log('Hello ' + this.name);
  }
}

function func(callback) {
  // callback();
  const obj = {
    name: 'Jiro',
    method: callback
  }
  obj.method();
}

func(obj.hello);
渡されたコールバック関数をオブジェクトのメソッドとして登録して実行

bind メソッド

bind メソッドで this を束縛する

bind メソッドを用いると、指定した値を this が参照するようにした新しい関数を生成することができる。

this キーワードが参照する値を指定した値で固定することを、「this を束縛する」と表現する。bind メソッドの第1引数で渡した値で this は束縛される。

function f() {
  console.log('this:', this);
  console.log('Hello ' + this.name);
}

const fb = f.bind({name: 'Jiro'});

f();
fb();
bind メソッドで this を束縛
bind メソッドは引数も束縛できる

bind メソッドの第2引数に値を指定すると、引数も束縛することができる。つまり、bind メソッドを用いると、this 引数が指定された値で束縛された新しい関数を生成できる。

function g(greet, name) {
  console.log(greet + ' ' + name);
}

const gb = g.bind(null, 'Hello', 'Taro');

gb();               // Hello Taro
gb('bye', 'Jiro');  // Hello Taro

このことを利用すれば、setTimeout 関数のような、受け取ったコールバック関数を引数なしで実行する関数を、引数ありで実行させることができる。

function f(callback) {
  callback();
}

f(console.log); // 何も表示しない
f(console.log.bind(null, 'Hello World')); // Hello World

setTimeout(console.log, 1000);  // 何も表示しない
setTimeout(console.log.bind(null, 'Hello World'), 1000);  // Hello World

call メソッド、apply メソッド

bind は関数を生成、call / apply は関数を実行

bind メソッドは、this や 引数を指定した値で束縛した新しい関数を生成するためのものだった。それに対し、call メソッドや apply メソッドは、this や引数を指定した値で束縛し、実行するためのもの。

function f() {
  console.log('this:', this);
  console.log('Hello ' + this.name);
}

const taro = {
  name: 'Taro'
}

// bindメソッド
const fb = f.bind(taro);  // 新しい関数を生成
fb(); // 実行

// callメソッド
f.call(taro); // 実行

// applyメソッド
f.apply(taro);  // 実行
call と apply の違いは引数の指定方法

bind メソッドと同様に、call メソッド・apply メソッドでも第2引数に指定した値で関数の引数を束縛することができる。call メソッドと apply メソッドの違いは、その引数の指定方法だけであり、apply メソッドでは引数を配列で指定する。

function g(greet, name) {
  console.log(greet + ' ' + name);
}

g.call(null, 'Hello', 'Taro');  // Hello Taro
g.apply(null, ['Bye', 'Jiro']); // Bye Jiro
apply メソッドの実践的な利用例

データを配列として管理していて、その配列を関数の引数に展開して渡したいときに apply メソッドを利用することができる。

例えば、Math.max メソッドは可変長引数(引数の個数が可変)の関数であり、引数で与えられた数値のうち最大の数を返却する。このような関数に対し、配列を直接渡したいときに apply メソッドを使う。

console.log(Math.max(1, 2, 3, 4, 5)); // 5

const arr = [1, 2, 3, 4, 5];
console.log(Math.max.apply(null, arr)); // 5

ただし、ES6 からはスプレッド演算子(…)を利用できるので、この様な使い方をする機会は少なくなった。

console.log(Math.max(1, 2, 3, 4, 5)); // 5

const arr = [1, 2, 3, 4, 5];
console.log(Math.max.apply(null, arr)); // 5

// スプレッド演算子(...)
console.log(Math.max(...arr));  // 5

アロー関数

アロー関数とは

アロー関数とは、無名関数を記述しやすくした省略記法であり、ES6 で導入されたもの。

// function キーワードによる関数宣言文
function func1(name) {
  return 'Hello ' + name;
}
console.log(func1('Taro')); // Hello Taro

// 無名関数による関数式
const func2 = function(name) {
  return 'Hello ' + name;
}
console.log(func2('Taro')); // Hello Taro

// アロー関数による関数式
const func3 = (name) => {
  return 'Hello ' + name;
}

const func4 = name => { // 引数が1つのときは丸括弧を省略できる
  return  'Hello ' + name;
}

const func5 = name => 'Hello ' + name;
  // 処理本体が単文であれば、ブロックの波括弧を省略でき、
  // また、文の戻り値がそのまま関数の戻り値と見なされるため return を省略できる

console.log(func3('Taro')); // Hello Taro
console.log(func4('Taro')); // Hello Taro
console.log(func5('Taro')); // Hello Taro
無名関数とアロー関数の違い

無名関数とアロー関数には次のような挙動の異なる点が存在する。

keyword無名関数アロー関数
this・単に関数として実行されるときはグローバルオブジェクトを参照する。
・オブジェクトのメソッドとして実行されるときはそのオブジェクトを参照する。
this キーワードが存在しないため、スコープチェーンを辿って、レキシカルスコープ(外部スコープ)の this が参照される。
arguments生成されるarguments が生成されないため、スコープチェーンを辿って、レキシカルスコープ(外部スコープ)の arguments が参照される。
new使用できる初期化できない
prototype使用できる使用できない
アロー関数における this

アロー関数では、this キーワードが存在しないため、スコープチェーンを辿ってレキシカルスコープ(外部スコープ)に this を探しに行く。

// 無名関数
const nameless = function() {
  console.log(this);
}

// アロー関数
const arrow = () => console.log(this);

const obj = {
  prop: 'Hello World',
  nameless, // nameless: nameless の省略記法
  arrow,     // arrow: arrow の省略記法
  func: function() {
    const arrow2 = () => console.log(this);
    arrow2();
  }
}

obj.nameless(); // {prop: "Hello World", nameless: ƒ, arrow: ƒ, func: ƒ}
obj.arrow();    // Window
obj.func();     // {prop: "Hello World", nameless: ƒ, arrow: ƒ, func: ƒ}

このサンプルの場合、特に最後の obj.func() の実行では、func が obj のメソッドとして実行されているため、そのときの func の実行コンテキストにおける this は obj を参照することになる。したがって、アロー関数 arrow2 が参照するレキシカルスコープの this は obj ということになる。

コンストラクタ関数とオブジェクト

コンストラクタ関数とは

オブジェクトとは「名前(プロパティ)と値(バリュー)をペアで管理する入れ物」であり、オブジェクトリテラル構文を用いて次のように作成できた。

const person = {
  firstName: 'Taro',
  lastName: 'Yamada',
  hello: function() {
    console.log('Hello');
  }
}

一方で、オブジェクトリテラル構文を用いないでオブジェクトを作成する方法も存在し、その一つとしてコンストラクタ関数(Constructor Function)を使用する方法がある。

コンストラクタ関数とは「オブジェクトを作成するための雛形」となる関数であり、実際にオブジェクトを生成する際には、new 演算子とともに使用する。

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

const taro = new Person('Taro', 'Yamada');
コンストラクタ関数によるオブジェクトの生成

コンストラクタ関数を定義する際は、普通の関数と区別するために、先頭の文字を大文字にして定義するのが慣習となっている。

また、このように new 演算子を用いてコンストラクタ関数からオブジェクトを生成することを「インスタンス化」と呼び、生成されたオブジェクトは「インスタンス」あるいは「インスタンスオブジェクト」と呼ばれる。

オブジェクトリテラル構文は、オブジェクトを直接記述するためのものであり、1つのオブジェクトが生成されるだけだが、コンストラクタ関数を用いると、それを雛形とした同じ形式のオブジェクトを簡単に何個でも作成することができる。

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

const taro = new Person('Taro', 'Yamada');
const jiro = new Person('Jiro', 'Yamada');
const hanako = new Person('Hanako', 'Yamada');
コンストラクタ関数によるオブジェクトの生成(複数個)

このように、テンプレート(雛形)を定めて、それに基づいてオブジェクトを作成したい場合にコンストラクタ関数を使用することができる。

prototype

関数オブジェクトの prototype プロパティ

関数もオブジェクトなのでプロパティを持てるわけだが、関数には prototype というとても重要なプロパティが存在している。

ECMAScript の仕様でも、次のように表現されている。

A prototype property is automatically created for every function, to allow for the possibility that the function will be used as a constructor.(関数がコンストラクターとして使用される可能性を考慮して、プロトタイププロパティがすべての関数に対して自動的に作成されます。)

13.2 Creating Function Objects

ブラウザの開発者ツールのコンソールで任意の関数を定義し、「関数名.prototype」でアクセスすると、下の画像のようにオブジェクトが表示される。

関数オブジェクトには prototype プロパティが存在する

関数が持つ prototype プロパティの値であるオブジェクトのことを prototype オブジェクトと呼ぶ。

オブジェクトの [[Prototype]] 内部プロパティ

すべてのオブジェクトには、[[Prototype]] という内部プロパティが存在している。ただし、この [[Prototype]] という表記は ECMAScript の仕様上のものであり、また、この内部プロパティには直接アクセスすることはできない。

All objects have an internal property called [[Prototype]]. The value of this property is either null or an object and is used for implementing inheritance.(すべてのオブジェクトには、[[Prototype]]という内部プロパティがあります。このプロパティの値はnullまたはオブジェクトのいずれかであり、継承を実装するために使用されます。)

8.6.2 Object Internal Properties and Methods

[[Prototype]] 内部プロパティは直接アクセスすることはできないが、Object.getPrototypeOf メソッドを使用すれば取得することはできる。引数にオブジェクトを渡して使用する。

When the getPrototypeOf function is called with argument O, the following steps are taken:
1. If Type(O) is not Object throw a TypeError exception.
2. Return the value of the [[Prototype]] internal property of O.

getPrototypeOf 関数が引数 O で呼び出されると、次の手順が実行されます。
1. Type(O)が Object でない場合、TypeError 例外をスローします。
2. Oの [[Prototype]] 内部プロパティの値を返します。

15.2.3.2 Object.getPrototypeOf ( O )

Object.getPrototypeOf メソッド
[[Prototype]] は __proto__ プロパティからもアクセスできる

オブジェクトの [[Prototype]] 内部プロパティには直接アクセスできないため、専用の Object.getPrototypeOf メソッドを使用するが、非推奨だが __proto__ プロパティからもアクセスすることができる。(MDN の「Object.prototype.__proto__」を参照)

Chrome の開発者ツールでは、オブジェクトの [[Prototype]] 内部プロパティを __proto__ で表示している。

__proto__ は [[Prototype]] 内部プロパティを表す

次のように、Object.getPrototypeOf メソッドと __proto__ プロパティが同じものを参照していることを確認できる。

__proto__ と Object.getPrototypeOf は同じものを参照する
インスタンス化の際に prototype が [[Prototype]] に設定されている

コンストラクタ関数から new 演算子でインスタンスを生成するとき、コンストラクタ関数のもつ prototype プロパティがインスタンスの [[Prototype]] 内部プロパティとして設定される。

つまりコンストラクタ関数から生成されたインスタンスは、[[Prototype]] 内部プロパティとして、コンストラクタ関数の prototype オブジェクトを共有することになる。

この事実は、Object.getPrototypeOf メソッドにより取得されるインスタンスの [[Prototype]] 内部プロパティと、コンストラクタ関数の prototype の厳密な等価性を検証すると、true が返されることから確認することができる。

function Person(name) {
  this.name = name;
}

const taro = new Person('Taro');

console.log(Person.prototype === Object.getPrototypeOf(taro));  // true
prototype プロパティの暗黙的な参照

インスタンス内のプロパティを参照するとき、インスタンス内にプロパティが見つからなければ、暗黙的に [[Prototype]] 内部プロパティが参照される。つまり、コンストラクタ関数の prototype オブジェクトが参照される。

When a constructor creates an object, that object implicitly references the constructor’s “prototype” property for the purpose of resolving property references.(コンストラクターがオブジェクトを作成すると、そのオブジェクトは、プロパティ参照を解決する目的で、コンストラクターの「プロトタイプ」プロパティを暗黙的に参照します。)

4.3.5 prototype

この仕組みにより、インスタンスはそれを生成したコンストラクタ関数の prototype オブジェクトのメンバーを共有し、あたかも自身のメンバーであるかのようにアクセスすることができる。

function Person(name) {
  this.name = name;
}

const taro = new Person('Taro');

// prototype オブジェクトに hello メソッドを追加
Person.prototype.hello = function() {
  console.log('Hello ' + this.name);
}

console.log(Person.prototype);  // {hello: ƒ, constructor: ƒ}

// インスタンスはコンストラクタ関数の prototype オブジェクトを
// 内部的に共有しており、暗黙的に参照することができる。
// インスタンス taro から hello メソッドにアクセスしてみる
taro.hello(); // Hello Taro
メモリ消費の観点から、メソッドは prototype に定義する

コンストラクタ関数の中でもメソッドを定義することもできるが、インスタンスが生成される度にメソッドの定義がコピーされることになり、メモリを無駄に消費する。

このため、インスタンスに共通のメソッドは、コンストラクタ関数の prototype オブジェクトに定義する。

function Person(name) {
  this.name = name;
  this.hello2 = function() {
    console.log('Hello ' + this.name);
  }
}

Person.prototype.hello = function() {
  console.log('Hello ' + this.name);
}

const taro = new Person('Taro');
const jiro = new Person('Jiro');

taro.hello2();   // Hello Taro
jiro.hello2();   // Hello Jiro

// taro.hello2 と jiro.hello2 は別物
console.log(taro.hello2 === jiro.hello2); // false

taro.hello();   // Hello Taro
jiro.hello();   // Hello Jiro

// taro.hello と jiro.hello は同一の実体
console.log(taro.hello === jiro.hello); // true
Object.create() によるオブジェクトの生成

Object.creat メソッドを使用すると、[[Prototype]] 内部プロパティを自由に設定したオブジェクトを生成できる。第1引数に [[Prototype]] 内部プロパティに設定したいオブジェクト、第2引数に生成するオブジェクトに追加するメンバーを指定する。(参考:ECMAScript の仕様

const protoObj = {
  apple: 'apple',
  orange: 'orange',
  banana: 'banana'
}

const obj = Object.create(protoObj, {
  red: {value: 'red'},
  blue: {value: 'blue'},
  green: {value: 'green'}
})

console.log(obj);
Object.create メソッドによるオブジェクトの生成

オブジェクトは、[[Prototype]] 内部プロパティに設定されているオブジェクトを暗黙的に参照することから、上記サンプルでは red, blue, green は明示的なメンバーであり、protoObj の apple, orange, banana は暗黙的に参照されるメンバーとなる。

明示的なメンバーと暗黙的なメンバーはどちらも参照できる

new 演算子

new 演算子とは

コンストラクタ関数からインスタンスを生成するために使用する演算子のこと。コンストラクタ関数内での return の記述次第で、次のような結果を与える。

  • コンストラクタ関数 F の戻り値が何らかのオブジェクトである場合は、new 演算子による new F(…) はそのオブジェクトを返す。
  • コンストラクタ関数 F の戻り値がオブジェクト以外の場合は(return が記述されていなくて undefined が返却される場合も含む)、new 演算子による new F(…) はコンストラクタ関数内の this を返す。

実際に次のような簡単なコードでも、戻り値がオブジェクトかどうかで振る舞いが異なることを確認できる。

function Person(name) {
  this.name = name;
  // return 文を省略(戻り値は undefined になる)
}

const taro = new Person('Taro');
console.log(taro);  // Person {name: "Taro"}

// Person.prototype オブジェクトにプロパティ・メソッドを追加
Person.prototype.greeting = 'Hello';
Person.prototype.hello = function() {
  console.log(this.greeting + ' ' + this.name);
}

console.log(taro.name);     // Taro
console.log(taro.greeting); // Hello
taro.hello();               // Hello Taro
function Person(name) {
  this.name = name;
  return 100;  // 数値(プリミティブ型)を return
}

const taro = new Person('Taro');
console.log(taro);  // Person {name: "Taro"}

// Person.prototype オブジェクトにプロパティ・メソッドを追加
Person.prototype.greeting = 'Hello';
Person.prototype.hello = function() {
  console.log(this.greeting + ' ' + this.name);
}

console.log(taro.name);     // Taro
console.log(taro.greeting); // Hello
taro.hello();               // Hello Taro
function Person(name) {
  this.name = name;
  return {hoge: 'hoge'};  // オブジェクトを return
}

const taro = new Person('Taro');
console.log(taro);  // {hoge: "hoge"}

// Person.prototype オブジェクトにプロパティ・メソッドを追加
Person.prototype.greeting = 'Hello';
Person.prototype.hello = function() {
  console.log(this.greeting + ' ' + this.name);
}

console.log(taro.name);     // undefined
console.log(taro.greeting); // undefined
taro.hello();               // Uncaught TypeError: taro.hello is not a function

このように戻り値がオブジェクトでないときは、this に格納したメンバーおよびコンストラクタ関数の prototype オブジェクトに格納したメンバーにアクセスできるが、戻り値がオブジェクトのときは、そのオブジェクトそのものが new の結果になり、this や prototype に格納したメンバーにはアクセスできない。

new 演算子の処理の詳細

new 演算子の処理をもう少し詳しく述べると、次のようなものになる。(参考:ECMAScript の 13.2.2 [[Construct]]、MDN の「new 演算子」)

以下は、関数 F についてのコード new F(…) が実行された場合の処理を表す。

  1. 空のまっさらなオブジェクト(以下、obj で表す)が生成される。
  2. obj に内部メソッドや内部プロパティを設定する。
  3. 関数 F の prototype オブジェクト(F.prototype)を、obj の [[Prototype]] 内部プロパティに設定する。ただし、F.prototype がオブジェクトでない(つまりプリミティブ型である)ときは、Object.prototype を [[Prototype]] 内部プロパティに設定する。
  4. 関数 F の実行 F(…) が行われる。このとき、関数 F の処理内の this キーワードには obj が設定されてから実行される。
  5. 関数 F を実行した結果、戻り値がオブジェクトなら、そのオブジェクトが new F(…) の結果として返される。それ以外(つまり、関数 F の戻り値がプリミティブ型)の場合は、obj(つまり関数 F の処理内の this)が new F(…) の結果として返される。

もっとシンプルに表現すれば、次のようになる。

  • 空のオブジェクト obj を生成する。
  • このオブジェクト obj の暗黙的な参照先である [[Prototype]] 内部プロパティに F.prototype あるいは Object.prototype を設定する。
  • コンストラクタ関数 F を(this を obj で束縛して)実行して、このオブジェクト obj にメンバーを設定させる。
  • コンストラクタ関数 F の戻り値がオブジェクトか否かで、コンストラクタ関数 F の戻り値または obj を返却する。

一般的に、コンストラクタ関数には return 文を書かない(つまり戻り値がプリミティブ型の undefined になる)ので、コンストラクタ関数 F から new F(…) により生成されるインスタンスオブジェクトは以下の性質を持つことになる。

  • コンストラクタ関数 F の処理内の this に追加したメンバーを持つ。
  • 暗黙的な参照により、コンストラクタ関数 F の prototype オブジェクト(F.prototype)のメンバーを共有する。
new 演算子を自作してみる

new 演算子についての理解を深めるために、new 演算子と同様の処理を行う関数を自作してみる。

これから作成する予定の new 演算子を模した関数の名称は neww とする。また、この関数は第1引数 constructor でコンストラクタ関数を受け取り、第2引数 args で任意個数の引数を配列として受け取ることにする。(ちなみに次のコードの …args は ES6 から導入された、任意個数の引数を配列で受け取るための構文を使用している。MDN を参照のこと)

function neww(constructor, ...args) {
  // これからこの部分を作っていく。
}

つまり、コンストラクタ関数 Person によるインスタンス化

const instance = new Person('Taro', 'Japapnese', 30);

のような記述を、neww 関数を使って次のように書けるようにすることを目標としている。

const instance = neww(Person, 'Taro', 'Japapnese', 30);

まず、「new 演算子の処理の詳細」の処理1~3までを Object.create メソッドを使って実装する。

function neww(constructor, ...args) {
  // 「new 演算子の処理の詳細」の処理1~3を実装
  const cproto = constructor.prototype;
  const proto = typeof cproto === "object" && cproto !== null ?
    cproto : Object.prototype;
  const obj = Object.create(proto);

  // 「new 演算子の処理の詳細」の処理4以降(未実装)
}

次に「new 演算子の処理の詳細」の処理4を実装する。

渡されたコンストラクタ関数 constructor の内部で使用する this キーワードに、生成したオブジェクト obj を指定してから関数を実行する。関数内部で使用する this キーワードを指定して、その関数を実行するメソッドとして call メソッドと apply メソッドがある。

今回の場合は、配列 args で引数を管理しているから、実行する関数に配列で引数を渡せる apply メソッドが相応しい。

function neww(constructor, ...args) {
  // 「new 演算子の処理の詳細」の処理1~3を実装
  const cproto = constructor.prototype;
  const proto = typeof cproto === "object" && cproto !== null ?
    cproto : Object.prototype;
  const obj = Object.create(proto);

  // 「new 演算子の処理の詳細」の処理4を実装
  const result = constructor.apply(obj, args);

  // 「new 演算子の処理の詳細」の処理5(未実装)
}

最後に「new 演算子の処理の詳細」の処理5を実装する。

関数 constructor の実行による戻り値を確認し、戻り値がオブジェクトならそのオブジェクトをそのまま neww の戻り値として採用する。戻り値がオブジェクトではないならば、obj を neww の戻り値として採用する。このときの obj は、関数 constructor の実行により何らかのプロパティやメソッドが追加されていて、かつ、暗黙的に参照される [[Prototype]] 内部プロパティには constructor.prototype(あるいは Object.prototype)が設定されている。つまり new 演算子の結果として相応しいオブジェクトとなっている。

function neww(constructor, ...args) {
  // 「new 演算子の処理の詳細」の処理1~3を実装
  const cproto = constructor.prototype;
  const proto = typeof cproto === "object" && cproto !== null ?
    cproto : Object.prototype;
  const obj = Object.create(proto);

  // 「new 演算子の処理の詳細」の処理4を実装
  const result = constructor.apply(obj, args);

  // 「new 演算子の処理の詳細」の処理5を実装
  return typeof result === "object" && result !== null ?
    result : obj;
}

ちなみに上記のコードにおいて、オブジェクトかどうかを typeof 演算子で確認するときに、プリミティブ型の null だけは “object” が返されてしまうため、null との比較を行なっている。

typeof null は "object" を返すため注意が必要

以上で new 演算子の処理を模倣した neww 関数の実装が完了した。上記実装を非推奨の __proto__ プロパティを使えば、より理解しやすいコードになる。

function neww(constructor, ...args) {
  // 空のオブジェクトを生成する。
  const obj = {};

  // このオブジェクトの暗黙的な参照先 __proto__を設定する。
  const cproto = constructor.prototype;
  const proto = typeof cproto === "object" && cproto !== null ?
    cproto : Object.prototype;
  obj.__proto__ = cproto;

  // コンストラクタ関数を実行して、このオブジェクトにメンバーを設定させる。
  // そのためには、コンストラクタ関数内の this を obj で束縛する。
  const result = constructor.apply(obj, args);

  // コンストラクタの戻り値がオブジェクトか否かで
  // return する値を分ける。
  return typeof result === "object" && result !== null ?
    result: obj;
}

instanceof 演算子

オブジェクト instanceof コンストラクタ」の形式で使用し、オブジェクトがコンストラクタから生成されたインスタンスであるかどうかを判定する。

オブジェクト obj がコンストラクタ関数 Cから生成されるときは、オブジェクト obj の [[Prototype]] 内部プロパティ ob.__proto__ にコンストラクタ関数 C の prototype オブジェクト C.prototype が設定される。(ただし、関数 C が何らかのオブジェクトを return するときは当てはまらない)

つまり「obj instanceof C」という演算は、「obj.__proto__ === C.prototype」という比較演算に置き換えることができる。

実際には、instanceof 演算子による「obj instanceof C」という演算では、オブジェクトの暗黙的な参照の連鎖(プロトタイプチェーン)を辿って、C.prototype と等しいものがあるかどうかを判定する。

obj.__proto__ === C.prototype
obj.__proto__.__proto__ === C.prototype
obj.__proto__.__proto__.__proto__ === C.prototype
...

サンプル1

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const taro = new Person('Taro', 32);

console.log(taro instanceof Person);  // true
console.log(taro.__proto__ === Person.prototype); // true

console.log(taro instanceof Object);  // true
console.log(taro.__proto__ === Object.prototype); // false
console.log(taro.__proto__.__proto__ === Object.prototype); // true

サンプル2

const obj = {a: 1, b:'apple'};

// このオブジェクトリテラル {a: 1, b:'apple'} は
// const obj = new Object();
// obj.a = 1;
// obj.b = 'apple';
// と同じなので、つまりコンストラクタ関数 Object から生成されている。
console.log(obj instanceof Object); // true
console.log(obj.__proto__ === Object.prototype);  // true

Function コンストラクタ

Function コンストラクタとは

JavaScript には関数を生成できる Function コンストラクタが用意されている。

Function コンストラクタを利用して関数 f を定義(生成)するには、次のような構文に従う。

const f = new Function(arg1, arg2, ... , functionBody); 

最後の引数 functionBody で関数の本体を指定する。またすべての引数は文字列で指定する。

const f = new Function('a', 'b', 'return a + b;');

console.log(f(1, 2));       // 3
console.log(f(10.3, 4.2));  // 14.5
関数の __proto__ は Function.prototype

次のサンプルで分かるように、関数宣言文で定義した関数の __proto__は Function.prototype と一致する。

function fn(a, b) {
  return a + b;
}

console.log(fn instanceof Function);  // true
console.log(fn.__proto__ === Function.prototype); // true

ただし、関数宣言文で定義した関数も Function コンストラクタで生成されている、とまでは言い切れない。関数宣言文による関数の定義と、Function コンストラクタによる関数の生成では、振る舞いの異なる部分が存在するためだ。

ECMAScript(13 Function Definition15.3.2.1 new Function (p1, p2, … , pn, body))によると、関数宣言文による関数の定義(生成)と Function コンストラクタによる関数の生成で、同じ処理(13.2 Creating Function Objects)を行なっているが生成される関数のスコープの設定が異なっている。

new Function(…) はグローバルスコープの関数を生成

new Function(…) により生成された関数はグローバルスコープを持つため、関数の処理本体の記述で外部変数が使われていた場合は、スクリプトスコープかグローバルスコープに変数を探しに行くことになる。(参考:ECMAScript

const a = 100;

function g() {
  const a = 1;
  const f = new Function('return a;');
  console.log(f());
}

g();  // 100

プロトタイプチェーン

オブジェクトには [[Prototype]] という、直接的にはアクセスできない内部プロパティが存在していて、その値は null またはオブジェクトのいずれかだった。(オブジェクトの [[Prototype]] 内部プロパティ

そしてこの内部プロパティにアクセスするには、Object.getPrototypeOf メソッドを使用したり、あるいは非推奨だが __proto__ プロパティを使用するのだった。

オブジェクトがプロパティ名を指定してアクセスされたとき、この [[Prototype]] 内部プロパティは、とても重要な働きをする。

オブジェクト obj がプロパティ名 p でアクセスされた場合、次のような手順でプロパティの検索が行われる。

  1. オブジェクト obj の直接のメンバーの中にプロパティ名 p を探す。見つかったらその値を返す。
  2. 見つからなかった場合は、[[Prototype]] 内部プロパティが参照される。[[Prototype]] 内部プロパティの値が null だったら、見つからなかったという結果(undefined)を返す。
  3. [[Prototype]] 内部プロパティの値がオブジェクトだった場合は、そのオブジェクトのメンバーが検索される。そこでプロパティ名 p が見つかればその値を返す。
  4. 見つからなかった場合は、さらにそのオブジェクトの [[Prototype]] 内部プロパティが参照される。…

というような [[Prototype]] 内部プロパティの参照が繰り返される。つまり、「obj.p」でオブジェクト obj のプロパティ p を参照した場合、

  1. obj の直接のメンバーが検索される。
  2. obj.__proto__ の直接のメンバーが検索される。
  3. obj.__proto__.__proto__ の直接のメンバーが検索される。

というように、__proto__(つまり [[Prototype]] 内部プロパティ)に設定されているオブジェクトが階層を深く降りていくように検索される。このような [[Prototype]] 内部プロパティによる連鎖をプロトタイプチェーンと呼ぶ。

これにより、JavaScript のオブジェクトは、自身の直接のメンバーを持つ他に、プロタイプチェーンで繋がれたオブジェクトのメンバーを暗黙的に共有していることになる。

function Person(name) {
  this.name = name;
  this.hello1 = function() {
    console.log('Hello1 ' + this.name);
  }
}

Person.prototype.hello2 = function() {
  console.log('Hello2 ' + this.name);
}
Object.prototype.hello3 = function() {
  console.log('Hello3 ' + this.name);
}

const taro = new Person('Taro');

// インスタンス taro の __proto__ は Person.prototype
console.log(taro.__proto__ === Person.prototype); //true

// taro.__proto__ の __proto__ は Object.prototype
console.log(taro.__proto__.proto__ === Person.prototype._proto__);  // true
console.log(Person.prototype.__proto__ === Object.prototype); // true

// taro の直接のメンバーにアクセス
taro.hello1();  // Hello1 Taro

// taro の __proto__ の直接のメンバーにアクセス
taro.hello2();            // Hello2 Taro
taro.__proto__.hello2();  // Hello2 undefined

// taro.__proto__ の __proto__ の直接のメンバーにアクセス
taro.hello3();                      // Hello3 Taro
taro.__proto__.hello3();            // Hello3 undefined
taro.__proto__.__proto__.hello3();  // Hello3 undefined
プロトタイプチェーン

hasOwnProperty メソッド と in 演算子

Object.prototype が持っている hasOwnProperty メソッドを使えば、オブジェクトが指定したプロパティを直接のメンバーとして持っているかどうかを判定することができる。

一方で、in 演算子を使えば、プロトタイプチェーンに存在するプロパティも含めて、オブジェクトが指定したプロパティを持っているかどうかを判定することができる。

つまり、これら2つの違いは、プロトタイプチェーンをさかのぼってチェックするかどうかという点にある。

function Person(name) {
  this.name = name;
}

Person.prototype.hello = function() {
  console.log('Hello ' + this.name);
}

const taro = new Person('Taro');
hasOwnProperty メソッド と in 演算子

プロトタイプ継承

コンストラクタ関数の prototype を利用して、機能を流用することをプロトタイプ継承という。

[[Prototype]] 内部プロパティによる継承

例えば、次のようなプロパティ・メソッドを持つオブジェクト greets を考える。

const greets = {
  name: 'anonymous',
  hello: function() {
    console.log('Hello ' + this.name);
  },
  bye: function() {
    console.log('Bye ' + this.name);
  }
}

このオブジェクト greets のプロパティ・メソッドを他のオブジェクト obj でも使えるようにする(継承する)には、次のように [[Prototype]] 内部プロパティへの暗黙的な参照を利用できる。([[Prototype]] 内部プロパティへのアクセスに __proto__ を利用した)

const greets = {
  name: 'anonymous',
  hello: function() {
    console.log('Hello ' + this.name);
  },
  bye: function() {
    console.log('Bye ' + this.name);
  }
}

const obj = {};

obj.__proto__ = greets;
console.log(obj.name);  // anonymous
obj.hello();  // Hello anonymous
obj.bye();    // Bye anonymous

// name プロパティを obj のメンバーとして定義
obj.name = 'Taro';
console.log(obj.name);  // Taro
obj.hello();  // Hello Taro
obj.bye();    // Bye Taro
コンストラクタ関数の prototype による継承

次に上記 greets オブジェクトを、次のようなコンストラクタ関数 Person から生成されるインスタンスでも使えるようにする(継承する)ことを考える。インスタンスの [[Prototype]] 内部プロパティへの参照 __proto__ は、コンストラクタ関数の prototype プロパティの値と一致することを利用する。

const greets = {
  name: 'anonymous',
  hello: function() {
    console.log('Hello ' + this.name);
  },
  bye: function() {
    console.log('Bye ' + this.name);
  }
}

function Person(name) {
  this.name = name;
}

Person.prototype = greets;

const taro = new Person('Taro');
console.log(taro.name);  // Taro
taro.hello();  // Hello Taro
taro.bye();    // Bye Taro

// ただし、この prototype の変更方法だと、
// その後の Person.prototype への変更が
// greets にも影響してしまう。
Person.prototype.getOrigin = function() {
  console.log('Person');
}

taro.getOrigin(); // Person
greets.getOrigin(); // Person

上記コードの最後の部分のコメントでも示してあるように、このコードの prototype の変更方法には問題がある。greets ととは別に prototype にメソッドを追加しようとすると、Person.prototype と greets は同一のオブジェクトであるため、greets にも追加されてしまう。

greets を継承するためには、[[Prototyep]] の暗黙的な参照によるプロトタイプチェーンに greets を繋げばよいので、Person.prototype にそのまま設定する必要は無い。

したがって、上記の問題を回避するために、Person.prototype に greets をそのまま設定するのではなく、greets を継承している([[Prototype]] として暗黙的に参照している)空のオブジェクトを生成して、それを設定する方法をとる。

Object.create メソッドを利用すれば、[[Prototype]] に greets を設定した空のオブジェクトを生成できる。

const greets = {
  name: 'anonymous',
  hello: function() {
    console.log('Hello ' + this.name);
  },
  bye: function() {
    console.log('Bye ' + this.name);
  }
}

function Person(name) {
  this.name = name;
}

// Person.prototype = greets;
Person.prototype = Object.create(greets);

const taro = new Person('Taro');
console.log(taro.name);  // Taro
taro.hello();  // Hello Taro
taro.bye();    // Bye Taro

Person.prototype.getOrigin = function() {
  console.log('Person');
}

taro.getOrigin(); // Person
greets.getOrigin(); // Uncaught TypeError: greets.getOrigin is not a function
コンストラクタ関数の prototype による継承

コンストラクタ関数の継承

あるコンストラクタ関数が別のコンストラクタ関数の機能を受け継ぐ(継承する)方法について考える。

例えば、次のようなコンストラクタ関数 Person を定義する。

function Person(name) {
  this.name = name;
}

Person.prototype.hello = function() {
  console.log('Hello ' + this.name);
}

このコンストラクタ関数 Person を継承して、別のコンストラクタ関数 Person2 を作りたい。Person2 は Person を受け継いで name プロパティ・hello メソッドを持つだけでなく、さらに age プロパティと profile メソッドを持つようにしたい。

function Person2(name, age) {
  // これから実装する
}

Person2.prototype.profile = function() {
  console.log(`name: ${this.name}, age: ${this.age}`);
}

// 以下が実現できるように実装したい
const taro = new Person2('Taro', 28);
taro.hello();   // Hello Taro
taro.profile(); // name: Taro, age: 28
Prototype を継承する

まず、Person の prototype オブジェクトである Person.prototype を受け継ぐところから実装する。

これは、新しく作成する Person2 のプロトタイプチェーンに Person.prototype を繋げばよいから、「コンストラクタ関数の prototype による継承」で述べた方法が使える。つまり、Object.create メソッドを使用して、継承させたい Person.prototype を暗黙的に参照する空のオブジェクトを作成し、これを Person2.prototype に設定する。

このようにすることで、Person.prototype に影響を与えることなく Person2.prototype に新しいメソッド profile を追加できるし、Person.prototype の持つ hello メソッドをオバーライド(上書き)することができる。

/************************
 * Person
 ************************/
function Person(name) {
  this.name = name;
}

Person.prototype.hello = function() {
  console.log('Hello ' + this.name);
}

/************************
 * Person2
 ************************/
function Person2(name, age) {
  // これから実装する
}

// Person.prototype を暗黙的に参照する空のオブジェクトを設定
Person2.prototype = Object.create(Person.prototype);

// Person.prototype に影響を与えることなく
// profile メソッドを追加できる。
Person2.prototype.profile = function() {
  console.log(`name: ${this.name}, age: ${this.age}`);
}
// Person.prototype に影響を与えることなく
// hello メソッドを上書きできる。
Person2.prototype.hello = function() {
  console.log(`Hello! My name is ${this.name}.`);
}

const taro = new Person('Taro');
const taro2 = new Person2('Taro', 28);

taro.hello();     // Hello Taro
// taro.profile();  Uncaught TypeError: taro.profile is not a function
taro2.hello();    // Hello! My name is undefined.
taro2.profile();  // name: undefined, age: undefined
プロパティを継承する

次に Person の持つプロパティ name を受け継ぐ方法について考える。

new 演算子の処理の詳細」で見たように、コンストラクタ関数の内部では、this にメンバーを設定しているだけだった。

そのため、新しく作成するコンストラクタ関数 Person2 の処理内では、this を Person に渡して、Person の持つメンバーを設定させればよい。これを実装するには、this を束縛して関数を実行することができる call メソッドを用いる。

また、Person2 で新しく追加するメンバー age については、通常の方法で this に追加すればよい。

/************************
 * Person
 ************************/
function Person(name) {
  this.name = name;
}

Person.prototype.hello = function() {
  console.log('Hello ' + this.name);
}

/************************
 * Person2
 ************************/
function Person2(name, age) {
  Person.call(this, name);
  this.age = age;
}

// Person.prototype を暗黙的に参照する空のオブジェクトを設定
Person2.prototype = Object.create(Person.prototype);

// Person.prototype に影響を与えることなく
// profile メソッドを追加できる。
Person2.prototype.profile = function() {
  console.log(`name: ${this.name}, age: ${this.age}`);
}
// Person.prototype に影響を与えることなく
// hello メソッドを上書きできる。
Person2.prototype.hello = function() {
  console.log(`Hello! My name is ${this.name}.`);
}

const taro = new Person('Taro');
const taro2 = new Person2('Taro', 28);

taro.hello();     // Hello Taro
// taro.profile();  Uncaught TypeError: taro.profile is not a function
taro2.hello();    // Hello! My name is Taro.
taro2.profile();  // name: Taro, age: 28
コンストラクタ関数の継承

クラス(Class)

ES6 から JavaScript にも class 構文が導入されたが、コンストラクタ関数を class 表記できるようにしただけであり、つまり、既にある機能を簡単に書けるようにしただけ(シンタックスシュガー)。本質的に新しいものが導入されたわけではない。

次のコードのコンストラクタ関数 Person とクラス Person2 は、細部に違いはあるが、実質的な意味は同じものを表現している。

/**************************
 * コンストラクタ関数 Person
 **************************/
function Person(name) {
  this.name = name;
}

Person.prototype.hello = function() {
  console.log('Hello ' + this.name);
}

/**************************
 * クラス Person2
 **************************/
class Person2 {
  constructor(name) {
    this.name = name;
  }

  hello() {
    console.log('Hello ' + this.name);
  }
}

const taro = new Person('Taro');
const taro2 = new Person2('Taro');
class はコンストラクタ関数と実質的に同じ

このように、次の対応関係にあることが分かる。

  • コンストラクタ関数の内部の記述は、class の constructor に記述する。
  • コンストラクタ関数の prototype に追加したメソッドは、class { } の中に記述する。

クラス継承

他のクラスのプロパティやメソッドを継承することをクラス継承という。クラス継承を行うには、extends というキーワードを使用する。

コンストラクタ関数の継承」のサンプルを class 構文で書き換えると、次のようになる。

/**************************
 * Person
 **************************/
// function Person(name) {
//   this.name = name;
// }

// Person.prototype.hello = function() {
//   console.log('Hello ' + this.name);
// }

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

  hello() {
    console.log('Hello ' + this.name);
  }
}

/**************************
 * Person2
 **************************/
// function Person2(name, age) {
//   Person.call(this, name);
//   this.age = age;
// }

// // Person.prototype を暗黙的に参照する空のオブジェクトを設定
// Person2.prototype = Object.create(Person.prototype);

// // Person.prototype に影響を与えることなく
// // profile メソッドを追加できる。
// Person2.prototype.profile = function() {
//   console.log(`name: ${this.name}, age: ${this.age}`);
// }
// // Person.prototype に影響を与えることなく
// // hello メソッドを上書きできる。
// Person2.prototype.hello = function() {
//   console.log(`Hello! My name is ${this.name}.`);
// }

class Person2 extends Person {
  constructor(name, age) {
    super(name);  // super で親クラスの constructor を実行
    this.age = age;
  }

  profile() {
    console.log(`name: ${this.name}, age: ${this.age}`);
  }
  hello() {
    console.log(`Hello! My name is ${this.name}.`);
  }
}

const taro = new Person('Taro');
const taro2 = new Person2('Taro', 28);

taro.hello();     // Hello Taro
taro2.hello();    // Hello! My name is Taro.
taro2.profile();  // name: Taro, age: 28
class 継承

このように、extends を使用したクラス継承の場合、prototype の継承は自動で行われるため、「コンストラクタの継承」の際に記述していた次のコードは必要はない。

// Person.prototype を暗黙的に参照する空のオブジェクトを設定
Person2.prototype = Object.create(Person.prototype);

super

super は関数コンテキスト内でのみ使用できる特別なキーワードであり、関数コンテキスト内であっても使用できる条件はかなり限られる。

super を使用すると継承元の関数を呼び出すことができる。使い方は次の2つ。

  • 他のクラスを継承したクラスの constructor の中で、継承元の constructor を呼び出す。
  • 継承元のクラスやオブジェクトのメソッドを呼び出す。
継承元の constructor を呼び出す

クラスの constructor は関数コンテキストとして実行され、継承元クラスの construcotr を super キーワードを使って呼び出すことができる。

class Person {
  constructor(name) {
    this.name = name;
  }
}
class Person2 extends Person {
  constructor(name, age) {
    super(name);  // super で親クラスの constructor を実行
    this.age = age;
  }
}

注意点として、constructor 内で this キーワードが使われる前に super により継承元の constructor を呼び出す必要がある。super による呼び出しの前に this を記述するとエラーになる。

constructor 内で super の前に this を使うとエラー
継承元のクラスのメソッドを呼び出す

super キーワードを使って、継承元のメソッドを呼び出すこともできる。

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

  hello() {
    console.log('Hello ' + this.name);
  }
}

/**************************
 * Person2
 **************************/
class Person2 extends Person {
  constructor(name, age) {
    super(name);  // super で親クラスの constructor を実行
    this.age = age;
  }

  hello() {
    super.hello();  // 親クラスの hello メソッドを呼び出す。
    console.log(`Hello! My name is ${this.name}.`);
  }
}

const taro2 = new Person2('Taro', 28);

taro2.hello();
// Hello Taro
// Hello! My name is Taro.
継承元のオブジェクトのメソッドを呼び出す

[[Prototype]] 内部プロパティによるオブジェクト間の継承でも、継承元のメソッドを呼び出すことができる。

const person = {
  name: 'anonymous',
  profile() {
    console.log(`My name is ${this.name}.`);
  }
}

const taro = {
  name: 'Taro',
  hello() {
    console.log('Hello!');
    super.profile();
  }
};
Object.setPrototypeOf(taro, person);

taro.hello();
// Hello!
// My name is Taro.

ただし、この時の super はオブジェクトリテラル内でしか使用できない。次のように hello メソッドをオブジェクトリテラルの外に出して、後から追加するように記述すると、構文エラーになる。

const person = {
  name: 'anonymous',
  profile() {
    console.log(`My name is ${this.name}.`);
  }
}

const taro = {
  name: 'Taro',
  // hello() {
  //   console.log('Hello!');
  //   super.profile();
  // }
};
Object.setPrototypeOf(taro, person);

taro.hello = function() {
  console.log('Hello!');
  super.profile();  // Uncaught SyntaxError: 'super' keyword unexpected here
}

taro.hello();

ビルトインオブジェクト

ビルトインオブジェクトとは

コード実行前にJavaScriptエンジンによって自動的に生成されるオブジェクトのことを、ビルトインオブジェクト(built-in object)という。組み込みオブジェクトとも呼ばれる。

ビルトインオブジェクトには、次のようなものがある。

  • Object
  • Array
  • String
  • Boolean
  • Number
  • Function
  • Symbol
  • Math
  • Date
  • etc…

これらビルトインオブジェクトは、ブラウザ環境では、グローバルオブジェクトである Window オブジェクトに格納されている。

Window オブジェクトにビルトインオブジェクトが格納されている

名前が大文字で始まるものがビルトインオブジェクトであり、そのほとんどはインスタンスを生成するためのコンストラクタ関数になっている。

ビルトインオブジェクトは大文字で始まる
ビルトインコンストラクタ

特にインスタンスを生成するためのビルトインオブジェクトは、ビルトインコンストラクタと呼ばれる。

例えば、配列を生成するためのビルトインコンストラクタに Array というものがある。

ビルトインコンストラクタ Array

「native code」と表示されているように、JavaScript エンジンによって準備されるビルトインオブジェクトは、C++ などで記述されているので、その中身を確認することはできない。

下の画像から分かるように、Array で生成されたインスタンス、つまり配列もオブジェクトであり、key: value 形式で値が格納されている。

また、__proto__ には Array.prototype が設定されていて、この prototype オブジェクトには、配列を操作するためのメソッドが格納されている。

Array のインスタンス

このように JavaScript エンジンによって自動的に生成されるオブジェクト(ビルトインオブジェクト)であっても、通常のオブジェクトと同様の構造をしている。

配列もオブジェクトである

配列は index を表す数値で arr[0] のようにアクセスするが、これは内部的に arr[“0″] のように文字列でのアクセスに変換されて、”0” というプロパティにアクセスしている。

const arr = [1, 2, 3, 4, 5];

console.log(arr[0]);    // 1
console.log(arr["0"]);  // 1
console.log(arr.hasOwnProperty(0));   // true
console.log(arr.hasOwnProperty("0")); // true

ラッパーオブジェクト

プリミティブ型以外のものは全てオブジェクトとして扱われる。逆にいうと、プリミティブ型の値はオブジェクトではないため、本来はオブジェクトのようなメソッドを持たない。

しかし、JavaScript では、単なる値であるプリミティブ型のデータから、いろいろな操作を行うためのメソッドを付与したラッパーオブジェクトと呼ばれるオブジェクトを生成する仕組みを備えている。

そして、プリミティブ型の値に対し、そのプロパティにアクセスしようとすると、対応するラッパーオブジェクトに暗黙的に変換される。

例えば、ビルトインオブジェクト String を使用すると、文字列のラッパーオブジェクトを生成することができる。

String による文字列のラッパーオブジェクトの生成

上の画像から、[[PrimitiveValue]] という JavaScript エンジンが内部で使用しているプロパティに「”Hello World”」というプリミティブ型の値が格納されていることが分かる。

また、プリミティブ型は immutable(不変)なため、その値を変更することはできないが、ラッパーオブジェクトのプロトタイプ __proto__ には、そのプリミティブ型の値から新たな値を生成するためのメソッドなどが用意されている。

ラッパーオブジェクトの __proto__ にはメソッドが格納されている

プリミティブ型の値から、ラッパーオブジェクトのこれらのメソッドを呼び出すと、暗黙的にラッパーオブジェクトへの変換が行われる。

プリミティブ型の値からメソッドを呼び出すと、ラッパーオブジェクトのメソッドが呼ばれる

Symbol

Symbol とは

シンボル型(symbol)は ES6 から導入された、一意で不変なデータを表すプリミティブ型であり、Symbol() 関数は symbol 型の値を返す関数。つまり、Symbol() 関数は、呼び出す度に異なる一意の値を返す。

Symbol() 関数を使用してシンボル型の値を取得する際には、new 演算子は使用しない。new 演算子を使用するとエラーになる。

const sym = Symbol();
console.log(sym); // Symbol()
console.log(typeof sym); // symbol

const sym2 = Symbol('Hello');
console.log(sym2);  // Symbol(Hello)

const sym3 = new Symbol();
// Uncaught TypeError: Symbol is not a constructor
Symbol() 関数の引数

Symbol() 関数の引数は、取得されるシンボルの意味を説明するために使用される。同じ引数を与えても異なるシンボルが取得されるため、引数に与えた文字列からシンボルを特定することはできない。

const h1 = Symbol('Hello');
const h2 = Symbol('Hello');

console.log(h1 === h2); // false
シンボルはオブジェクトのプロパティとして利用できる

シンボルはオブジェクトのプロパティの識別子として使用できる。ただしシンボルは変数に格納して使用するものなので、シンボルをオブジェクトのプロパティに使用するときは、ブラケット構文で記述する必要がある。

const hello = Symbol('hello');

const obj = {};
obj[hello] = function() {
  console.log('Hello');
}

obj[hello](); // Hello

また、Computed property names でシンボルを使用することもできる。

const sym = Symbol();
const obj = {
  a: 1,
  b: 2,
  c: 3,
  [sym]: 4
}

console.log(obj);
// {
//   a: 1,
//   b: 2,
//   c: 3,
//   Symbol(): 4
// }

オブジェクトのプロパティとディスクリプタ―

ディスクリプタ―とは

オブジェクトの各プロパティは、値を保持する value の他にも configurable, enumerable, writable という設定値を保持している。これらを設定することによって、プロパティのとる挙動を設定することができる。

また、これら設定値のことをディスクリプターと呼ぶ。(上記4つの設定値の他に、set, get というオプションとして設定できる項目が存在する)

設定値概要
valueプロパティに設定された値を保持
configurabletrue の場合、プロパティの削除や設定の変更が可能
enumerabletrue の場合、プロパティはループで列挙される
writabletrue の場合、value の変更が可能
setセッター関数
getゲッター関数
ディスクリプタ―の取得

Object.getOwnPropertyDescriptor メソッドを使用して、オブジェクトの指定したプロパティのディスクリプタ―を取得することができる。

const obj = {a: 'apple', b: 'banana'}

// オブジェクト obj のプロパティ a のディスクリプタを取得
const descriptor = Object.getOwnPropertyDescriptor(obj, 'a');

console.log(descriptor);
ディスクリプタ―を取得

このようにオブジェクトリテラルでオブジェクトを定義した場合、プロパティの value 以外の設定値は true となる。

プロパティの追加・変更

Object.defineProperty メソッドを使用して、オブジェクトにプロパティを追加したり、既存のプロパティを変更することができる。

const obj = {a: 'apple', b: 'banana'}

Object.defineProperty(obj, 'c', {
  value: 'orange'
})

const descriptor = Object.getOwnPropertyDescriptor(obj, 'c');
console.log(obj);
console.log(descriptor);
Object.defineProperty メソッドによるプロパティの追加

このように Object.defineProperty メソッドによりプロパティを定義した場合、設定値のデフォルトは false となる。

setter と getter

getter とは

ゲッター(getter)とは、get 構文を使用して定義するプロパティであり、そのプロパティを読み出そうとした時に、プロパティに結びつけられた関数が実行される。

ゲッターは「読み出し」アクセスをした時に関数が実行されるだけで、それ自体は値を保持しない。

const person = {
  firstName: 'Taro',
  lastName: 'Yamada',
  get fullName() {
    return `${this.lastName} ${this.firstName}`;
  }
}

console.log(person.fullName);     // Yamada Taro
console.log(person['fullName']);  // Yamada Taro

person.fullName = '';
console.log(person.fullName); // Yamada Taro
setter とは

セッター(setter)とは、set 構文を使用して定義するプロパティであり、そのプロパティに値を書き込もうとした時に、プロパティに結びつけられた関数が実行される。

セッターは「書き込み」アクセスした時に関数が実行されるだけで、それ自体は値を保持しない。

const person = {
  firstName: '',
  lastName: '',
  set fullName(str) {
    str = typeof str === "string" ? str : '';
    const arr = str.split(' ');
    this.lastName = arr[0];
    this.firstName = arr.length > 1 ? arr[1] : '';
  }
}

person.fullName = 'Yamada Taro';
console.log(person);  // {firstName: "Taro", lastName: "Yamada"}

console.log(person.fullName); // undefined
Object.defineProperty メソッドによる定義

オブジェクトのプロパティが持つディスクリプタ―には、value, configurable, enumerable, writable の他に setget があり、この2つはオプションとして設定できる。(設定しないときは undefined となる。)

この set と get を使用してゲッターやセッターを定義することもできる。

const person = {
  firstName: '',
  lastName: ''
}

Object.defineProperty(person, 'fullName', {
  get: function() {
    return `${this.lastName} ${this.firstName}`;
  },
  set: function(str) {
    str = typeof str === "string" ? str : '';
    const arr = str.split(' ');
    this.lastName = arr[0];
    this.firstName = arr.length > 1 ? arr[1] : '';
  }
});

person.fullName = 'Yamada Taro';
console.log(person);          // {firstName: "Taro", lastName: "Yamada"}
console.log(person.fullName); // Yamada Taro
コンストラクタ関数における getter, setter

コンストラクタ関数から生成されたインスタンスで、ゲッターやセッターを使用できるようにすることを考える。

ゲッターやセッターはプロパティとしてアクセスできる関数であり、それ自身は値を保持しない。そのため、インスタンス毎にメモリの確保は不要なので、通常のメソッドと同様にコンストラクタ関数の prototype に定義する。

function Person(firstName, lastName) {
  this._firstName = firstName;
  this._lastName = lastName;
}

// Person.prototype に fullName プロパティを
// ゲッターおよびセッターとして追加する。
Object.defineProperty(Person.prototype, 'fullName', {
  get: function() {
    return `${this._lastName} ${this._firstName}`;
  },
  set: function(str) {
    str = typeof str === "string" ? str : '';
    const arr = str.split(' ');
    this._lastName = arr[0];
    this._firstName = arr.length > 1 ? arr[1] : '';
  }
});

const taro = new Person('Taro', 'Yamada');
console.log(taro.fullName); // Yamada Taro

taro.fullName = 'Sato Taro';
console.log(taro.fullName); // Sato Taro
class における getter, setter

class では、get 構文・set 構文を使用してゲッター・セッターを定義することができる。

次のコードのコンストラクタ関数 Person とクラス Person2 は、細部に違いはあるが、実質的な意味は同じものを表現している。

/**************************
 * コンストラクタ関数 Person
 **************************/
function Person(firstName, lastName) {
  this._firstName = firstName;
  this._lastName = lastName;
}

// Person.prototype に fullName プロパティを
// ゲッターおよびセッターとして追加する。
Object.defineProperty(Person.prototype, 'fullName', {
  get: function() {
    return `${this._lastName} ${this._firstName}`;
  },
  set: function(str) {
    str = typeof str === "string" ? str : '';
    const arr = str.split(' ');
    this._lastName = arr[0];
    this._firstName = arr.length > 1 ? arr[1] : '';
  }
});

/**************************
 * クラス Person2
 **************************/
class Person2 {
  constructor(firstName, lastName) {
    this._firstName = firstName;
    this._lastName = lastName;
  }

  get fullName() {
    return `${this._lastName} ${this._firstName}`;
  }
  set fullName(str) {
    str = typeof str === "string" ? str : '';
    const arr = str.split(' ');
    this._lastName = arr[0];
    this._firstName = arr.length > 1 ? arr[1] : '';
  }
}

const taro = new Person('Taro', 'Yamada');
console.log(taro.fullName); // Yamada Taro
taro.fullName = 'Sato Taro';
console.log(taro.fullName); // Sato Taro

const taro2 = new Person2('Taro', 'Yamada');
console.log(taro2.fullName);  // Yamada Taro
taro2.fullName = 'Sato Taro';
console.log(taro2.fullName);  // Sato Taro
class における getter, setter

静的メソッド(static メソッド)

静的メソッド(static メソッド)とは

コンストラクタ関数やクラスからインスタンス化を行わずに使用できるメソッドのことを静的メソッド(static メソッド)と呼ぶ。

インスタンス経由で呼び出すメソッドとは異なり、コンストラクタやクラスから直接呼び出すことができる。また、インスタンスを経由しないため、this キーワードを静的メソッド内で使用しても意味がない。

コンストラクタ関数における静的メソッド

コンストラクタ関数において静的メソッドを定義するには、コンストラクタ関数(関数オブジェクト)に直接メソッドを追加する。

function Person(name) {
  this.name = name;
}

Person.prototype.profile = function() {
  console.log(`My name is ${this.name}.`);
}

// 静的メソッド
Person.hello = function() {
  console.log('Hello World');
}

Person.hello();   // Hello World
Person.profile(); // Uncaught TypeError: Person.profile is not a function

このようにコンストラクタ関数に直接メソッドを追加しても、new 演算子によるインスタンス生成には全く影響を与えない。なぜなら、インスタンス化におけるコンストラクタ関数の役割が、新しく生成されるオブジェクトが暗黙的に prototype オブジェクトを参照するように設定することと、新しく生成されるオブジェクト(this)へメンバーを追加することであるため。

class における静的メソッド

class において静的メソッドを定義するには、メソッド定義の先頭に static キーワードを付与する。

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

  profile() {
    console.log(`My name is ${this.name}.`);
  }

  // 静的メソッド
  static hello() {
    console.log('Hello World');
  }
}

Person.hello();   // Hello World
Person.profile(); // Uncaught TypeError: Person.profile is not a function

チェーンメソッド

オブジェクトのメソッドの戻り値を this にすることで、次のような形でメソッドを繋げて呼び出すことができる。このようなメソッドのことをチェーンメソッドと呼ぶ。

taro.hello(jiro).introduce().shakeHands(jiro).bye(jiro);

一つのオブジェクトに対して、連続した処理を記述する必要がある場合に、チェーンメソッドとして実装しておけば記述の簡略化が図れる。

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

  hello(person) {
    console.log(`Hello ${person.name}!`);
    return this;
  }
  introduce() {
    console.log(`My name is ${this.name}.`);
    return this;
  }
  shakeHands(person) {
    console.log(`${this.name} shake hands with ${person.name}.`);
    return this;
  }
  bye(person) {
    console.log(`Bye ${person.name}!`);
    return this;
  }
}

const taro = new Person('Taro');
const jiro = new Person('Jiro');

taro.hello(jiro);       // Hello Jiro!
taro.introduce();       // My name is Taro.
taro.shakeHands(jiro);  // Taro shake hands with Jiro.
taro.bye(jiro);         // Bye Jiro!

taro.hello(jiro)        // Hello Jiro!
  .introduce()          // My name is Taro.
  .shakeHands(jiro)     // Taro shake hands with Jiro.
  .bye(jiro);           // Bye Jiro!

反復処理

演算子と優先順位

演算子とは

値(オペランド、被演算子という)を元に何らかの処理を行い、結果(戻り値)を返す記号のことを演算子という。

例えば、「1 + 2」という演算では、1 や 2 がオペランドであり、記号 + が演算子となる。そしてこの演算の結果、3 という値が戻り値として返される。

console.log(1 + 2); // 3

他の例として、イコール記号「=」で表される代入演算子を見てみる。

例えば、変数 a に対して 3 を代入する「a = 3」という演算は、a や 3 がオペランドであり、左辺 a に右辺 3 を代入するという処理を行い、かつその結果(戻り値)として 3 を返却する。

let a;
console.log(a = 3); // 3
console.log(a);     // 3

このサンプルからも分かるように、代入演算子はただ代入という処理を行うだけでなく、戻り値を返すという機能も持っている。

演算子の優先順位

演算子は何らかの処理を行い、かつ戻り値を返却するため、さらにその戻り値を使って演算を行うことができる。

例えば、「1 + 2」という演算の戻り値を変数 a に代入することを考える。この場合、次のようなコードでその処理が実現できる。

a = 1 + 2;

ただし、このコードには代入演算子「=」と加算演算子「+」という2つの演算子が存在している。そのため、左側から「a = 1」という代入演算が先に行われ、その戻り値 1 と数値 2 の加算を行うと解釈することもできてしまう。

そのような曖昧さを排除するため、このような1つの式の中に複数の演算子が存在する場合は、演算子の優先順位に基づいて演算が実行されることになっている。(MDN:演算子の優先順位

したがって、先ほどの「a = 1 + 2;」というコードであれば、代入演算子「=」よりも加算演算子「+」の方が優先順位が高いため、「1 + 2」という演算が先に実行され、その戻り値 3 を得た後で変数 a への代入「a = 3」が実行される。そしてこの処理により変数 a に 3 が格納され、戻り値として 3 が返却される。ただしこのコードの場合、最後の戻り値 3 は他で利用されることなく破棄されることになる。

演算子の結合性

演算子の優先順位だけでは、優先順位が等しい演算子が複数存在する場合に問題が生じる。例えば、次のようなコードを考える。

let x = 0, y = 1, z = 2;
x = y = z = 3;

この2行目のコードでは代入演算子「=」が複数存在しているが、優先順位が同じなため、演算の実行順序を決定する助けにはならない。

このような演算子の優先順位が等しい場合のために、演算子には「結合性」というものが定められている。(MDN:演算子の優先順位

演算子の結合性により、演算子の左側・右側のどちらから演算を実行するのかを決定することができる。結合性が「左から右」であれば、左側から演算を実行し、その後で右側の演算が実行される。

上記の「x = y = z = 3;」というコードであれば、代入演算子「=」の結合性は「右から左」なので、右側にある演算が先に実行される。したがって「z = 3」という演算がまず実行され、変数 z に 3 が格納され、戻り値 3 が返却される。そして、この戻り値 3 を用いて「y = 3」という演算が実行され、変数 y に 3 が格納され、戻り値 3 が返却される。最後に、この戻り値 3 を用いて「x = 3」という演算が実行され、変数 x に 3 が格納され、戻り値 3 が返却される。最後の戻り値は利用されることなく破棄される。

インクリメント演算子・デクリメント演算子

演算子はオペランドを元に何らかの処理を行い、戻り値を返すものだった。

演算子の中でもインクリメント演算子とデクリメント演算子は、演算子の記号をオペランドの前に置くか、後に置くかで動きが変わるので注意が必要。

let a = 0;
console.log(++a); // 1
console.log(a);   // 1

a = 0;
console.log(a++); // 0
console.log(a);   // 1

このようにオペランドの前にインクリメント演算子「++」を置いた場合、オペランドの変数を +1 して、かつその値を戻り値として返却する。

一方、オペランドの後にインクリメント演算子「++」を置いた場合、オペランドの変数を +1 するという処理は同じだが、戻り値は +1 する前の値が返却される。(参考:ECMAScript

ループ文とプロックスコープ

ループ文では、1ループ毎にブロックスコープが切り替わる

for(let i = 0; i < 10; i++) {
  const j = i;
  console.log(j);
  
  setTimeout(function() {
    console.log(j * 10);
  }, 1000);
}
ループ文では、1ループ毎にブロックスコープが切り替わる

const は再宣言も再代入もエラーを発生するはずだが、このようにループの中で const 宣言を使用してもエラーが発生しない。つまり、ループ毎にスコープが切り替わっていることになる。

また、setTimeOut により1000ミリ秒後に実行される処理の中でも、各ループ毎に生成される異なるブロックスコープの変数 j の値を参照していることが分かる。

for … in と列挙可能性

列挙可能性とは

列挙可能性とは、オブジェクトの各プロパティが持っているディスクリプターの中に含まれる enumerable という設定値のことを指す。

また、enumerable が true に設定されているプロパティのことを、列挙可能プロパティと呼ぶ。(参考:MDN

for … in 文

for … in 文は、オブジェクトから一つずつプロパティを取り出して反復処理するための構文。

const obj = {
  a: 1,
  b: 2,
  c: 3
}

for (const prop in obj) {
  console.log(prop, obj[prop]);
}

// a 1
// b 2
// c 3

次のような性質を持っている。

  • 列挙可能プロパティのみ列挙する(取り出す)。
  • 列挙する順序は保証されていない。
  • プロトタイプチェーン内も列挙対象となる。
  • Symbol で定義したプロパティは列挙されない。
列挙可能プロパティのみ列挙する

列挙可能プロパティは、for … in 文で列挙されるが、false の場合は列挙されない。

enumerablefor … in 文
true列挙される
false列挙されない
列挙する順序は保証されていない

for … in 文による反復処理においてプロパティが列挙される順番は、基本的にはオブジェクトにプロパティを追加した順番やオブジェクトリテラルでの定義順になるが、それが保証されているわけではない。

つまり以下のサンプルを実行すると、console には a, b, c の順番で出力されるが、この動作は保証されてはいない。

const obj = {
  a: 1,
  b: 2,
  c: 3
}

for (const prop in obj) {
  console.log(prop);
}
  
// a
// b
// c
プロトタイプチェーン内も列挙対象となる

for … in 文では、オブジェクトが暗黙的に参照しているオブジェクトのプロパティ、つまりプロトタイプチェーン内にあるプロパティも列挙の対象となる。

const obj = {
  a: 1,
  b: 2,
  c: 3
}

// obj.__proto__ === Object.prototype
// は true つまり、obj は Object.prototype を暗黙的に参照している。
// この Object.prototype にメソッドを追加してみる。
Object.prototype.hello = function() {
  console.log('Hello');
}
obj.hello();  // Hello

for (const prop in obj) {
  console.log(prop);
}

// a
// b
// c
// hello <-- 列挙された!!

// なお、Object.prototype がデフォルトで持っている他のメソッドなどが列挙されないのは
// その列挙可能性(enumerable)が false のため。
// 例えば、Object.hasOwnProperty() メソッドで確認してみる。
const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, 'hasOwnProperty');
console.log(descriptor);
// {
//   writable: true,
//   enumerable: false,   <--- これが false になっている
//   configurable: true,
//   value: ƒ
// }

// したがって hello() メソッドも enumerable を false にすれば列挙されない。
Object.defineProperty(Object.prototype, 'hello', {
  enumerable: false
});

for (const prop in obj) {
  console.log(prop);
}

// a
// b
// c

もし、オブジェクトが自身で所有しているプロパティのみ列挙したい場合は、Object.prototype.hasOwnProperty() メソッドを使用する。

const obj = {
  a: 1,
  b: 2,
  c: 3
}

Object.prototype.hello = function() {
  console.log('Hello');
}
obj.hello();  // Hello

for (const prop in obj) {
  if (obj.hasOwnProperty(prop)) {
    console.log(prop);
  }
}

// a
// b
// c
Symbol で定義したプロパティは列挙されない

for … in 文は、オブジェクトの Symbol() を使って定義したプロパティは列挙対象としない。そのプロパティの enumerable が true であったとしても列挙されない。

const sym = Symbol();
const obj = {
  a: 1,
  b: 2,
  c: 3,
  [sym]: 4
}

console.log(obj);
// {
//   a: 1,
//   b: 2,
//   c: 3,
//   Symbol(): 4
// }

for (const prop in obj) {
  console.log(prop);
}

// a
// b
// c

const descriptor = Object.getOwnPropertyDescriptor(obj, sym);
console.log(descriptor);
// {
//   value: 4,
//   writable: true,
//   enumerable: true,    <-- true になっているが列挙されない
//   configurable: true
// }

for … of と反復可能性

イテレーター(Iterator)とは

イテレータ―とは、反復処理を行う際に使用するオブジェクトであり、イテレーターを持つオブジェクトとしては、次のものがある。

  • String オブジェクト
  • Array オブジェクト
  • Map オブジェクト
  • Set オブジェクト
  • arguments オブジェクト
  • etc …

これらイテレーターを持つオブジェクトのことを反復可能オブジェクトと呼ぶ。反復可能オブジェクトでは、for … of 文によるループ処理が可能になっている。

より厳密にイテレータを理解するためには、その定義を知る必要がある。

IteratorResult オブジェクト

イテレーターの定義を見る前に、その定義で使用されている IteratorResult オブジェクトについて知っている必要がある。

IteratorResult オブジェクトとは、2つのプロパティ donevalue を含んでいるオブジェクトとして定義されている。(参考:26.1.1.5 The IteratorResult Interface

プロパティ反復処理における意味
donetrue または false反復が完了したか
value任意反復処理に渡される値
// done プロパティ(Boolean 型)と value プロパティ
// を持っていれば、それは IteratorResult オブジェクトと呼べる。

// したがって、次の obj1, obj2 は IteratorResult オブジェクト
const obj1 = {
  done: true,
  value: 100
}

const obj2 = {
  value: 'Hello',
  name: 'Taro',
  age: 32,
  done: false
}
イテレーター(Iterator)の定義

イテレーター(Iterator)は、next メソッドを持っているオブジェクトとして定義される。ただし、next メソッドは IteratorResult オブジェクトを戻り値とする関数でなければならない。(参考:26.1.1.2 The Iterator Interface

プロパティ
nextIteratorResult オブジェクトを返却する関数
// done プロパティ(Boolean 型)と value プロパティ
// を持っていれば、それは IteratorResult オブジェクトと呼べる。

// したがって、次の obj1, obj2 は IteratorResult オブジェクト
const obj1 = {
  done: true,
  value: 100
}
const obj2 = {
  value: 'Hello',
  name: 'Taro',
  age: 32,
  done: false
}

// IteratorResult オブジェクトを返す関数を next プロパティとして
// 持っているオブジェクトは、イテレーター(Iterator)と呼べる。

// したがって、次の以下のオブジェクトはイテレーター
const obj3 = {
  next: function() {
    return {
      done: true,
      value: 'apple'
    }
  }
}
const obj4 = {
  name: 'Taro',
  hello: function() {
    console.log('Hello');
  },
  next: function() {return obj2;}
}
イテレーターを用いた反復処理

イテレーターとは、next メソッドを持つオブジェクトであり、その next メソッドを実行すると done と value という2つの値が取得できる。

このイテレーターを使って反復処理を実装するには、例えば、反復の度に次のような手順を踏めばよい。

  1. イテレーターの next メソッドを実行し、done と value を取得する。
  2. done が true なら反復処理は完了なので、反復処理のループから抜ける。
  3. 取得した value の値を使って目的の処理を行う。
// 反復処理の対象となるオブジェクト
const targetObj = {
  // 反復処理したくなるようなメンバーを持つ
}

// イテレーターを作成
const it = {
  next: function() {
    // 反復を制御するための処理
    // 戻り値は done, value をプロパティに持つオブジェクト
    // done は反復処理が完了したかどうかを表す(Boolean)
    // value は対象オブジェクト(targetObj)から取り出した一つの値
  }
}

while(true) {
  result = it.next();
  if (result.done) break;
  // 対象のオブジェクトに対する処理
}

例えば、配列に対しては次のようなイテレーターを考えることができる。

// 反復処理の対象のオブジェクト(例:配列)
const targetObj = [1, 2, 3, 4, 5];

// イテレーター
const it = {
  index: 0,
  next: function() {
    if (this.index < targetObj.length) {
      return {
        done: false,
        value: targetObj[this.index++]
      }
    } else {
      return {
        done: true,
        // value は省略
      }
    }
  }
}

while(true) {
  const result = it.next();
  if (result.done) break;
  console.log(result.value * 2);
}

// 2
// 4
// 6
// 8
// 10
イテレーターを関数から生成する

イテレーターを他のオブジェクトの反復処理でも使用できるようにするためには、イテレーターに反復処理の対象のオブジェクトを渡す必要がある。つまり、反復対象のオブジェクトを受け取り、そのイテレーターを返却する関数を作成することが必要。

例えば、以前のサンプルは次のように書き換えることができる。また次のサンプルでは、next メソッドの中で使用していた index を戻り値のイテレーターに含めずに、関数のローカル変数に変更した。つまりクロージャーの仕組みを利用した。これは it.index のようにして、生成したイテレーターから index にアクセスできないようにするため。

// 反復処理の対象のオブジェクト(例:配列)
const targetObj1 = [1, 2, 3, 4, 5];
const targetObj2 = [6, 7, 8, 9, 10];

// 対象オブジェクトを受け取り、イテレーターを返却する関数
function generateIterator(obj) {
  let index = 0;  // プライベート変数
  return {
    next: function() {
      if (index < obj.length) {
        return {
          done: false,
          value: obj[index++]
        }
      } else {
        return {
          done: true,
          // value は省略
        }
      }
    }
  }
}

const it1 = generateIterator(targetObj1);
const it2 = generateIterator(targetObj2);

while(true) {
  const result = it1.next();
  if (result.done) break;
  console.log(result.value * 2);
}

while(true) {
  const result = it2.next();
  if (result.done) break;
  console.log(result.value * 2);
}

// 2
// 4
// 6
// 8
// 10
// 12
// 14
// 16
// 18
// 20

これでイテレーターを生成する関数を作成できたが、このサンプルのイテレーターをすべての配列で使用できるようにすることを考えるなら、配列のインスタンスのメソッドとして使えるように、Array.prototype に定義するのが自然だろう。

その場合、対象オブジェクトは this で参照できるので引数は省略できる。

// イテレーターを返却する関数 generateIterator を
// Array.prototype に追加し、全ての配列からメソッドとして呼び出せるようにする。
Array.prototype.generateIterator = function() {
  let index = 0;  // プライベート変数
  const that = this;
  return {
    next: function() {
      if (index < that.length) {
        return {
          done: false,
          value: that[index++]
        }
      } else {
        return {
          done: true,
          // value は省略
        }
      }
    }
  }
}

// 反復処理の対象のオブジェクト(例:配列)
const targetObj1 = [1, 2, 3, 4, 5];
const targetObj2 = [6, 7, 8, 9, 10];

const it1 = targetObj1.generateIterator();
const it2 = targetObj2.generateIterator();

while(true) {
  const result = it1.next();
  if (result.done) break;
  console.log(result.value * 2);
}

while(true) {
  const result = it2.next();
  if (result.done) break;
  console.log(result.value * 2);
}

// 2
// 4
// 6
// 8
// 10
// 12
// 14
// 16
// 18
// 20
イテレーターによる反復処理を関数化してみる

上記のサンプルを見ると、イテレーターを使った while 文による反復処理の部分も、関数化すれば記述が簡潔になる。

例えば、反復対象のオブジェクトが、generatorIterator というイテレーターを生成するメソッドを持っていることを前提とした場合、イテレーターを使用して反復処理を行う関数 loop を次のように実装することができる。

// 反復処理を関数化
// ただし、イテレーターを生成するメソッド generateIterator を
// 渡された反復の対象オブジェクトが持っていることを前提としている。
function loop(obj, callback) {
  const it = obj.generateIterator();
  while(true) {
    const result = it.next();
    if (result.done) break;
    callback(result.value);
  }
}

これを使うと先述のサンプルは、次のように書き換えることができる。

// イテレーターを返却する関数 generateIterator を
// Array.prototype に追加し、全ての配列からメソッドとして呼び出せるようにする。
Array.prototype.generateIterator = function() {
  let index = 0;  // プライベート変数
  const that = this;
  return {
    next: function() {
      if (index < that.length) {
        return {
          done: false,
          value: that[index++]
        }
      } else {
        return {
          done: true,
          // value は省略
        }
      }
    }
  }
}

// 反復処理の対象のオブジェクト(例:配列)
const targetObj1 = [1, 2, 3, 4, 5];
const targetObj2 = [6, 7, 8, 9, 10];

loop(targetObj1, function(val) {
  console.log(val * 2);
})

loop(targetObj2, function(val) {
  console.log(val * 2);
})

// 2
// 4
// 6
// 8
// 10
// 12
// 14
// 16
// 18
// 20

このように、「反復処理したいオブジェクトには、そのイテレーターを生成する generateIterator メソッドを定義する」というルールを定めれば、反復処理は上記 loop 関数で抽象化することができる。

そして実は ES6 からはそれに相当する仕様が存在している。シンボル値 Symbol.iterator(@@iterator メソッド)for … of 構文がそれに当たる。

反復可能(Iterable)オブジェクト

反復可能オブジェクトとは

上記の generateIterator メソッドに相当するのが、ECMAScript の @@iterator メソッドとなっている。そしてこのメソッドを持っているオブジェクトのことを反復可能オブジェクトと呼んでいる。

つまり反復可能オブジェクトとは、イテレーターを戻り値とする関数である @@iterator メソッドを持っているオブジェクトとして定義される。(参考:26.1.1.1 The Iterable Interface

プロパティ
@@iteratorイテレーターを返却する関数

また、この「@@iterator」という名称は ECMAScript の仕様における説明のための名称であり、JavaScript のコードではシンボル値 Symbol.iterator を使用する。(参考:6.1.5.1 Well-Known Symbols

console.log(typeof Symbol.iterator);  // symbol
console.log(Symbol.iterator);         // Symbol(Symbol.iterator)

// つまりこれは、JavaScript エンジンにより
// Symbol.iterator = Symbol('Symbol.iterator');
// のような形で既に Symbol.iterator にシンボル値が
// 格納済みであることを意味する。

このシンボル値 Symbol.iterator を使用して反復可能オブジェクトを作成するには、次のようにブラケット構文を使うか、もしくはオブジェクトリテラルにおける Computed property names を使用する。

// ブラケット構文で @@iterator メソッドを実装する場合
const iterableObj1 = {};
iterableObj1[Symbol.iterator] = function() {
  // イテレーターを返却する
}

// オブジェクトリテラルの Computed property names で
// @@iterator メソッドを実装する場合
const iterableObj2 = {
  [Symbol.iterator]: function() {
    // イテレーターを返却する
  }
}
String, Array, Map などのオブジェクトは反復可能

String, Array, Map, Set などでは、その prototype に Symbol.iterator をデフォルトで持っているため、そのインスタンスオブジェクトは全て反復可能オブジェクトとなる。

Array.prototype オブジェクトの中に Symbol.iterator が存在する
Object のオブジェクトは反復可能ではない

Object.prototype には、Symbol.iterator が存在しないため、例えばオブジェクトリテラルなどは反復可能オブジェクトではない。

Object.prototype には、Symbol.iterator が存在しない

しかし、適当なイテレーターを返すメソッドを Symbol.iterator をキーとして登録すれば、反復可能なオブジェクトにすることはできる。

// オブジェクトリテラル
const obj = {
  a: 1,
  b: 2,
  c: 3
}

// オブジェクトリテラルは反復可能ではない
console.log(Symbol.iterator in obj);  // false

// Object.entries() メソッドは ES2017 で導入された静的メソッドであり、
// 引数に指定したオブジェクトに対して
// 自身が所有する列挙可能な文字列プロパティの
// すべての [key, value] の配列を返却する。
console.log(Object.entries(obj));
// [["a", 1], ["b", 2], ["c", 3]]

for(const pair of Object.entries(obj)) {
  console.log(pair);
}
// ["a", 1]
// ["b", 2]
// ["c", 3]

// これと同等な反復処理を与える @@iterator メソッドを実装してみる。
// ただし、Object.prototype への追加はすべてのオブジェクトに影響を及ぼすため、
// あくまで勉強のための実装であることに注意。
Object.prototype[Symbol.iterator] = function() {
  const keys = Object.keys(this);
  let index = 0;
  const that = this;
  return {
    next: function() {
      const key = keys[index++];
      return {
        done: index > keys.length,
        value: [key, that[key]]
      }
    }
  }
}

for (const pair of obj) {
  console.log(pair);
}
// ["a", 1]
// ["b", 2]
// ["c", 3]

for … of 文

for … of 文は ES6 で導入された構文であり、反復可能オブジェクトの反復処理を行うための構文。

const arr = ['a', 'b', 'c'];

for (const val of arr) {
  console.log(val);
}

// a
// b
// c

for … of 文の動作は、反復可能オブジェクトが持つイテレーターの挙動に従う。

先述した反復処理を抽象化した関数 loop に相当するのがこの for … of 構文となっている。つまりこの for … of 構文は、内部的に反復対象オブジェクトの持つ @@iterator メソッド(シンボル値 Symbol.iterator でアクセスする)を使ってオブジェクトのイテレーターを生成し、反復の度にイテレーターの next メソッドを使って done と value を取得して反復処理を行なっている。

ジェネレーター(Generator)

ジェネレーター関数

function* 宣言で定義される関数はジェネレーター関数と呼ばれ、イテレーターの一種であるジェネレーターを生成するための特殊な関数となっている。ジェネレーター関数を使用することにより、イテレーターを生成する記述を簡略化することができる。

// ジェネレーター関数の定義
function* generator() {
  // ここに yield キーワードなどを使用した
  // イテレーターを生成するための処理を書く
}

// ジェネレーター(イテレーター)を生成する。
const gen = generator();

console.log('next' in gen);   // true
console.log(typeof gen.next); // function
console.log(gen.next());      // {value: undefined, done: true}

このサンプルから分かるように、function* 宣言で定義された関数(ジェネレーター関数)の戻り値であるオブジェクト(ジェネレーター)gen は、next メソッドを持っており、また next メソッドは done と value という2つのプロパティを持つオブジェクトを返却する。

したがって、イテレーターの定義により、このジェネレーター gen もイテレーターであることが分かる。

yield キーワード

ジェネレーター関数の中では yield キーワードを使用することができる。つぎのサンプルでジェネレーター関数と yield キーワードの機能を確認できる。

// ジェネレーター関数の定義
function* generator() {
  console.log('1 回目:generator() の中');
  yield 100;
  console.log('2 回目:generator() の中');
  yield 200;
  console.log('3 回目:generator() の中');
  return 300;
  console.log('return より後は実行されない');
}

// ジェネレーターを生成
const gen = generator();

// ジェネレーターの next メソッドを呼び出す
console.log('1 回目:next() の呼び出し');
console.log(gen.next());
console.log('2 回目:next() の呼び出し');
console.log(gen.next());
console.log('3 回目:next() の呼び出し');
console.log(gen.next());
console.log('4 回目:next() の呼び出し');
console.log(gen.next());
console.log('5 回目:next() の呼び出し');
console.log(gen.next());

// 1 回目:next() の呼び出し
// 1 回目:generator() の中
// {value: 100, done: false}
// 2 回目:next() の呼び出し
// 2 回目:generator() の中
// {value: 200, done: false}
// 3 回目:next() の呼び出し
// 3 回目:generator() の中
// {value: 300, done: true}
// 4 回目:next() の呼び出し
// {value: undefined, done: true}
// 5 回目:next() の呼び出し
// {value: undefined, done: true}

ジェネレーター関数 generator から生成されたジェネレーター gen は、次のような振る舞いをしていることが分かる。

  1. 1回目の gen.next() メソッドを呼び出すと、ジェネレーター関数 generator の先頭から処理の実行が開始される。
  2. 最初の yield キーワードに到達すると関数の実行は停止し、yield キーワードに指定した値 100 を value に設定し、かつ done に false を設定したオブジェクト {value: 100, done: false} を戻り値として gen.next() メソッドの呼び出しが終了する。
  3. 2回目の gen.next() メソッドを呼び出すと、ジェネレーター関数 generator の最初の yield キーワードの直後から実行が再開される。
  4. 2番目の yield キーワードに到達すると関数の実行は停止し、yield キーワードに指定した値 200 を value に設定し、かつ done に false を設定したオブジェクト {value: 200, done: false} を戻り値として gen.next() メソッドの呼び出しが終了する。
  5. 3回目の gen.next() メソッドを呼び出すと、ジェネレーター関数 generator の2番目の yield キーワードの直後から実行が再開される。
  6. return キーワードに到達すると関数の実行は終了し、return キーワードに指定した値 300 を value に設定し、かつ done に true を設定したオブジェクト {value: 300, done: true} を戻り値として gen.next() メソッドの呼び出しが終了する。
  7. これ以降は gen.next() メソッドを呼び出すと、ジェネレーター関数の内部は実行されず、オブジェクト {value: undefined, done: true} が返却される。

これは次のようにまとめられる。

  • ジェネレーターの next メソッドを実行すると、それを生成したジェネレーター関数が実行される。
  • yield キーワードにより、ジェネレーター関数の処理が一時停止し、次回はその続きから再開される。
  • return キーワードにより、ジェネレーター関数の実行が終了する。
  • yield キーワードにより、反復の継続を表すオブジェクト {value: xxx, done: false} が返る。
  • return キーワードにより、反復の完了を表すオブジェクト {value: xxx, done: true} が返る。
ジェネレーターを用いた反復処理

ジェネレーターはイテレーターなので、次のサンプルのような形で反復処理に使用することができる。

function* generator() {
  let index = 0;
  while(index < this.length) {
    yield this[index++];
  }
  return;
}

const arr = [1, 2, 3, 4, 5];
arr.generator = generator;

const gen = arr.generator();

while(true) {
  const result = gen.next();
  if (result.done) break;
  console.log(result.value * 2);
}

// 2
// 4
// 6
// 8
// 10
ジェネレーターを使用した反復オブジェクト

ジェネレーター関数はイテレーターを返却するので、オブジェクトに Symbol.iterator をキーとするメソッドとして登録すれば、そのオブジェクトは反復可能オブジェクトになる。

function* generator() {
  // 何らかの処理
}

const obj = {
  [Symbol.iterator]: generator
}

for (const val of obj) {
  // 反復処理
}

また、上記サンプルはオブジェクトにおけるメソッドの省略記法で書くと次のようになる。

const obj = {
  *[Symbol.iterator]() {  // アスタリスクは前に置く
    // 何らかの処理
  }
}

for (const val of obj) {
  // 反復処理
}

イテレーターを使用した反復オブジェクトのサンプルを、ジェネレーターを使って書き換えると次のようになる。

// オブジェクトリテラル
const obj = {
  a: 1,
  b: 2,
  c: 3
}

// オブジェクトリテラルは Symbol.iterator を持たないため、
// 反復可能オブジェクトではない。
// そこでジェネレーターを使って反復可能オブジェクトにしてみる。

obj[Symbol.iterator] = function*() {
  // const keys = Object.keys(this);
  // let index = 0;
  // while(index < keys.length) {
  //   const key = keys[index++];
  //   yield [key, this[key]];
  // }
  // return;
  for (const key in this) {
    if (this.hasOwnProperty(key)) {
      yield [key, this[key]];
    }
  }
}

for (const pair of obj) {
  console.log(pair);
}
// ["a", 1]
// ["b", 2]
// ["c", 3]

スプレッド演算子(Spread Operator)

スプレッド演算子とは

スプレッド演算子「…」は ES6 で導入された演算子であり、反復可能なオブジェクトを展開することができる。また ES9 からはオブジェクトの列挙可能なプロパティの展開もできるようになった。

反復可能オブジェクトの展開でスプレッド演算子を利用できる場面は、

  • 配列リテラルでの展開: let arr = [a, b, …iterableObj, c, d];
  • 関数呼び出し時の実引数の展開: func(a, b, …iterableObj, c, d);

であり、列挙可能なプロパティの展開でスプレッド演算子を利用できる場面は、

  • オブジェクトリテラルでの展開: let o = {a: 1, b: 2, …obj, c: 3, d: 4};

となっている。

反復可能オブジェクトの展開

配列リテラル関数呼び出し時の実引数において、反復可能オブジェクトをスプレッド演算子を用いて展開することができる。

配列リテラルでスプレッド演算子を利用するの例

const iterableObj = ['apple', 'orange', 'banana'];
const arr = [1, 2, ...iterableObj, 3, 4];

console.log(arr);
// [1, 2, "apple", "orange", "banana", 3, 4]
// スプレッド演算子を使って配列をコピーできる。
const arr1 = [1, 2, 3, 4, 5];
const copyArr1 = [...arr1];

console.log(copyArr1);  // [1, 2, 3, 4, 5]
console.log(arr1 === copyArr1); // false

// ただし、プリミティブ型でない要素を持っている場合、
// その参照がコピーされるため、完全なコピーにはならない点に注意!!
const notPrimitive = [1, 2, 3];

const arr2 = ['apple', 'orange', notPrimitive, 'banana'];
const copyArr2 = [...arr2];

console.log(arr2[2] === notPrimitive);  // true
console.log(copyArr2[2] === notPrimitive);  // true

notPrimitive.push(4);
// notPrimitive --> [1, 2, 3, 4]
// arr2 --> ["apple", "orange", [1, 2, 3, 4], "banana"]
// copyArr2 --> ["apple", "orange", [1, 2, 3, 4], "banana"]
// 反復可能オブジェクトでもスプレッド演算子を利用できる。
const iterableObj = {a: 1, b: 2, c: 3};
iterableObj[Symbol.iterator] = function*() {
  for (const key in this) {
    yield [key, this[key]];
  }
}

const arr = [...iterableObj];
console.log(arr);
// [["a", 1], ["b", 2], ["c", 3]]

関数呼び出しでスプレッド演算子を利用する例

function myfunc(arg1, arg2, arg3, arg4, arg5) {
  console.log(arg1);
  console.log(arg2);
  console.log(arg3);
  console.log(arg4);
  console.log(arg5);
}

// 反復可能オブジェクト
const iterableObj = {a: 1, b: 2, c: 3};
iterableObj[Symbol.iterator] = function*() {
  for (const key in this) {
    yield [key, this[key]];
  }
}

myfunc('apple', ...iterableObj, 'orange');
// apple
// ["a", 1]
// ["b", 2]
// ["c", 3]
// orange
列挙可能なプロパティの展開

オブジェクトリテラルの中でスプレッド演算子を利用することで、別のオブジェクトの列挙可能なプロパティを展開することができる。

ただし、この場合のスプレッド演算子の挙動は、反復可能オブジェクトの配列リテラル・関数呼び出しにおける展開とは全く異なるメカニズムが働いている。反復可能性は関係ないことに注意。

const obj1 = {a: 1, b: 2, c: 3};
const obj2 = {...obj1};

console.log(obj2);  // {a: 1, b: 2, c: 3}
const obj1 = {a: 1, b: 2, c: 3};
const obj2 = {a: 'apple', ...obj1, b: 'banana'};

console.log(obj2);  // {a: 1, b: "banana", c: 3}

関数の残余引数(Rest Parameter)

残余引数とは

ES6 からは、関数の定義における最後の引数に「…」を接頭辞として付けると、その位置にある残りの実引数を配列で受け取ることができる。この配列として受け取る引数のことを残余引数と呼んでいる。

function myfunc(arg1, arg2, ...restArgs) {
  console.log(arg1);
  console.log(arg2);
  console.log(restArgs);
}

myfunc('apple', 'orange', 'banana', 1, 2, 3, 4);
// apple
// orange
// ["banana", 1, 2, 3, 4]
可変長引数の関数を作成

残余引数を使うことで簡単に可変長引数の関数を作成することができる。

function sum(...restArgs) {
  let result = 0;
  for (const val of restArgs) {
    result += val;
  }
  return result;
}

console.log(sum(1, 2, 3));        // 6
console.log(sum(1, 2, 3, 4, 5));  // 15

Map と Set

Map オブジェクト

Map とは

Map は ES6 から導入された、キーと値のペアを管理するためのオブジェクト。Object もキーと値のペアを管理する点では同じだが、管理は専用のメソッドを介して行う。

// Map のオブジェクトを作成するにはコンストラクタから生成する。
const map = new Map();

// キーと値のペアを追加するには、専用の set メソッドを使用する。
map.set('1', 'apple');
map.set(1, true);
map.set(false, [1, 2, 3]);

// 値を取得するには、専用の get メソッドを使用する。
console.log(map.get('1'));    // apple
console.log(map.get(1));      // true
console.log(map.get(false));  // [1, 2, 3]

// キーと値のペアを削除するには、専用の delete メソッドを使用する。
map.delete('1');
console.log(map.get('1'));  // undefined

また次のサンプルから分かるように、Map のオブジェクトは通常のオブジェクトとはデータの管理の仕方が根本的に異なっている。

set メソッドで追加したキーと値のペアは [[Entries]] と書かれたプロパティに配列のような形で格納され、通常のオブジェクトプロパティとは区別されていることが分かる。

const map = new Map();

map.set('1', 'apple');
map.set(1, true);
map.set(false, [1, 2, 3]);

// 通常のオブジェクトプロパティとして追加してみる
map.a = 1;
map.b = 2;
map.c = 3;
Map のオブジェクトのデータ構造
任意の型をキーとして利用できる

任意の型をキーとして利用できるので、オブジェクトや関数さえもキーとすることができる。ただし、プリミティブ型ではないもの(つまりオブジェクト)をキーとして利用する際には、いったん変数に格納して参照を保持し、その変数をキーとして利用するという方法をとる必要があるので注意。

const map = new Map();

// オブジェクトをキーとして利用する
const key1 = {};
map.set(key1, 'value1');
console.log(map.get(key1)); // value1

// 関数をキーとして利用する
const key2 = function() {};
map.set(key2, 'value2');
console.log(map.get(key2)); // value2
for … in 文でキーを列挙することはできない
const map = new Map();

map.set('1', 'apple');
map.set(1, true);
map.set(false, [1, 2, 3]);

console.log('for ... in 実行前');
for (const key in map) {
  console.log(key);
}
console.log('for ... in 実行後');

// for ... in 実行前
// for ... in 実行後
for … of 文で反復処理を行うことができる
const map = new Map();

map.set('1', 'apple');
map.set(1, true);
map.set(false, [1, 2, 3]);

for (const pair of map) { // キーと値のペアが配列で取り出される
  console.log(pair);
}

// ["1", "apple"]
// [1, true]
// [false, Array(3)]

for (const [key, val] of map) {
  console.log(key, val);
}

// 1 apple
// 1 true
// false [1, 2, 3]

Set オブジェクト

Set とは

Set は ES6 から導入された、重複しない値の集合を管理するためのオブジェクト。

// Set のオブジェクトを作成するには、コンストラクタから生成する。
const myset = new Set();

// 値を追加するには、専用の add メソッドを使用する。
myset.add('10');
myset.add(10);
myset.add(true);

const obj = {};
myset.add(obj);

// インデックスやキーなどで要素にアクセスする手段がない。

// has メソッドで指定した値の有無を判定できる。
console.log(myset.has('1000')); // false
console.log(myset.has('10'));   // true
console.log(myset.has(obj));    // true

// 指定した値を削除するには delete メソッドを使用する。
myset.delete(obj);
console.log(myset.has(obj));    // false

また Map と同様に、Set のオブジェクトは通常のオブジェクトとはデータの管理の仕方が根本的に異なっている。

add メソッドで追加した値は [[Entries]] と書かれたプロパティに配列のような形で格納され、通常のオブジェクトプロパティとは区別されていることが分かる。

const myset = new Set();

myset.add('10');
myset.add('10');
myset.add(true);

// 通常のオブジェクトプロパティとして追加してみる。
myset.a = 1;
myset.b = 2;
myset.c = 3;
Set のオブジェクトのデータ構造
重複する値を格納することはできない
const myset = new Set();

myset.add('10');
myset.add(10);
myset.add(true);
console.log(myset); // Set(3) {"10", 10, true}

// 重複した値を追加しようとしても無視される。
myset.add(10);
console.log(myset); // Set(3) {"10", 10, true}
インデックスやキーがないため、for … in で列挙できない
const myset = new Set();

myset.add('10');
myset.add(10);
myset.add(true);

console.log('for ... in 実行前');
for (const key in myset) {
  console.log(key);
}
console.log('for ... in 実行後');

// for ... in 実行前
// for ... in 実行後
for … of 文で反復処理を行うことができる
const myset = new Set();

myset.add('10');
myset.add(10);
myset.add(true);
myset.add({a:1, b:2});
myset.add(function() {});

for (const val of myset) {
  console.log(val);
}

// 10
// 10
// true
// {a:1, b:2}
// ƒ () {}
インデックスやキーなどで要素にアクセスできない

Set にはインデックスやキーなどで要素にアクセスする手段がないため、インデックスでアクセスしたい場合には配列に変換する。

const myset = new Set();

myset.add('10');
myset.add(10);
myset.add(true);
myset.add({a:1, b:2});
myset.add(function() {});

console.log(myset); // Set(5) {"10", 10, true, {…}, ƒ}

// 配列に変換
const arr = Array.from(myset);
console.log(arr);   // (5) ["10", 10, true, {…}, ƒ]

// スプレッド演算子でも配列に変換できる。
const arr2 = [...myset];
console.log(arr2);  // (5) ["10", 10, true, {…}, ƒ]

非同期処理

ブラウザとスレッド

スレッドとは

スレッド(thread)とは、英語で「糸」という意味だが、プログラミングにおいては、「連続して実行される一本の処理の流れ」を意味する。

まず「処理A」の実行を開始し、「処理A」が終了したら「処理B」の実行を開始し、「処理B」が終了したら「処理C」を開始する・・・、というように、「処理A」「処理B」「処理C」・・・を順番に実行する一本の処理の流れのことをスレッドと呼んでいる。

ブラウザに存在するスレッド

ブラウザの JavaScript に関連するスレッドには、

  • Main Thread
  • Service Worker
  • Web Worker

など色々なスレッドがあるが、主に JavaScript が実行されるのは Main Thread であり、このスレッドが一番重要。

メインスレッド(Main Thread)とは

ブラウザのメインスレッドでは「JavaScript の実行」と「レンダリング(画面描画処理)」という2つの処理が行われる。JavaScript の実行が先に行われ、その結果、画面への変更がある場合にはレンダリングが行われる。

FPS(Frames Per Second)

FPS は1秒間あたりの画面(フレーム)更新頻度の単位であり、例えば、1秒間に 60 回画面を更新する場合、その更新頻度は 60fps であると表現される。これは 16.7ms に1回画面が更新されることを意味する。

60fps の更新頻度であれば、人の目にはスムーズに変化しているように見える。現在の液晶テレビは 60fps であり、昔のブラウン管テレビは 30fps だったので、30fps ~ 60fps の間であれば見るに堪える画面変化になる。

同期処理と非同期処理

同期処理とは

JavaScript における同期処理とは、コードに記述した通りの順番で実行され、その実行が完了してから次の処理に進むような処理を同期処理と呼んでいる。

// 同期処理として実行される関数を定義
// 引数 ms で指定した時間(ミリ秒単位)の間、
// ループを継続することでメインスレッドを占有する。
function sleep(ms, n) {
  console.log(`sleep start (${n} 回目)`);
  const start = new Date();
  while (new Date() - start < ms);
  console.log(`sleep end (${n} 回目)`);
}

// 同期処理なので順番に実行される
sleep(5000, 1);
sleep(5000, 2);
sleep(5000, 3);

// sleep start (1 回目)
// sleep end (1 回目)
// sleep start (2 回目)
// sleep end (2 回目)
// sleep start (3 回目)
// sleep end (3 回目)

非同期処理とは

JavaScript における非同期処理とは、同期処理ではない処理、つまり順番に実行しない処理や完了しなくても次の処理に進んでしまうような処理のことをいう。

function sleep(ms, n) {
  console.log(`sleep start (${n} 回目)`);
  const start = new Date();
  while (new Date() - start < ms);
  console.log(`sleep end (${n} 回目)`);
}

function greet(msg) {
  console.log(msg);
}

setTimeout(function() { // この無名関数は非同期処理となる
  console.log('Asynchronous processing')
}, 1000);

sleep(2000, 1);   // 同期処理
greet('hello1');  // 同期処理
sleep(3000, 2);   // 同期処理
greet('hello2');  // 同期処理

// sleep start (1 回目)
// sleep end (1 回目)
// hello1
// sleep start (2 回目)
// sleep end (2 回目)
// hello2
// Asynchronous processing

このサンプルにおける「Asynchronous processing」というコンソールへの出力は、setTimeout を実行してから1秒後(1000ms 後)に出力されることが期待されるが、実際には最後の同期処理の「hello2」が出力された後になっている。つまり実行が後回しにされている。

この非同期処理の振る舞いを理解するためには、コールスタック・タスクキュー・イベントループ・Web APIs の働きを知る必要がある。

コールスタック(Call Stack)

コールスタックについては以前に説明したが、次のように「実行中のコードが辿ってきたコンテキスト」を積み重ねたものだった。そしてコールスタックの一番上に積まれているコンテキストが、現在実行中のコンテキストを表すのだった。

コールスタック

タスクキュー(Task Queue)

実行待ちの非同期処理の行列のことをタスクキューと呼ぶ。つまりブラウザのような JavaScript 実行環境には、非同期処理が順番に1列に並んで実行されるのを待つような場所があり、それをタスクキューと呼んでいる。

タスクキュー

ちなみにキュー(queue)とは、順番を待っている人が作る行列を意味する。キュー(行列)は、先に並んだ人が先に出ていくという性質を持つが、この性質を FIFO(First In First Out)と呼ぶ。

タスクキューも FIFO であり、先に並んだ非同期処理が先に出ていくという性質を持っている。

イベントループ(Event Loop)

イベントループは、ブラウザなどの JavaScript 実行環境が持つ仕組みの一つであり、コールスタックが空かどうかを監視し、空であればタスクキューの先頭に並んでいるコールバック関数(非同期処理)を取り出して実行させるもの。

イベントループ

Web APIs

以前に説明したが、ブラウザの JavaScript 実行環境では、Web APIs と呼ばれる JavaScript からブラウザを操作するための機能が提供されている。例えば、setTimeout 関数や マウス操作のイベントなども Web APIs の中で実装されている。

特に、Web APIs の機能の中でも非同期処理を行うものを利用する場合は、行わせたい非同期処理の内容をコールバック関数として記述し、該当の API のメソッドを呼び出してコールバック関数を渡し、API に登録するという使い方をする。

Web APIs

非同期処理の実行の流れ

では、先ほどのサンプルを、コールスタック・タスクキュー・イベントループ・Web APIs の働きから理解してみる。

function sleep(ms, n) {
  console.log(`sleep start (${n} 回目)`);
  const start = new Date();
  while (new Date() - start < ms);
  console.log(`sleep end (${n} 回目)`);
}

function greet(msg) {
  console.log(msg);
}

setTimeout(function myfunc() { // この myfunc は非同期処理となる
  console.log('Asynchronous processing')
}, 1000);

sleep(2000, 1);   // 同期処理
greet('hello1');  // 同期処理
sleep(3000, 2);   // 同期処理
greet('hello2');  // 同期処理

// sleep start (1 回目)
// sleep end (1 回目)
// hello1
// sleep start (2 回目)
// sleep end (2 回目)
// hello2
// Asynchronous processing

まず最初の2つの function キーワードによる関数の宣言部分は、グローバルコンテキストにおいて2つの関数オブジェクト sleep と greet を作成しているだけなので特に問題はないだろう。

そしてこれらの関数の宣言後に、まず WebAPIs が提供する setTimeout 関数により、関数 myfunc をタイマーに設定値 1000ms とともに登録している。

setTimeout 関数により myfunc をタイマーに登録

setTimeout 関数は myfunc をタイマーに登録したら、Web APIs が保有しているタイマーを動作させ、すぐに終了する。

setTimeout 関数はすぐに終了する

次に「sleep(2000, 1);」がにより sleep 関数が実行される。2000 を指定しているので 2000ms の間、メインスレッドを占有する。その間に先ほどの Web APIs のタイマーは、スタートから設定値の 1000ms に到達する。

これにより Web APIs のタイマーは、登録されているコールバック関数 myfunc をタスクキューに追加する。タイマーの設定時間が経過したら myfunc は実行されるのではなく、タスクキューに追加されるという点が重要となる。

タイマーの設定時間が経過したら myfunc はタスクキューに追加される

これらの Web APIs による登録した非同期処理のタスクキューへの追加は、メインスレッドの実行とは独立して行われる。

そしてタスクキューに追加されたコールバック関数は、イベントループによって取り出されるわけだが、イベントループはコールスタックが空になるまでタスクキューから取り出すことはしない

そのため、グローバルコンテキストから呼ばれている処理

sleep(2000, 1);   // 同期処理
greet('hello1');  // 同期処理
sleep(3000, 2);   // 同期処理
greet('hello2');  // 同期処理

が全て実行を終了し、グローバルコンテキストがコールスタックから削除されるまで、タスクキューの先頭で待っている myfunc は実行されない。

コールスタックが空になったら myfunc が取り出される

以上がサンプルの処理の流れを、コールスタック・タスクキュー・イベントループ・Web APIs の連携から理解したものとなっている。

以上のことから、JavaScript における非同期処理は、次のような特徴を持つことが分かる。

  • 非同期処理の本体はコールバック関数として記述される。
  • 非同期処理を記述する時は、setTimeout 関数や addEventListener メソッドのように API にコールバック関数を登録する関数と、実際に非同期で行わせたい処理(コールバック関数)の2つをセットにして記述する。
  • 非同期処理は、タスクキューに追加された順番で実行される。
  • 非同期処理は、タスクキューに追加され、イベントループによってコールスタックが空のときに取り出されて実行される。

非同期処理のチェーン

ある非同期処理から別の非同期処理を実行させたい場合の記述方法について考える。例えば、通信である情報をリクエストして、情報の取得に成功したら別の情報をリクエストする、などのケースがそれにあたる。

// 非同期処理を実行する関数の例
// asynchronousProc 関数は、呼び出しから1秒後に callback を実行する。
function asynchronousProc(callback) {
  setTimeout(callback, 1000);
}

// 1秒後に実行させたい処理
const func1 = function() {
  console.log('first');
}

// さらにその1秒後に実行させたい処理
const func2 = function() {
  console.log('second');
}

// さらにその1秒後に実行させたい処理
const func3 = function() {
  console.log('third');
}

// 非同期処理を開始する
asynchronousProc(function() {
  func1();
  asynchronousProc(function() {
    func2();
    asynchronousProc(function() {
      func3();
    })
  })
})

このように非同期処理から別の非同期処理を連続して実行させる場合、コールバック関数の中で別の非同期処理を実行するということを繰り返すため、このような入れ子状の記述になる。

Promise

Macrotasks と Microtasks

async と await

Fetch API による非同期ネットワーク通信

例外処理とエラー

モジュール構文(import, export)

Strict モード

Proxy オブジェクト

Reflect オブジェクト

WeakMap オブジェクト

JSON 形式と JSON オブジェクト

localStorage, sessionStorage

タイトルとURLをコピーしました