Open9

JavaScript覚え書き

yub0nyub0n

ECMAScript

ECMAScript の生い立ち

1990年代、インターネット黎明期に以下の2つのブラウザがあり、
それぞれにクライアントサイドのスクリプト言語が存在した。

  • Netscape Navigator:JavaScript
  • Internet Explorer:JScript

JavaScriptとJScriptは非互換だったため、
それらJavaScript言語のコア部分を仕様策定したものECMAScript[1]である。
ブラウザ間での仕様を統一することで開発者が楽に

ECMAScript のバージョン

ES5→ES6へのバージョンアップには6年間かかっており、
ES6では多くの機能追加がなされた。
仕様策定はLiving Standardという方式に則っており、
機能ごとに仕様を策定し、仕様が決まったものから最新版の仕様書に追加していく。
ECMAScript proposals

Edition 公開年 通称 追加された主な機能
5 2009 ES5 strictモード、getter/setter、プロパティ末尾のカンマを無視
6 2015 ES6(ES2015) クラス、モジュール、イテレータ、アロー関数、let、const ...
7 2016 ES7(ES2016) 冪乗演算子、Array.prototype.includes
8 2017 ES8(ES2017) 非同期関数 (async/await)
9 2018 ES9(ES2018) オブジェクトに対するスプレッド構文
10 2019 ES10(ES2019) Array.prototype.flat/flatMap
11 2020 ES11(ES2020) null条件演算子、null合体演算子
脚注
  1. ECMAインターナショナルという情報通信システム分野における標準化団体が策定。
    ECMAScriptはECMAインターナショナルが標準化した仕様のうちの1つという位置付け。 ↩︎

yub0nyub0n

JavaScript の実行環境

ブラウザの内部構造

ブラウザには以下のようなさまざまな機能がある中に、JavaScriptエンジンも存在している。

図: ブラウザの主な構成要素

  • ユーザーインターフェイス
  • ブラウザエンジン
  • データストレージ
  • レンダリングエンジン
  • ネットワーキング
  • JavaScriptエンジン
  • UIバックエンド

ブラウザとJavaScriptエンジン

ブラウザのJSエンジンの内部では、
ECMAScript + Web APIs(DOM API、Fetch API、XHR API、WebRTCなど)
が内包されて利用できるようになっている。
組み込むことでどの環境でもJavaScriptが実行できるようになるJavaScriptエンジンのことを
Universal JavaScriptと呼ぶ。
主要なブラウザとJavaScriptエンジンは以下の通り。

ブラウザ JavaScriptエンジン
Google Chrome V8
Opera V8
Microsoft Edge(chromium) V8
Safari JavaScriptCore
Firefox SpiderMonkey
Microsoft Edge Chakra
Internet Explorer Chakra

JavaScriptが実行される仕組み

JavaScriptコードの実行にあたって、以下の2つの要素がJSエンジンによって事前に準備される。

  • グローバルオブジェクト
    ... JSエンジンによって生成されるどこからでもアクセス可能なオブジェクト
     (→ブラウザ上ではwindowオブジェクトがこれに該当し、この中にWeb APIsが含まれている)
  • this
    ... 実行コンテキストによって指すオブジェクトが変わる
     (→グローバルコンテキストではwindowオブジェクトを指す)
console
> window
<• ▶︎ Window {window: Window, self: Window, document: document, name: '', location: Location,}
> this
<• ▶︎ Window {window: Window, self: Window, document: document, name: '', location: Location,}

実行コンテキスト

実行されるコードと上記2つの要素を含めて実行コンテキスト(=コードを実行する際の文脈・状況)と呼ぶ。

  • グローバルコンテキスト(cf. モジュールコンテキスト)
    ... jsファイル直下のコンテキスト
     実行中のコンテキスト内の変数・関数 / グローバルオブジェクト / this の3つが利用可能
  • 関数コンテキスト
    ... jsファイル内で宣言された関数内でのコンテキスト
     実行中のコンテキスト内の変数・関数 / arguments / super / this / 外部変数 の5つが利用可能
      * super は特殊な環境でのみ利用可能
  • evalコンテキスト
    ... evalメソッドは現在非推奨であるため割愛
main.js
let a = 0;
 // グローバルコンテキスト
console.log(this, a);

function b() {
    // 関数コンテキスト
    console.log(this, arguments, a);
}
b();
console
 ▶︎ Window {window: Window, self: Window, document: document, name: '', location: Location,}
 0
----------------------------------------------------------------------------------------------------
 ▶︎ Window {window: Window, self: Window, document: document, name: '', location: Location,}
 ▶︎ Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]callee: ƒ b()length: 0Symbol(Symbol.iterator): ƒ values()[[Prototype]]: Object
 0

コールスタック

実行中のコードがたどってきたコンテキストの積み重ねをコールスタックと呼ぶ。

main.js
function a() {
}
function b() {
    a();
}
function c() {
    b();
}
c();

上記のコードの場合、コールスタックは以下のようになり、ブラウザのDeveloperTools上でも確認できる。
グローバルコンテキスト → c → b → a

aが実行されると、コールスタックからaが消え、bが実行されているコンテキストとなる。
グローバルコンテキスト → c → b → aグローバルコンテキスト → c → b
このようなスタックの仕組みを後入れ先出し LIFOと呼ぶ。(Last In, First Out)

ホイスティング

コンテキスト内で宣言した変数や関数の定義をコード実行前にメモリに配置することをホイスティングと呼ぶ。(宣言の巻き上げ)

main.js
// 宣言の巻き上げが可能な例
a(); // -> a is called
console.log(b); // -> undefined(宣言のみ行われるため0の代入は行われていない)

function a () {
    console.log('a is called');
}
var b = 0;

// 宣言の巻き上げができない例
console.log(c); // -> Uncaught ReferenceError: Cannot access 'c' before initialization
console.log(d); // -> Uncaught ReferenceError: Cannot access 'd' before initialization

let c = 0;
const d = function() { // 関数式(無名関数で変数として宣言)の場合は変数と同様の挙動となる
    console.log('d is called');
}

※JSエンジンによっても挙動が変わってくる場合があるので注意

yub0nyub0n

スコープ

スコープ
... 実行中のコードから値と式が参照できる範囲のこと。スコープには以下の5種類が存在する。

  • グローバルスコープ
  • スクリプトスコープ
  • 関数スコープ
  • ブロックスコープ
  • モジュールスコープ

グローバルスコープ, スクリプトスコープ

グローバルスコープ
... windowオブジェクトのこと。
 varを使って宣言した変数や関数の場合、windowオブジェクトのプロパティとして格納されることで
 どこからでも呼び出せる。
スクリプトスコープ
... (ブロックに囲まれていない状態で)letやconstを使って宣言した変数の場合に適用されるスコープ。
 どこからでも呼び出せて、一般的にはスクリプトスコープもグローバルスコープと呼ばれる。

main.js
let a = 0; // スクリプトスコープ
var b = 0; // グローバルスコープ
function c() {} // グローバルスコープ

console.log(b);
console.log(window.b); // console.log(b);と同義

window.d = 1; // var d = 1;と同義 
let d = 2;
console.log(d); // スコープチェーンという仕組みにより、let d = 2;が採用される

スコープはブラウザのDeveloperToolsで確認可能。

関数スコープ, ブロックスコープ

関数スコープ
... 関数の波括弧{}内でのみ参照が可能というスコープ。
 関数の内部で宣言した変数はその関数内でのみ呼び出すことができ、関数の外では参照エラーとなる。
ブロックスコープ
... 波括弧{}(=ブロック)[1]内で、letやconstを使って[2]宣言した変数の場合に適用されるスコープ。
 ブロックの内部で宣言した変数はそのブロック内でのみ呼び出すことができ、ブロックの外では参照エラーとなる。

main.js
// 関数スコープ
function a() {
    let b = 0;
    console.log(b); // -> 0
    var c = 1;
    console.log(c); // -> 1
}
a();
console.log(b); // Uncaught ReferenceError: b is not defined
console.log(c); // Uncaught ReferenceError: c is not defined

// ブロックスコープ
{
    const d = 2;
    console.log(d); // -> 2
    var e = 3;
    console.log(e); // -> 3
}
console.log(d); // Uncaught ReferenceError: d is not defined
console.log(e); // -> 3
脚注
  1. 波括弧{}内であっても、関数の波括弧{}内の場合は関数スコープとなる。 ↩︎

  2. varを使った変数や関数の宣言の場合はブロックスコープは適用されない。 ↩︎

yub0nyub0n

スコープ②

スコープと実行コンテキスト

グローバルコンテキスト(cf. モジュールコンテキスト)
... jsファイル直下のコンテキスト
 実行中のコンテキスト内の変数・関数 / グローバルオブジェクト / this の3つが利用可能

グローバルコンテキストの「実行中のコンテキスト内の変数・関数」
グローバルスコープ

関数コンテキスト
... jsファイル内で宣言された関数内でのコンテキスト
 実行中のコンテキスト内の変数・関数 / arguments / super / this / 外部変数 の5つが利用可能

関数コンテキストの「実行中のコンテキスト内の変数・関数」
実行中の関数スコープ

レキシカルスコープ

レキシカルスコープ
... コードを書く場所によって参照できる変数が変わるスコープ。[1]
 コードを記述した時点で決定するため、静的スコープとも呼ばれる。

main.js
let a = 2; // グローバル(スクリプト)スコープ
function fn1() {
    let b = 1; // 関数スコープ
    function fn2() {
        let c = 3;
        console.log(b); // bは関数スコープだが、fn2がfn1の関数スコープ内で宣言されているため参照可能
    }
    fn2();
}
fn1();

上記のコードの場合、参照可能な範囲(自分より外側が参照可能なため外部スコープとも呼ぶ)は以下の通りになる。

  • fn1の関数スコープからは、グローバルスコープのa(とfn1) が参照可能
  • fn2の関数スコープからは、グローバルスコープのa(とfn1) と fn1の関数スコープのb(とfn2) が参照可能
    レキシカルスコープのもう一つの意味として、上記の例のような、実行中のコードから見た外部スコープのことも指す。

関数コンテキスト
... jsファイル内で宣言された関数内でのコンテキスト
 実行中のコンテキスト内の変数・関数 / arguments / super / this / 外部変数 の5つが利用可能

関数コンテキストの「外部変数」
レキシカルスコープ

スコープチェーン

スコープチェーン
... スコープが複数階層で、連なっている状態。

main.js
let a = 2;
function fn1() {
    let a = 1;
    function fn2() {
        let a = 3;
        console.log(a); // -> 3
    }
    fn2();
}
fn1();
main.js
let a = 2; // スクリプトスコープ
window.a = 4; // グローバルスコープ
function fn1() {
    function fn2() {
        console.log(a); // -> 2 (グローバルスコープがスクリプトスコープより外側であるためletが採用される)
    }
    fn2();
}
fn1();

※ホイスティングとスコープチェーンとの組み合わせ

main.js
let a = 2;
window.a = 4;
function fn1() {
    // let a = 1;
    function fn2() {
        console.log(a); // -> undefined (varはif文の中でブロックスコープが形成されない
        if (true) {           //          & ホイスティングによってaがundefinedで宣言されてしまうため)
            var a = 3;
        }
    }
    fn2();
}
fn1();
脚注
  1. "lexical"というのは元々「語彙の、辞書の、辞書的な」という意味で、
    この場合はソースコードのどこに何を書いているかという意味。 ↩︎

yub0nyub0n

スコープ③

クロージャー

クロージャー
... レキシカルスコープの変数を関数が使用している状態。

main.js
function fn1() {
let b = 1;
    function fn2() {
        console.log(b); // 外側の(レキシカル)スコープの変数bを参照している状態=クロージャー
    }
    fn2();
}
fn1();

クロージャーを使った実装①:プライベート変数の定義

関数を呼ぶたびに1ずつインクリメントした数字をコンソールに出力する関数

【NG例】

main.js
let num = 0;
function increment() {
    console.log(++num);
}
increment(); // -> 1
increment(); // -> 2
increment(); // -> 3
num = 0; // この書き方だとどこからでもnumにアクセスできるため、誤って上書きされてしまう可能性がある
increment(); // -> 1

【OK例】

main.js
function incrementFactory() {
    let num = 0; // 関数スコープの中にnumを定義
    function increment() {
        console.log(++num); // クロージャー
    }
    return increment; // 関数を返り値にする
}
const increment = incrementFactory(); // incrementFactoryが呼ばれたタイミングでのみnumは初期化される
increment(); // -> 1
increment(); // -> 2
increment(); // -> 3
increment(); // -> 4
// クロージャーを使ってnumをプライベート変数化しているため、numを誤って上書きされてしまう可能性がない

クロージャーを使った実装②:動的な関数生成

引数に数字を渡すことで関数を動的に生成する

main.js
function addNumberFactory(num) {
    function addNumber(value) {
        return num + value;
    }
    return addNumber;
}
const add5 = addNumberFactory(5); // 引数に5を足す関数を生成
const add10 = addNumberFactory(10); // 引数に10を足す関数を生成
console.log(add5(3)); // -> 8
console.log(add10(3)); // -> 13

即時関数(Immediately Invoked Function Expression; IIFE)

即時関数
... 関数定義と同時に一度だけ実行される関数。
 一度しか実行しないような処理(初期化など)を明示的に行ったり、
 即時関数の関数スコープを利用して、スコープ内の変数と外側の変数を明確に区別したりする場合に利用する。

main.js
// 一番単純な例
(function() {
    console.log('called');
})(); // -> called

// 引数を受け取る例
(function(name) {
    console.log('Hello, ' + name);
})('Mike'); // -> Hello, Mike

// 引数を受け取り、返り値を返す例
let result = (function(num) { // このような関数式になっている場合はfunctionの外側を()で囲まなくても即時で実行可能
    console.log('called');
    return num * 10; // 実行結果を呼び出し元に返却
})(6); // -> called
console.log(result); // -> 60

即時関数の関数スコープを利用して、スコープ内の変数と外側の変数を明確に区別する例

main.js
let c = (function() {
    let privateVal = 1; // プライベートな変数
    let publicVal = 2;
    function privateFn() { // プライベートな関数
        console.log('privateFn is called');
    }
    function publicFn() {
        console.log('publicFn is called: ' + privateVal++);
    }
    return { // 返り値にすることでパブリックにする
        publicVal,
        publicFn
        // ちなみに、オブジェクトリテラルでは変数名とプロパティ名が一致する場合は": 変数名"を省略可
    }
})();
 // cのプロパティとして呼び出しが可能に
console.log(c.publicVal); // -> 2
c.publicFn(); // -> publicFn is called: 1
c.publicFn(); // -> publicFn is called: 2
c.publicFn(); // -> publicFn is called: 3
yub0nyub0n

変数

let, const, var の違い

方法 再宣言 再代入 スコープ 初期化(ホイスティング)
let × ブロック ×
const × × ブロック ×
var 関数 undefined

再宣言

main.js
var a = 0;
var a = 1; // OK

let b = 0;
let b = 1; // Uncaught SyntaxError: Identifier 'a' has already been declared

再代入

main.js
let c = 0;
c = 1; // OK

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

スコープ

main.js
{
    var e = 0;
    let f = 0;
}
console.log(e); // 0
console.log(f); // Uncaught ReferenceError: e is not defined

初期化(ホイスティング)

main.js
console.log(g); // undefined
console.log(h); // Uncaught ReferenceError: Cannot access 'h' before initialization
var g = 0;
let h = 0;

データ型

英名
真偽値 boolean true / false
数値 number 7
文字列 string "Hello"
undefined (変数の未定義) undefined undefined
null (変数が参照を持たない,空) null null
シンボル symbol 一意の値
BigInt BigInt 15n
オブジェクト object { a : 'value' }

暗黙的な型変換

  • 動的型付け言語
    • 変数宣言時の型の宣言:なし
    • 変数を使用する状況によって、変数の型が変更される
    • メリット:記述量が少なくなる(小規模のプロジェクトでは便利)
// 動的型付け言語の例:JavaScript
let a = 0;
  • 静的型付け言語
    • 変数宣言時の型の宣言:あり
    • 変数を使用する状況によらず、常に同じ型を保持
    • メリット:変数の型が分からなくなることがない(大規模のプロジェクトではメンテナンスしやすい)
           型が決まっているため速度が出やすい
// 静的型付け言語の例:Java
int a = 0;

暗黙的な型変換
... 変数が呼ばれた状況によって変数の型が自動的に変換されること。

main.js
function printTypeAndValue(val) {
    console.log(typeof val, val); // 型と値をログに出力
}

let a = 0;
printTypeAndValue(a); // number 0
let b = '1' + a;
printTypeAndValue(b); // string 10 (暗黙的な型変換によって、aがstringに変換される)
let c = 15 - b;
printTypeAndValue(c); // number 5 (暗黙的な型変換によって、bがnumberに変換される)
let d = c - null;
printTypeAndValue(d); // number 5 (暗黙的な型変換によって、nullがnumberの0に変換される)
let e = d - true;
printTypeAndValue(e); // number 4 (暗黙的な型変換によって、trueがnumberの1に変換される)
let f = parseInt('1') + e;
printTypeAndValue(f); // number 5 (parseIntメソッドで明示的に型変換)
let g = f - undefined;
printTypeAndValue(g); // number NaN (undefinedは暗黙的な型変換ができないためNaN[非数, Not-a-Number]が返る)
yub0nyub0n

変数②

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

main.js
function printEquality(a, b) {
    console.log(a === b);
    console.log(a == b);
}

let a = '1';
let b = 1;
let c = true;
printEquality(a, b); // false true (抽象的な等価性では、ToNumber('1')==1という判定に変換される)
printEquality(b, c); // false true (抽象的な等価性では、1==ToNumber(true)という判定に変換される)
printEquality(a, c); // false true (抽象的な等価性では、'1'==ToNumber(true) -> ToNumber('1')==1という順で判定が変換される)

let e = '';
let f = 0;
let g = '0';
printEquality(e, f); // false true (抽象的な等価性では、ToNumber('')==0という判定に変換される)
printEquality(f, g); // false true (抽象的な等価性では、0==ToNumber('0')という判定に変換される)
printEquality(e, g); // false false (型が同一なので、抽象的な等価性でも文字列の中身で比較)

let h = null;
let i = undefined;
printEquality(h, i); // false true (抽象的な等価性では、nullとundefinedは等価となる)

falsy と truthy

  • falsyな値:Booleanで真偽値に変換した際にfalseになる値 ... false, 0, 0n, "", null, undefined, NaN
  • truthyな値:Booleanで真偽値に変換した際にtrueになる値 ... 上記以外
main.js
let a; // 変数が代入なしで宣言された場合は undefined
console.log(Boolean(a)); // false
a = 0;
console.log(Boolean(a)); // false
a = '';
console.log(Boolean(a)); // false
a = null;
console.log(Boolean(a)); // false
a = undefined;
console.log(Boolean(a)); // false
a = NaN; // 非数, Not-a-Number
console.log(Boolean(a)); // false

AND条件 と OR条件

  • AND条件(論理積) &&:すべてのオペランドがtrueの場合にtrueになる
  • OR条件(論理和) ||:オペランドのうち1つ以上がtrueである場合にtrueになる
main.js
const a = 1, b = 0, c = 3, d = 0, e = 0; // 複数まとめて変数宣言も可

// AND条件は左から評価していき, falseが見つかった時点でその値を返す (短絡評価)
console.log(a && b && c && d && e); // 1->0 => 0
// OR条件は左から評価していき, trueが見つかった時点でその値を返す (短絡評価)
console.log(e || d || c || b || a);  // 0->0->3 => 3
// グループ化によって評価が変わるので注意
console.log(a || b && d); // 0->0->1 => 1
console.log((a || b) && d); // 1->0->0 => 0

【応用例】

main.js
function hello(name) { // ES6からはデフォルト引数で同様の表現が可
    name = name || 'Tom'; // if (!name) { name = 'Tom'; }
    console.log('Hello ' + name);
}

hello('Bob'); // Hello Bob
hello(); // Hello Tom (デフォルトでTomが入る)

let name;
name && hello(name); // 何もしない  // if (name) { hello(name); }
name = 'Bob';
name && hello(name); // Hello Bob
yub0nyub0n

変数③

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

プリミティブ型

=> string, number, boolean, undefined, null, symbol, BigInt

  • 変数には値が格納される
  • 一度作成するとその値を変更することはできない (immutable)

【メモリ空間のイメージ】

let a = 'Hello';

別の値を変数に代入しても'Hello'というデータ自体を変更できるわけではない

a = 'Bye';

変数を別の変数に代入して値をコピーした場合、'Hello'というデータもメモリ上でコピーされ、
それぞれ異なる参照となる (値渡しという)

let a = 'Hello';
let b = a;

そのため、bの値を上書きしてもaの値には影響がない

b = 'Bye';

オブジェクト型

=> プリミティブ型以外 (= 名前(プロパティ)付きの参照を管理する入れ物)

  • 変数には参照が格納される
  • 値を変更することができる (mutable)

【メモリ空間のイメージ】

let a = {
    prop: 'Hello'
}

新たにプロパティを追加する際はaからオブジェクトへの参照が変わることなく、
プロパティを変更(追加)することができる

a.prop2 = 'Bye';

変数を別の変数に代入して値をコピーした場合、{ ... }への参照だけがメモリ上でコピーされるため、
abはそれぞれ同じオブジェクトを参照することになる (参照渡しという)

let a = {
    prop: 'Hello'
}
let b = a;

そのため、bのプロパティの値を上書きするとaのプロパティの値も変更されてしまう

b.prop = 'Bye';

ただし、bに対してオブジェクトを再代入するとaとは別のオブジェクトを参照するようになる

b = {};
yub0nyub0n

変数④

参照と引数

関数の引数に変数を渡す場合も、プリミティブ型であれば値渡し/オブジェクトであれば参照渡しとなる

let a = 0; // プリミティブ型
function fn1(arg1) {
    arg1 = 1;
    console.log(a, arg1);
}
fn1(a); // 0  1

let b = { // オブジェクト
    prop: 0
}
function fn2(arg2) {
    arg2.prop = 1;
    console.log(b, arg2);
}
fn2(b); // {prop: 1}  {prop: 1}