0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScript Primerを読んでみた感想。初学者が学んだこと・良かった点・微妙だった点

Posted at

はじめに

今後、フロントエンドのReact,Next.js,TypeScriptを触るための基礎としてJavaScriptの学習に入りました。

良かったところ

1つ1つ細く概念から使い方まで解説されていて、JSの基礎を網羅できるところ。

悪かったところ

知識量が膨大なので、知識に優先順位をつけるとさらにわかりやすくなると思った。
あまり実務で使わない分野の知識と重要な知識が乱雑しており、非効率に感じた。単元に学習優先度などがあれば重要な知識から重点的に学べるため効率よくインプットすることができそう。

学んだこと

代入

再代入できない

const bookTitle = "JavaScript Primer";
bookTitle = "新しいタイトル"; // => TypeError: invalid assignment to const 'bookTitle'

再代入できる

let bookTitle;
bookTitle = "JavaScript Primer";

データ型

プリミティブ
特徴:イミュータブル
具体例:
真偽値(Boolean): trueまたはfalseのデータ型
数値(Number): 42 や 3.14159 などの数値のデータ型
巨大な整数(BigInt): ES2020から追加された9007199254740992nなどの任意精度の整数のデータ型
文字列(String): "JavaScript" などの文字列のデータ型
undefined: 値が未定義であることを意味するデータ型
null: 値が存在しないことを意味するデータ型
シンボル(Symbol): ES2015から追加された一意で不変な値のデータ型

リテラル
特徴:ミュータブル
具体例:
プリミティブ型以外のデータ
オブジェクト、配列、関数、クラス、正規表現、Dateなど

テンプレートリテラル内で${変数名}と書いた場合に、その変数の値を埋め込むことができる

const str = "文字列";
console.log(`これは${str}です`); // => "これは文字列です"

オブジェクトリテラル

オブジェクトリテラルは{}(中カッコ)を書くことで、新しいオブジェクトを作成できる

const obj = {}; // 中身が空のオブジェクトを作成
const obj = {
    "key": "value"
};
// ドット記法
console.log(obj.key); // => "value"
// ブラケット記法
console.log(obj["key"]); // => "value"

演算子

++インクリメント演算子、--ディクリメント演算子

let num = 1;

num++; // num は 2 になる
++num; // num は 2 になる

違いは返り値

num++(後置インクリメント)
→ 先に今の値を返してから、num を +1 する

++num(前置インクリメント)
→ 先に num を +1 してから、増えた値を返す

let num = 1;
console.log(num++); // 1(表示してから増える)
console.log(num);   // 2
let num = 1;
console.log(++num); // 2(増やしてから表示)
console.log(num);   // 2

ディクリメント演算子も容量は同じ。

分割代入

const array = [1, 2];
// aには`array`の0番目の値、bには1番目の値が代入される
const [a, b] = array;
console.log(a); // => 1
console.log(b); // => 2

AND演算子(&&)

AND演算子(&&)は、左辺の値の評価結果がtrueならば、右辺の評価結果を返す。一方で、左辺の値の評価結果がfalseならば、そのまま左辺の値を返す。

// 左辺はfalsyではないため、評価結果として右辺を返す
console.log("文字列" && "右辺の値"); // => "右辺の値"
console.log(42 && "右辺の値"); // => "右辺の値"
// 左辺がfalsyであるため、評価結果として左辺を返す
console.log("" && "右辺の値"); // => ""
console.log(0 && "右辺の値"); // => 0
console.log(null && "右辺の値"); // => null

OR演算子(||)

OR演算子(||)は、左辺の値の評価結果がtrueならば、そのまま左辺の値を返し一方で、左辺の値の評価結果がfalseであるならば、右辺の評価結果を返す

// 左辺がtrueなので、左辺の値が返される
console.log(true || "右辺の値"); // => true
// 左辺がfalseなので、右辺の値が返される
console.log(false || "右辺の値"); // => "右辺の値"

Nullish coalescing演算子(??)

Nullish coalescing演算子(??)は、左辺の値がnullishであるならば、右辺の評価結果を返す。 nullishとは、評価結果がnullまたはundefinedとなる値のこと

// 左辺がnullishであるため、右辺の値の評価結果を返す
console.log(null ?? "右辺の値"); // => "右辺の値"
console.log(undefined ?? "右辺の値"); // => "右辺の値"
// 左辺がnullishではないため、左辺の値の評価結果を返す
console.log(true ?? "右辺の値"); // => true
console.log(false ?? "右辺の値"); // => false

条件(三項)演算子(?と:)

条件式 ? Trueのとき処理する式 : Falseのとき処理する式;

const valueA = true ? "A" : "B";
console.log(valueA); // => "A"
const valueB = false ? "A" : "B";
console.log(valueB); // => "B"
function addPrefix(text, prefix) {
    // `prefix`が指定されていない場合は"デフォルト:"を付ける
    const pre = typeof prefix === "string" ? prefix : "デフォルト:";
    return pre + text;
}

console.log(addPrefix("文字列")); // => "デフォルト:文字列"
console.log(addPrefix("文字列", "カスタム:")); // => "カスタム:文字列"

グループ化演算子((と))

const a = 1;
const b = 2;
const c = 3;
console.log(a + b * c); // => 7
console.log((a + b) * c); // => 9
if ((typeof a === "string" && typeof b === "string") || (typeof x === "number" && typeof y === "number")) {
    // `a`と`b`が文字列型 または
    // `x`と`y`が数値型
}

Symbolとは「絶対に他と被らない“特別なID(値)”を作れるプリミティブ型」

const a = Symbol("id");
const b = Symbol("id");

console.log(a === b); // false(説明が同じでも別物)

デフォルト引数

Nullish coalescing演算子(??)を利用することでも、 OR演算子(||)の問題を避けつつデフォルト値を指定できる

function addPrefix(text, prefix) {
    const pre = prefix || "デフォルト:";
    return pre + text;
}

関数の仮引数に対して引数の個数が多い場合、あふれた引数は単純に無視される

可変長引数

関数において引数の数が固定ではなく、任意の個数の引数を受け取りたい場合
表記方法および具体例:Math.max(...args)は引数を何個でも受け取り、受け取った引数の中で最大の数値を返す関数

const max = Math.max(1, 5, 10, 20);
console.log(max); // => 20

Rest parameters

function fn(...args) {
    // argsは、渡された引数が入った配列
    console.log(args); // => ["a", "b", "c"]
}
fn("a", "b", "c");
function fn(arg1, ...restArgs) {
    console.log(arg1); // => "a"
    console.log(restArgs); // => ["b", "c"]
}
fn("a", "b", "c");

2番目以降から代入する場合は、restArgsを使用する

Spread構文

配列を展開して関数の引数に渡す構文

Spread構文は、配列の前に...をつけた構文のことで、関数には配列の値を展開したものが引数として渡される

function fn(x, y, z) {
    console.log(x); // => 1
    console.log(y); // => 2
    console.log(z); // => 3
}
const array = [1, 2, 3];
// Spread構文で配列を引数に展開して関数を呼び出す
fn(...array);
// 次のように書いたのと同じ意味
fn(array[0], array[1], array[2]);

関数の引数と分割代入

変数名.keyの形で取り出すことができる

function printUserId(user) {
    console.log(user.id); // => 42
}
const user = {
    id: 42
};
printUserId(user);

オブジェクトの分割代入

const user = {
    id: 42
};
// オブジェクトの分割代入
const { id } = user;
console.log(id); // => 42
// 関数の引数の分割代入
function printUserId({ id }) {
    console.log(id); // => 42
}
printUserId(user);

配列の分割代入

function print([first, second]) {
    console.log(first); // => 1
    console.log(second); // => 2
}
const array = [1, 2];
print(array);

関数もオブジェクト

関数もオブジェクトなので引数に取ることができる

function fn() {
    console.log("fnが呼び出されました");
}
// 関数`fn`を`myFunc`変数に代入している
const myFunc = fn;
myFunc();

関数式

関数式とは、関数を値として変数へ代入している式

// 関数式
const 変数名 = function() {
    // 関数を呼び出したときの処理
    // ...
    return 関数の返り値;
};
// 関数式は変数名で参照できるため、"関数名"を省略できる
const 変数名 = function() {
};
// 関数宣言では"関数名"は省略できない
function 関数名() {
}

関数式では、名前を持たない関数を変数に代入できる、このような名前を持たない関数を無名関数と言う

利用用途

関数式でも関数に名前をつけることができる、しかし、この関数の名前は関数の外からは呼ぶことができない。
一方、関数の中からは呼ぶことができるため、再帰的に関数を呼び出す際などに利用される。

// factorialは関数の外から呼び出せる名前
// innerFactは関数の外から呼び出せない名前
const factorial = function innerFact(n) {
    if (n === 0) {
        return 1;
    }
    // innerFactを再帰的に呼び出している
    return n * innerFact(n - 1);
};
console.log(factorial(3)); // => 6

Arrow Function

関数式にはfunctionキーワードを使った方法以外に、Arrow Functionと呼ばれる書き方がある

// Arrow Functionを使った関数定義
const 変数名 = () => {
    // 関数を呼び出したときの処理
    // ...
    return 関数の返す値;
};
// 仮引数の数と定義
const fnA = () => { /* 仮引数がないとき */ };
const fnB = (x) => { /* 仮引数が1つのみのとき */ };
const fnC = x => { /* 仮引数が1つのみのときは()を省略可能 */ };
const fnD = (x, y) => { /* 仮引数が複数のとき */ };
// 値の返し方
// 次の2つの定義は同じ意味となる
const mulA = x => { return x * x; }; // ブロックの中でreturn
const mulB = x => x * x;            // 1行のみの場合はreturnとブロックを省略できる
const array = [1, 2, 3];
// 仮引数が1つなので`()`を省略できる
// 関数の処理が1つの式なので`return`文を省略できる
const doubleArray = array.map(value => value * 2);
console.log(doubleArray); // => [2, 4, 6]

###mapの処理
配列の各要素を1つずつ取り出して関数を適用し、その結果を同じ順番で集めた“新しい配列”を返す

コールバック関数:引数として渡される関数のこと

function 高階関数(コールバック関数) {
    コールバック関数();
}
const array = [1, 2, 3];
const output = (value) => {
    console.log(value);
};
array.forEach(output);
// 次のように実行しているのと同じ
// output(1); => 1
// output(2); => 2
// output(3); => 3

forEachメソッドとは

配列の要素を先頭から1つずつ取り出して、指定した処理(関数)を実行するメソッド。
ただし 戻り値として新しい配列は返さない

コールバック関数となる無名関数をその場で定義して渡せる

const array = [1, 2, 3];
array.forEach((value) => {
    console.log(value);
});

メソッド

オブジェクトのプロパティである関数

const obj = {
    method1: function() {
        // `function`キーワードでのメソッド
    },
    method2: () => {
        // Arrow Functionでのメソッド
    }
};

空オブジェクトのobjを定義してから、methodプロパティへ関数を代入してもメソッドを定義できる

const obj = {};
obj.method = function() {
};

メソッドを呼び出す場合は、関数呼び出しと同様にオブジェクト.メソッド名()と書くことで呼び出せる。

const obj = {
    method: function() {
        return "this is method";
    }
};
console.log(obj.method()); // => "this is method"

メソッドの短縮記法

オブジェクトリテラルの中で メソッド名(){ /メソッドの処理/ } と書くことができる

const obj = {
    method() {
        return "this is method";
    }
};
console.log(obj.method()); // => "this is method"

文と式

42のようなリテラルやfooといった変数、関数呼び出しが式です。 また、1 + 1のような式と演算子の組み合わせも式

// 1という式の評価値を表示
console.log(1); // => 1
// 1 + 1という式の評価値を表示
console.log(1 + 1); // => 2
// 式の評価値を変数に代入
const total = 1 + 1;
// 関数式の評価値(関数オブジェクト)を変数に代入
const fn = function() {
    return 1;
};
// fn() という式の評価値を表示
console.log(fn()); // => 1

処理する1ステップが1つの文

const isTrue = true;
// isTrueという式がif文の中に出てくる
if (isTrue) {
}

式文

一方で、式(Expression)は文(Statement)になれます。文となった式のことを式文と呼ぶ、基本的に文が書ける場所には式を書ける

その際に、式文(Expression statement)は文の一種であるため、セミコロンで文を区切っている

// 式文であるためセミコロンをつけている
;

ブロック文

{
    ;
    ;
}

基本的にはif文やfor文など他の構文と組み合わせて書く

// if文とブロック文の組み合わせ
if (true) {
    console.log("文1");
    console.log("文2");
}

文の末尾にはセミコロンをつけるとしていましたが、 例外としてブロックで終わる文の末尾には、セミコロンが不要となっている

// ブロックで終わらない文なので、セミコロンが必要
if (true) console.log(true);
// ブロックで終わる文なので、セミコロンが不要
if (true) {
    console.log(true);
}

条件分岐

if文

if文を使うことで、プログラム内に条件分岐を書ける

if文は次のような構文が基本形となります。 条件式の評価結果がtrueであるならば、実行する文が実行される

if (条件式) {
    実行する文;
}
if (true) {
    console.log("この行は実行されます");
}

実行する文が1つのみの場合は、{ と } のブロックを省略できるが、どこまでがif文かがわかりにくくなるため、常にブロックで囲むことが推奨されている

if (true)
    console.log("この行は実行されます");

if文は条件式に比較演算子などを使い、その比較結果によって処理を分岐するためによく使われる、 次のコードでは、xが10よりも大きな値である場合に、if文の中身が実行される

const x = 42;
if (x > 10) {
    console.log("xは10より大きな値です");
}

else if文

const version = "ES6";
if (version === "ES5") {
    console.log("ECMAScript 5");
} else if (version === "ES6") {
    console.log("ECMAScript 2015");
} else if (version === "ES7") {
    console.log("ECMAScript 2016");
}

else文

const num = 1;
if (num > 10) {
    console.log(`numは10より大きいです: ${num}`);
} else {
    console.log(`numは10以下です: ${num}`);
}

ネストしたif文

if (条件式A) {
    if (条件式B) {
        // 条件式Aと条件式Bがtrueならば実行される文
    }
}

ネストしたif文の例として、今年がうるう年かを判定する

うるう年の条件
・西暦で示した年が4で割り切れる年はうるう年
・ただし、西暦で示した年が100で割り切れる年はうるう年ではない
・ただし、西暦で示した年が400で割り切れる年はうるう年

const year = new Date().getFullYear();
if (year % 4 === 0) { // 4で割り切れる
    if (year % 100 === 0) { // 100で割り切れる
        if (year % 400 === 0) { // 400で割り切れる
            console.log(`${year}年はうるう年です`);
        } else {
            console.log(`${year}年はうるう年ではありません`);
        }
    } else {
        console.log(`${year}年はうるう年です`);
    }
} else {
    console.log(`${year}年はうるう年ではありません`);
}

一般的にはネストは浅いほうが、読みやすいコードとなるので以下のように修正できる

const year = new Date().getFullYear();
if (year % 400 === 0) { // 400で割り切れる
    console.log(`${year}年はうるう年です`);
} else if (year % 100 === 0) { // 100で割り切れる
    console.log(`${year}年はうるう年ではありません`);
} else if (year % 4 === 0) { // 4で割り切れる
    console.log(`${year}年はうるう年です`);
} else { // それ以外
    console.log(`${year}年はうるう年ではありません`);
}

switch文

switch文は、次のような構文で式の評価結果が指定した値である場合に行う処理を並べて書く

switch () {
    case ラベル1:
        // `式`の評価結果が`ラベル1`と一致する場合に実行する文
        break;
    case ラベル2:
        // `式`の評価結果が`ラベル2`と一致する場合に実行する文
        break;
    default:
        // どのcaseにも該当しない場合の処理
        break;
}
// break; 後はここから実行される
const version = "ES6";
switch (version) {
    case "ES5":
        console.log("ECMAScript 5");
        break;
    case "ES6":
        console.log("ECMAScript 2015");
        break;
    case "ES7":
        console.log("ECMAScript 2016");
        break;
    default:
        console.log("しらないバージョンです");
        break;
}
// "ECMAScript 2015" と出力される

break;を忘れてしまうと意図しないcase節が実行されてしまう。 そのため、case節とbreak文が多用されているswitch文が出てきた場合、 別の方法で書けないかを考えるべきサインとなる。

switch文はif文の代用として使うのではなく、次のように関数と組み合わせて条件に対する値を返すパターンとして使うことが多い

function getECMAScriptName(version) {
    switch (version) {
        case "ES5":
            return "ECMAScript 5";
        case "ES6":
            return "ECMAScript 2015";
        case "ES7":
            return "ECMAScript 2016";
        default:
            return "しらないバージョンです";
    }
}
// 関数を実行して`return`された値を得る
getECMAScriptName("ES6"); // => "ECMAScript 2015"

ループと反復処理

while文

while文は条件式がtrueであるならば、反復処理を行う

while (条件式) {
    実行する文;
}
let x = 0;
console.log(`ループ開始前のxの値: ${x}`);
while (x < 10) {
    console.log(x);
    x += 1;
}
console.log(`ループ終了後のxの値: ${x}`);

無限ループ

let i = 1;
// 条件式が常にtrueになるため、無限ループする
while (i > 0) {
    console.log(`${i}回目のループ`);
    i += 1;
}

do-while文

do {
    実行する文;
} while (条件式);

do-while文の実行フロー

①実行する文を実行
②条件式 の評価結果がtrueなら次のステップへ、falseなら終了
③ステップ1へ戻る

必ず最初に実行する文を処理

そのため、次のコードのように最初から条件式を満たさない場合でも、 初回の実行する文が処理され、コンソールへ1000と出力される

const x = 1000;
do {
    console.log(x); // => 1000
} while (x < 10);

for文

for (初期化式; 条件式; 増分式) {
    実行する文;
}

for文の実行フローは次のようになりる

①初期化式 で変数の宣言
②条件式 の評価結果がtrueなら次のステップへ、falseなら終了
③実行する文 を実行
④増分式 で変数を更新
⑤ステップ2へ戻る

次のコードでは、for文で1から10までの値を合計して、その結果をコンソールへ出力

let total = 0; // totalの初期値は0
// for文の実行フロー
// iを0で初期化
// iが10未満(条件式を満たす)ならfor文の処理を実行
// iに1を足し、再び条件式の判定へ
for (let i = 0; i < 10; i++) {
    total += i + 1; // 1から10の値をtotalに加算している
}
console.log(total); // => 55

任意の数値が入った配列を受け取り、その合計値を返す sum 関数を実装する、 numbers配列に含まれている要素を先頭から順番に変数totalへ加算することで合計値を計算する

function sum(numbers) {
    let total = 0;
    for (let i = 0; i < numbers.length; i++) {
        total += numbers[i];
    }
    return total;
}

console.log(sum([1, 2, 3, 4, 5])); // => 15

配列のforEachメソッド

forEachメソッドでの反復処理は、次のように書ける

const array = [1, 2, 3];
array.forEach(currentValue => {
    // 配列の要素ごとに呼び出される処理
});

forEachメソッドのコールバック関数には、配列の要素が先頭から順番に渡されて実行される

const array = [1, 2, 3];
array.forEach(currentValue => {
    console.log(currentValue);
});
// 1
// 2
// 3
// と順番に出力される

先ほどのfor文の例と同じ数値の合計を返すsum 関数をforEachメソッドで実装してみる

function sum(numbers) {
    let total = 0;
    numbers.forEach(num => {
        total += num;
    });
    return total;
}

sum([1, 2, 3, 4, 5]); // => 15

forEachはfor文の条件式に相当するものはなく、必ず配列のすべての要素を反復処理します。 変数iといった一時的な値を定義する必要がないため、シンプルに反復処理を書ける

break文

break文は処理中の文から抜けて次の文へ移行する制御文

while (true) {
    break; // *1 へ
}
// *1 次の文
switch文で出てきたものと同様で処理中のループ文を終了できる

次のコードでは配列の要素に1つでも偶数を含んでいるかを判定してる

const numbers = [1, 5, 10, 15, 20];
// 偶数があるかどうか
let isEvenIncluded = false;
for (let i = 0; i < numbers.length; i++) {
    const num = numbers[i];
    // numが2で割り切れるなら偶数
    if (num % 2 === 0) {
        isEvenIncluded = true;
        break;
    }
}
console.log(isEvenIncluded); // => true

1つでも偶数があるかがわかればいいため、配列内から最初の偶数を見つけたらfor文での反復処理を終了します。 このような処理は、使い回せるように関数として実装する

// 引数の`num`が偶数ならtrueを返す
function isEven(num) {
    return num % 2 === 0;
}
// 引数の`numbers`に偶数が含まれているならtrueを返す
function isEvenIncluded(numbers) {
    let isEvenIncluded = false;
    for (let i = 0; i < numbers.length; i++) {
        const num = numbers[i];
        if (isEven(num)) {
            isEvenIncluded = true;
            break;
        }
    }
    return isEvenIncluded;
}
const array = [1, 5, 10, 15, 20];
console.log(isEvenIncluded(array)); // => true

早期リターン:条件が見つかった状態ですぐに答えを返す
return文は現在の関数を終了させることができるため、次のように書くこともできる、 numbersに1つでも偶数が含まれていれば結果はtrueとなるため、偶数の値が見つかった時点でtrueを返す

function isEven(num) {
    return num % 2 === 0;
}
function isEvenIncluded(numbers) {
    for (let i = 0; i < numbers.length; i++) {
        const num = numbers[i];
        if (isEven(num)) {
            return true;
        }
    }
    return false;
}
const numbers = [1, 5, 10, 15, 20];
console.log(isEvenIncluded(numbers)); // => true

配列のsomeメソッド

someメソッドは、配列の各要素をテストする処理をコールバック関数として受け取り、コールバック関数が、一度でもtrueを返した時点で反復処理を終了し、someメソッドはtrueを返す

const array = [1, 2, 3, 4, 5];
const isPassed = array.some(currentValue => {
    // テストをパスするとtrue、そうでないならfalseを返す
});

someメソッドを使うことで、配列に偶数が含まれているかは次のように書くことができる、受け取った値が偶数であるかをテストするコールバック関数としてisEven関数を渡す

function isEven(num) {
    return num % 2 === 0;
}
const numbers = [1, 5, 10, 15, 20];
console.log(numbers.some(isEven)); // => true

continue文

continue文は現在の反復処理を終了して、次の反復処理を行う、continue文は、while、do-while、forの中で使うことができる

while (条件式) {
    // 実行される処理
    continue; // `条件式` へ
    // これ以降の行は実行されません
}
// `number`が偶数ならtrueを返す
function isEven(num) {
    return num % 2 === 0;
}
// `numbers`に含まれている偶数だけを取り出す
function filterEven(numbers) {
    const results = [];
    for (let i = 0; i < numbers.length; i++) {
        const num = numbers[i];
        // 偶数ではないなら、次のループへ
        if (!isEven(num)) {
            continue;
        }
        // 偶数を`results`に追加
        results.push(num);
    }
    return results;
}
const array = [1, 5, 10, 15, 20];
console.log(filterEven(array)); // => [10, 20]

Falseの時は配列に入れずにTrueの時だけ配列に入れる

if (isEven(num)) {
    results.push(num);
}

配列のfilterメソッド

filterメソッドには、配列の各要素をテストする処理をコールバック関数として渡します。 コールバック関数がtrueを返した要素のみを集めた新しい配列を返す

const array = [1, 2, 3, 4, 5];
// テストをパスしたものを集めた配列
const filteredArray = array.filter((currentValue, index, array) => {
    // テストをパスするならtrue、そうでないならfalseを返す
});

filter は 「要素ごとに判定して、trueの要素だけ集める」。
currentValue が 判定対象の値 で、index(インデックス番号) と array(配列) は必要なときだけ使う

filterメソッドを使い、偶数だけの配列を作成する

function isEven(num) {
    return num % 2 === 0;
}

const array = [1, 5, 10, 15, 20];
console.log(array.filter(isEven)); // => [10, 20]

for...in文

for (プロパティ in オブジェクト) {
    実行する文;
}


const obj = {
    "a": 1,
    "b": 2,
    "c": 3
};
// 注記: ループのたびに毎回新しいブロックに変数keyが定義されるため、再定義エラーが発生しない
for (const key in obj) {
    const value = obj[key];
    console.log(`key:${key}, value:${value}`);
}
// "key:a, value:1"
// "key:b, value:2"
// "key:c, value:3"

オブジェクトに対する反復処理のためにfor...in文は有用に見えますが、多くの問題を持っている

JavaScriptでは、オブジェクトは何らかのオブジェクトを継承してる
for...in文は、対象となるオブジェクトのプロパティを列挙する場合に、親オブジェクトまで列挙可能なものがあるかを探索して列挙する
そのため、オブジェクト自身が持っていないプロパティも列挙されてしまい、意図しない結果になる場合がある

安全にオブジェクトのプロパティを列挙するには、Object.keys静的メソッド、Object.values静的メソッド、Object.entries静的メソッドなどが利用できる

先ほどの例である、オブジェクトのキーと値を列挙するコードはfor...in文を使わずに書ける、Object.keys静的メソッドは引数のオブジェクト自身が持つ列挙可能なプロパティ名の配列を返す
そのためfor...in文とは違い、親オブジェクトのプロパティは列挙されない

for...of文

for...of文では、iterableオブジェクトから次の返す値を1つ取り出し、variableに代入して反復処理を行う

for (variable of iterable) {
    実行する文;
}

インデックス値ではなく配列の値を列挙する

const array = [1, 2, 3];
for (const value of array) {
    console.log(value);
}
// 1
// 2
// 3

JavaScriptではStringオブジェクトもiterableです。 そのため、文字列を1文字ずつ列挙できます。

const str = "𠮷野家";
for (const value of str) {
    console.log(value);
}
// "𠮷"
// "野"
// "家"

オブジェクト

オブジェクトはプロパティの集合です。プロパティとは名前(キー)と値(バリュー)が対になったもの

オブジェクトを作成する

// プロパティを持たない空のオブジェクトを作成
const obj = {};
// プロパティを持つオブジェクトを定義する
const obj = {
    // キー: 値
    "key": "value"
};
```javascript
オブジェクトリテラルのプロパティ名(キー)はクォート("や')を省略できる
そのため、次のように書いても同じ
// プロパティ名(キー)はクォートを省略することが可能
const obj = {
    // キー: 値
    key: "value"
};

数名として利用できないプロパティ名はクォート("や')で囲む必要があり

const object = {
    // キー: 値
    my-prop: "value" // NG
};

my-propというプロパティ名を定義する場合は、クォート("や')で囲む必要がある

const obj = {
    // キー: 値
    "my-prop": "value" // OK
};

複数のプロパティを定義するには、それぞれのプロパティを,(カンマ)で区切る

const color = {
    // それぞれのプロパティは`,`で区切る
    red: "red",
    green: "green",
    blue: "blue"
};
const name = "名前";
// `name`というプロパティ名で`name`の変数を値に設定したオブジェクト
const obj = {
    name: name
};
console.log(obj); // => { name: "名前" }

プロパティ名と値に指定する変数名が同じ場合は{ name }のように省略して書ける。 次のコードは、プロパティ名nameに変数nameを値にしたプロパティを設定する

const name = "名前";
// `name`というプロパティ名で`name`の変数を値に設定したオブジェクト
const obj = {
    name
};
console.log(obj); // => { name: "名前" }

{}はObjectのインスタンスオブジェクト

ObjectはJavaScriptのビルトインオブジェクト
オブジェクトリテラル({})は、このビルトインオブジェクトであるObjectを元にして新しいオブジェクトを作成するための構文

オブジェクトリテラル以外の方法として、new演算子を使うことで、Objectから新しいオブジェクトを作成できる

// プロパティを持たない空のオブジェクトを作成
// = `Object`からインスタンスオブジェクトを作成
const obj = new Object();
console.log(obj); // => {}

プロパティへのアクセス

オブジェクトのプロパティにアクセスする方法として、ドット記法(.)を使う方法とブラケット記法などがある

const obj = {
    key: "value"
};
// ドット記法で参照
console.log(obj.key); // => "value"
// ブラケット記法で参照
console.log(obj["key"]); // => "value"
obj.key; // OK
// プロパティ名が数字から始まる識別子は利用できない
obj.123; // NG
// プロパティ名にハイフンを含む識別子は利用できない
obj.my-prop; // NG

ブラケット記法では、[と]の間に任意の式を書ける

const obj = {
    key: "value",
    123: 456,
    "my-key": "my-value"
};

console.log(obj["key"]); // => "value"
// プロパティ名が数字からはじまる識別子も利用できる
console.log(obj[123]); // => 456
// プロパティ名は暗黙的に文字列に変換されているため、次も同じプロパティを参照している
console.log(obj["123"]); // => 456
// プロパティ名にハイフンを含む識別子も利用できる
console.log(obj["my-key"]); // => "my-value"

ブラケット記法ではプロパティ名に変数も利用できる
次のコードでは、プロパティ名にmyLangという変数をブラケット記法で指定している

const languages = {
    ja: "日本語",
    en: "英語"
};
const myLang = "ja";

オブジェクトと分割代入

同じオブジェクトのプロパティに何度もアクセスする場合に、何度もオブジェクト.プロパティ名と書くと冗長となりやすいから、短い名前で利用できるように、そのプロパティを変数として定義し直すことがある

const languages = {
    ja: "日本語",
    en: "英語"
};
const ja = languages.ja;
const en = languages.en;
console.log(ja); // => "日本語"
console.log(en); // => "英語"

このようにオブジェクトのプロパティを変数として定義し直すときには、分割代入(Destructuring assignment)が利用できる

const languages = {
    ja: "日本語",
    en: "英語"
};
const { ja, en } = languages;
console.log(ja); // => "日本語"
console.log(en); // => "英語"

プロパティの追加

// 空のオブジェクト
const obj = {};
// `key`プロパティを追加して値を代入
obj.key = "value";
console.log(obj.key); // => "value"

ブラケット記法はobject[式]の式の評価結果を文字列にしたものをプロパティ名として利用できる

const key = "key-string";
const obj = {};
// `key`の評価結果 "key-string" をプロパティ名に利用
obj[key] = "value of key";
// 取り出すときも同じく`key`変数を利用
console.log(obj[key]); // => "value of key"

コード解説

オブジェクト obj"key-string" という名前のプロパティを新しく作って、そこに "value of key" を入れてるという意味

const key = "key-string";
const obj = {};

key には文字列 "key-string" が入ってる
obj は空のオブジェクト {}

obj[key] = "value of key";

key の中身は "key-string" だから、これは実質こう

obj["key-string"] = "value of key";

つまり obj はこうなる

{ "key-string": "value of key" }
console.log(obj[key]);

これも実質こう

console.log(obj["key-string"]);

ブラケット記法を用いたプロパティ定義は、オブジェクトリテラルの中でも利用できる

const key = "key-string";
// Computed Propertyで`key`の評価結果 "key-string" をプロパティ名に利用
const obj = {
    [key]: "value"
};
console.log(obj[key]); // => "value"

プロパティの削除

オブジェクトのプロパティを削除するにはdelete演算子を利用する、削除したいプロパティをdelete演算子の右辺に指定して、プロパティを削除できる

const obj = {
    key1: "value1",
    key2: "value2"
};
// key1プロパティを削除
delete obj.key1;
// key1プロパティが削除されている
console.log(obj); // => { "key2": "value2" }

constで定義したオブジェクトは変更可能
先ほどのコード例で、constで宣言したオブジェクトのプロパティがエラーなく変更できていることがわかる
次のコードを実行してみると、値であるオブジェクトのプロパティが変更できていることがわかる

const obj = { key: "value" };
obj.key = "Hi!"; // constで定義したオブジェクト(`obj`)が変更できる
console.log(obj.key); // => "Hi!"

JavaScriptのconstは値を固定するのではなく、変数への再代入を防ぐためのもの

作成したオブジェクトのプロパティの変更を防止するにはObject.freeze静的メソッドを利用する

"use strict";
const object = Object.freeze({ key: "value" });
// freezeしたオブジェクトにプロパティを追加や変更できない
object.key = "value"; // => TypeError: "key" is read-only

JavaScriptでは存在しないプロパティへアクセスした場合に例外が発生しない
プロパティ名を間違えた場合に単にundefinedという値を返すため、間違いに気づきにくいという問題がある

const widget = {
    window: {
        title: "ウィジェットのタイトル"
    }
};
// `window`を`windw`と間違えているが、例外は発生しない
console.log(widget.windw); // => undefined
// さらにネストした場合に、例外が発生する
// `undefined.title`と書いたのと同じ意味となるため
console.log(widget.windw.title); // => TypeError: widget.windw is undefined
// 例外が発生した文以降は実行されません

プロパティを持っているかを確認する方法として、次の4つがある

  • undefinedとの比較
  • in演算子
  • Object.hasOwn静的メソッド
  • Object.prototype.hasOwnPropertyメソッド

プロパティの存在確認: undefinedとの比較

存在しないプロパティへアクセスした場合にundefinedを返すため、実際にプロパティアクセスすることでも判定できる
次のコードで、keyプロパティの値がundefinedではないという条件式で、プロパティが存在するかを判定している

const obj = {
    key: "value"
};
// `key`プロパティが`undefined`ではないなら、プロパティが存在する?
if (obj.key !== undefined) {
    // `key`プロパティが存在する?ときの処理
    console.log("`key`プロパティの値は`undefined`ではない");
}

しかし、この方法はプロパティの値がundefinedであった場合に、プロパティそのものが存在するかを区別できないという問題がある

プロパティが存在するかを判定するにはin演算子かObject.hasOwn静的メソッドを利用する

プロパティの存在確認: in演算子を使う

in演算子は、指定したオブジェクト上に指定したプロパティがあるかを判定し真偽値を返す

"プロパティ名" in オブジェクト; // true or false

次のコードではobjにkeyプロパティが存在するかを判定
in演算子は、プロパティの値は関係なく、プロパティが存在した場合にtrueを返す

const obj = { key: undefined };
// `key`プロパティを持っているならtrue
if ("key" in obj) {
    console.log("`key`プロパティは存在する");
}

プロパティの存在確認: Object.hasOwn静的メソッド

Object.hasOwn静的メソッドは、対象のオブジェクトが指定したプロパティを持っているかを判定できる

const obj = {};
// objが"プロパティ名"を持っているかを確認する
Object.hasOwn(obj, "プロパティ名"); // true or false

Optional chaining演算子(?.)

深い(ネストした)プロパティに安全にアクセスするための仕組み

何が問題だったのか

widget.window.title みたいにネストしているとき、widget.window が 存在しない(undefined) 可能性がある。その状態で widget.window.title を読もうとすると、実質こうなる

undefined.title

これは 例外(TypeError) になる。
だから昔はこうやって「順番に存在確認」してた:

if (widget.window !== undefined && widget.window.title !== undefined) {
  ...
}

でも、ネストが深くなるほど && が増えて冗長になる。

そこでOptional chaining(?.)を使う

「左側が null または undefined なら、その時点で止めて undefined を返す」 演算子。

ドット . の代わりに ?. を使う。

obj?.a?.b

挙動のイメージ

obj が null/undefined → そこで止まって undefined
obj はあるけど obj.a が undefined → そこで止まって undefined
obj.a.b まで存在する → その値を返す

つまり 存在しないプロパティを読んでも落ちない(例外にならない)。

?. と ?? の組み合わせが気持ちいい理由

?. は安全にアクセスできても、結果は undefined になることがある。

そこで ??(Nullish coalescing)を使うと、

左が null or undefined なら右を採用

それ以外(空文字 "" や 0 はそのまま)なら左を採用

const title = widget?.window?.title ?? "未定義";

toStringメソッド

オブジェクトのtoStringメソッドは、オブジェクト自身を文字列化するメソッド

const obj = { key: "value" };
console.log(obj.toString()); // => "[object Object]"
// `String`コンストラクタ関数は`toString`メソッドを呼んでいる
console.log(String(obj)); // => "[object Object]"

オブジェクトの列挙

最初に紹介したように、オブジェクトはプロパティの集合です。 そのオブジェクトのプロパティを列挙する方法として、次の3つの静的メソッド

Object.keys静的メソッド: オブジェクトのプロパティ名の配列にして返す
Object.values静的メソッド: オブジェクトの値の配列にして返す
Object.entries静的メソッド: オブジェクトのプロパティ名と値の配列の配列を返す

const obj = {
    "one": 1,
    "two": 2,
    "three": 3
};
// `Object.keys`はキーを列挙した配列を返す
console.log(Object.keys(obj)); // => ["one", "two", "three"]
// `Object.values`は値を列挙した配列を返す
console.log(Object.values(obj)); // => [1, 2, 3]
// `Object.entries`は[キー, 値]の配列を返す
console.log(Object.entries(obj)); // => [["one", 1], ["two", 2], ["three", 3]]

これらのプロパティを列挙する静的メソッドと配列のforEachメソッドなどを組み合わせれば、プロパティに対して反復処理ができる

const obj = {
    "one": 1,
    "two": 2,
    "three": 3
};
const keys = Object.keys(obj);
keys.forEach(key => {
    console.log(key);
});
// 次の値が順番に出力される
// "one"
// "two"
// "three"

オブジェクトのマージと複製

Object.assign静的メソッド[ES2015]は、あるオブジェクトを別のオブジェクトに代入(assign)できる

Object.assign静的メソッドは、targetオブジェクトに対して、1つ以上のsourcesオブジェクトを指定する。 sourcesオブジェクト自身が持つ列挙可能なプロパティを第一引数のtargetオブジェクトに対してコピーする、Object.assign静的メソッドの返り値は、targetオブジェクトになる

const obj = Object.assign(target, ...sources);

オブジェクトのマージ

const objectA = { a: "a" };
const objectB = { b: "b" };
const merged = Object.assign({}, objectA, objectB);
console.log(merged); // => { a: "a", b: "b" }

第一引数には空のオブジェクトではなく、既存のオブジェクトも指定できます。 第一引数に既存のオブジェクトを指定した場合は、そのオブジェクトのプロパティが変更

const objectA = { a: "a" };
const objectB = { b: "b" };
const merged = Object.assign(objectA, objectB);
console.log(merged); // => { a: "a", b: "b" }
// `objectA`が変更されている
console.log(objectA); // => { a: "a", b: "b" }
console.log(merged === objectA); // => true

プロパティ名が重複した時の処理

// `version`のプロパティ名が被っている
const objectA = { version: "a" };
const objectB = { version: "b" };
const merged = Object.assign({}, objectA, objectB);
// 後ろにある`objectB`のプロパティで上書きされる
console.log(merged); // => { version: "b" }

オブジェクトのspread構文でのマージ

オブジェクトのspread構文は、Object.assignとは異なり必ず新しいオブジェクトを作成

const objectA = { a: "a" };
const objectB = { b: "b" };
const merged = {
    ...objectA,
    ...objectB
};
console.log(merged); // => { a: "a", b: "b" }

プロパティ名が被った場合の優先順位は、後ろにあるオブジェクトが優先される

オブジェクトの複製

// 引数の`obj`を浅く複製したオブジェクトを返す
const shallowClone = (obj) => {
    return Object.assign({}, obj);
};
const obj = { a: "a" };
const cloneObj = shallowClone(obj);
console.log(cloneObj); // => { a: "a" }
// オブジェクトを複製しているので、異なるオブジェクトとなる
console.log(obj === cloneObj); // => false

注意点として、Object.assign静的メソッドはsourcesオブジェクトのプロパティを浅くコピー(shallow copy)する点、shallow copyとは、sourcesオブジェクトの直下にあるプロパティだけをコピーするということ
そのプロパティの値がオブジェクトである場合に、ネストした先のオブジェクトまでも複製するわけではない

const shallowClone = (obj) => {
    return Object.assign({}, obj);
};
const obj = {
    level: 1,
    nest: {
        level: 2
    },
};
const cloneObj = shallowClone(obj);
// `nest`プロパティのオブジェクトは同じオブジェクトのままになる 
console.log(cloneObj.nest === obj.nest); // => true

逆にプロパティの値までも再帰的に複製してコピーすることを深いコピーdeep copyと呼ぶdeep copyは再帰的にshallow copyすることで実現する

// 引数の`obj`を浅く複製したオブジェクトを返す
const shallowClone = (obj) => {
    return Object.assign({}, obj);
};
// 引数の`obj`を深く複製したオブジェクトを返す
function deepClone(obj) {
    const newObj = shallowClone(obj);
    // プロパティがオブジェクト型であるなら、再帰的に複製する
    Object.keys(newObj)
        .filter(k => typeof newObj[k] === "object")
        .forEach(k => newObj[k] = deepClone(newObj[k]));
    return newObj;
}
const obj = {
    level: 1,
    nest: {
        level: 2
    }
};
const cloneObj = deepClone(obj);
// `nest`オブジェクトも再帰的に複製されている
console.log(cloneObj.nest === obj.nest); // => false

プロトタイプオブジェクト

Objectはすべての元

Objectには、他のArray、String、Functionなどのオブジェクトとは異なる特徴がある
それは、他のオブジェクトはすべてObjectを継承しているという点

// `Object.prototype`オブジェクトに`toString`メソッドの定義がある
console.log(typeof Object.prototype.toString); // => "function"
const obj = {
    "key": "value"
};
// `obj`インスタンスは`Object.prototype`に定義されたものを継承する
// `obj.toString`は継承した`Object.prototype.toString`を参照している
console.log(obj.toString === Object.prototype.toString); // => true
// インスタンスからプロトタイプメソッドを呼び出せる
console.log(obj.toString()); // => "[object Object]"

このインスタンスからprototypeオブジェクト上に定義されたメソッドを参照できる仕組みをプロトタイプチェーンという

プロトタイプメソッドとインスタンスメソッドの優先順位

プロトタイプメソッドよりも優先してインスタンスのメソッドが呼び出されている

// オブジェクトのインスタンスにtoStringメソッドを定義
const customObject = {
    toString() {
        return "custom value";
    }
};
console.log(customObject.toString()); // => "custom value"

Object.hasOwn静的メソッドとin演算子との違い

Object.hasOwn静的メソッドは、指定したオブジェクト自体が指定したプロパティを持っているかを判定する
一方、in演算子はオブジェクト自身が持っていなければ、そのオブジェクトの継承元であるprototypeオブジェクトまで探索して持っているかを判定する

const obj = {};
// `obj`というオブジェクト自体に`toString`メソッドが定義されているわけではない
console.log(Object.hasOwn(obj, "toString")); // => false
// `in`演算子は指定されたプロパティ名が見つかるまで親をたどるため、`Object.prototype`まで見にいく
console.log("toString" in obj); // => true

オブジェクトの継承元を明示するObject.create静的メソッド

Object.create静的メソッドを使うと、第一引数に指定したprototypeオブジェクトを継承した新しいオブジェクトを作成できる

// const obj = {} と同じ意味
const obj = Object.create(Object.prototype);
// `obj`は`Object.prototype`を継承している
// そのため、`obj.toString`と`Object.prototype.toString`は同じとなる
console.log(obj.toString === Object.prototype.toString); // => true

ArrayもObjectを継承している

ビルトインオブジェクトArrayもArray.prototypeを持っているのと同じように、配列(Array)のインスタンスはArray.prototypeを継承する
さらに、Array.prototypeはObject.prototypeを継承しているため、ArrayのインスタンスはObject.prototypeも継承している

Arrayのインスタンス → Array.prototype → Object.prototype

Object.prototypeはすべてのオブジェクトの親となるオブジェクトであること

ArrayのインスタンスでtoStringメソッドを呼び出すとArray.prototype.toStringが優先して呼び出される

const numbers = [1, 2, 3];
// `Array.prototype.toString`が定義されているため、`Object.prototype.toString`とは異なる出力形式となる
console.log(numbers.toString()); // => "1,2,3"

Object.prototypeを継承しないオブジェクト

Object.create(null)とすることでObject.prototypeを継承しないオブジェクトを作成できる

// 親がnull、つまり親がいないオブジェクトを作る
const obj = Object.create(null);
// Object.prototypeを継承しないため、hasOwnPropertyが存在しない
console.log(obj.hasOwnProperty); // => undefined

配列

配列の作成とアクセス

const emptyArray = [];
const numbers = [1, 2, 3];
// 2次元配列(配列の配列)
const matrix = [
    ["a", "b"],
    ["c", "d"]
];

2次元配列(配列の配列)からの値の読み取りも同様に配列[インデックス]でアクセスできる

// 2次元配列(配列の配列)
const matrix = [
    ["a", "b"],
    ["c", "d"]
];
console.log(matrix[0][0]); // => "a"

配列のlengthプロパティは配列の要素の数を返す

const array = ["one", "two", "three"];
console.log(array.length); // => 3
// 配列の要素数 - 1 が 最後の要素のインデックスとなる
console.log(array[array.length - 1]); // => "three"

一方、存在しないインデックスにアクセスした場合はどうなるか? JavaScriptでは、存在しないインデックスに対してアクセスした場合に、例外ではなくundefinedを返す

const array = ["one", "two", "three"];
// `array`にはインデックスが100の要素は定義されていない
console.log(array[100]); // => undefined

配列の中に隙間があるものを疎な配列と呼ぶ
一方、隙間がなくすべてのインデックスに要素がある配列を密な配列と呼ぶ

// 未定義の箇所が1つ含まれる疎な配列
// インデックスが1の値を省略しているので、カンマが2つ続いていることに注意
const sparseArray = [1, , 3];
console.log(sparseArray.length); // => 3
// 1番目の要素は存在しないため undefined が返る
console.log(sparseArray[1]); // => undefined

Array.prototype.at

相対的なインデックスの値を指定して配列の要素へアクセスできる

const array = ["a", "b", "c"];
//
console.log(array.at(0)); // => "a"
console.log(array.at(1)); // => "b"
// 後ろから1つ目の要素にアクセス
console.log(array.at(-1)); // => "c"
// -1は、次のように書いた場合と同じ結果
console.log(array[array.length - 1]); // => "c"

配列[インデックス]のインデックスに-1を指定すると、配列オブジェクトの"-1"というプロパティ名へのアクセスとなる
そのため配列[-1]と書くと、大抵の場合はundefinedが返される

const array = ["a", "b", "c"];
console.log(array[-1]); // => undefined

オブジェクトが配列かどうかを判定する

あるオブジェクトが配列かどうかを判定するにはArray.isArray静的メソッドを利用する
Array.isArray静的メソッドは引数が配列ならばtrueを返す

const obj = {};
const array = [];
console.log(Array.isArray(obj)); // => false
console.log(Array.isArray(array)); // => true

typeof演算子では配列かどうかを判定することはできない
配列もオブジェクトの一種であるため、typeof演算子の結果が"object"となる

配列と分割代入

配列の分割代入では、左辺に配列リテラルのような構文で定義したい変数名を書く
右辺の配列から対応するインデックスの要素が、左辺で定義した変数に代入される

const array = ["one", "two", "three"];
const [first, second, third] = array;
console.log(first);  // => "one"
console.log(second); // => "two"
console.log(third);  // => "three"

配列から要素を検索

配列から指定した要素を検索する目的には、 主に次の3つがある

その要素のインデックスが欲しい場合
その要素自体が欲しい場合
その要素が含まれているかという真偽値が欲しい場合

配列にはそれぞれに対応したメソッドが用意されている

インデックスを取得

ArrayのindexOfメソッドを利用して、配列の中から"JavaScript"という文字列のインデックスを取得する
indexOfメソッドは引数と厳密等価演算子(===)で一致する要素を先頭から検索して該当する要素のインデックスを返し、該当する要素がない場合は-1を返す
indexOfメソッドには対となるlastIndexOfメソッドがあり、lastIndexOfメソッドでは末尾から検索した結果が得られる

const array = ["Java", "JavaScript", "Ruby", "JavaScript"];
// 先頭から探索して最初に見つかった"JavaScript"のインデックスを取得
const indexOfJS = array.indexOf("JavaScript");
console.log(indexOfJS); // => 1
// 末尾から探索して最初に見つかった"JavaScript"のインデックスを取得
const lastIndexOfJS = array.lastIndexOf("JavaScript");
console.log(lastIndexOfJS); // => 3
console.log(array[indexOfJS]); // => "JavaScript"
console.log(array[lastIndexOfJS]); // => "JavaScript"
// "JS" という要素はないため `-1` が返される
console.log(array.indexOf("JS")); // => -1
console.log(array.lastIndexOf("JS")); // => -1

indexOfメソッドは配列からプリミティブな要素を発見できるが、オブジェクトは持っているプロパティが同じでも別オブジェクトだと異なるものとして扱われる

const obj = { key: "value" };
const array = ["A", "B", obj];
console.log(array.indexOf({ key: "value" })); // => -1
// リテラルは新しいオブジェクトを作るため、異なるオブジェクトだと判定される
console.log(obj === { key: "value" }); // => false
// 等価のオブジェクトを検索してインデックスを返す
console.log(array.indexOf(obj)); // => 2

異なるオブジェクトだが値は同じものを見つけたい場合には、ArrayのfindIndexメソッドが利用できる
findIndexメソッドの引数には配列の各要素をテストする関数をコールバック関数として渡す

// colorプロパティを持つオブジェクトの配列
const colors = [
    { "color": "red" },
    { "color": "green" },
    { "color": "blue" }
];
// `color`プロパティが"blue"のオブジェクトのインデックスを取得
const indexOfBlue = colors.findIndex((obj) => {
    return obj.color === "blue";
});
console.log(indexOfBlue); // => 2
console.log(colors[indexOfBlue]); // => { "color": "blue" }

findIndexメソッドでインデックスを取得し、そのインデックスで配列へアクセスすればよいだけ

条件に一致する要素を取得

しかし、findIndexメソッドを使って要素を取得するケースでは、 そのインデックスが欲しいのか、またはその要素自体が欲しいのかがコードとして明確ではない

より明確に要素自体が欲しいということを表現するには、Arrayのfindメソッドが使える
findメソッドには、findIndexメソッドと同様にテストする関数をコールバック関数として渡す
findメソッドの返り値は、要素そのものとなり、要素が存在しない場合はundefinedを返す

// colorプロパティを持つオブジェクトの配列
const colors = [
    { "color": "red" },
    { "color": "green" },
    { "color": "blue" }
];
// `color`プロパティが"blue"のオブジェクトを取得
const blueColor = colors.find((obj) => {
    return obj.color === "blue";
});
console.log(blueColor); // => { "color": "blue" }
// 該当する要素がない場合は`undefined`を返す
const whiteColor = colors.find((obj) => {
    return obj.color === "white";
});
console.log(whiteColor); // => undefined

findメソッドにも対となるfindLastメソッドがあり、findLastメソッドは末尾から検索した結果が得られる

// dateとcountプロパティを持つオブジェクトの配列
const records = [
    { date: "2020/12/1", count: 5 },
    { date: "2020/12/2", count: 11 },
    { date: "2020/12/3", count: 9 },
    { date: "2020/12/4", count: 12 },
    { date: "2020/12/5", count: 3 }
];
// 10より大きい`count`プロパティを持つ最初のオブジェクトを取得
const firstRecord = records.find((record) => {
    return record.count > 10;
});
// 10より大きい`count`プロパティを持つ最後のオブジェクトを取得
const lastRecord = records.findLast((record) => {
    return record.count > 10;
});
console.log(firstRecord); // => { date: "2020/12/2", count: 11 }
console.log(lastRecord); // => { date: "2020/12/4", count: 12 }

指定範囲の要素を取得

配列から指定範囲の要素を取り出す方法としてArrayのsliceメソッドが利用できる
sliceメソッドは、第一引数の開始位置から第二引数の終了位置(終了位置の要素は含まない)までの範囲を取り出した新しい配列を返す

const array = ["A", "B", "C", "D", "E"];
// インデックス1から4まで(4の要素は含まない)の範囲を取り出す
console.log(array.slice(1, 4)); // => ["B", "C", "D"]
// 第二引数を省略した場合は、第一引数から末尾の要素までを取り出す
console.log(array.slice(1)); // => ["B", "C", "D", "E"]
// マイナスを指定すると後ろから数えた位置となる
console.log(array.slice(-1)); // => ["E"]
// 第一引数と第二引数が同じ場合は、空の配列を返す
console.log(array.slice(1, 1)); // => []
// 第一引数 > 第二引数の場合、常に空配列を返す
console.log(array.slice(4, 1)); // => []

真偽値を取得

ArrayのindexOfメソッドを利用し、該当する要素が含まれているかを判定
indexOfメソッドの結果をindexOfJSに代入していますが、含まれているかを判定する以外には利用していない

const array = ["Java", "JavaScript", "Ruby"];
// `indexOf`メソッドは含まれていないときのみ`-1`を返すことを利用
const indexOfJS = array.indexOf("JavaScript");
if (indexOfJS !== -1) {
    console.log("配列にJavaScriptが含まれている");
    // ... いろいろな処理 ...
    // `indexOfJS`は、含まれているのかの判定以外には利用してない
}

Arrayのincludesメソッドを利用

const array = ["Java", "JavaScript", "Ruby"];
// `includes`は含まれているなら`true`を返す
if (array.includes("JavaScript")) {
    console.log("配列にJavaScriptが含まれている");
}

Arrayのfindメソッドのようにテストするコールバック関数を利用して真偽値を得るには、Arrayのsomeメソッド

// colorプロパティを持つオブジェクトの配列
const colors = [
   { "color": "red" },
   { "color": "green" },
   { "color": "blue" }
];
// `color`プロパティが"blue"のオブジェクトがあるかどうか
const isIncludedBlueColor = colors.some((obj) => {
   return obj.color === "blue";
});
console.log(isIncludedBlueColor); // => true

追加と削除

要素を配列の末尾へ追加するにはArrayのpushが利用できる
一方、末尾から要素を削除するにはArrayのpopが利用できる

const array = ["A", "B", "C"];
array.push("D"); // "D"を末尾に追加
console.log(array); // => ["A", "B", "C", "D"]
const poppedItem = array.pop(); // 最末尾の要素を削除し、その要素を返す
console.log(poppedItem); // => "D"
console.log(array); // => ["A", "B", "C"]

要素を配列の先頭へ追加するにはArrayのunshiftが利用できる
一方、配列の先頭から要素を削除するにはArrayのshiftが利用できる

const array = ["A", "B", "C"];
array.unshift("S"); // "S"を先頭に追加
console.log(array); // => ["S", "A", "B", "C"]
const shiftedItem = array.shift(); // 先頭の要素を削除
console.log(shiftedItem); // => "S"
console.log(array); // => ["A", "B", "C"]

配列同士を結合

Arrayのconcatメソッドを使うことで配列と配列を結合した新しい配列を作成

const array = ["A", "B", "C"];
const newArray = array.concat(["D", "E"]);
console.log(newArray); // => ["A", "B", "C", "D", "E"]

concatメソッドは配列だけではなく任意の値を要素として結合できる

const array = ["A", "B", "C"];
const newArray = array.concat("新しい要素");
console.log(newArray); // => ["A", "B", "C", "新しい要素"]

配列の展開

...(Spread構文)を使うことで、配列リテラル中に既存の配列を展開できる
配列リテラルの末尾に配列を展開してる
これは、Arrayのconcatメソッドで配列同士を結合するのと同じ結果になる

const array = ["A", "B", "C"];
// Spread構文を使った場合
const newArray = ["X", "Y", "Z", ...array];
// concatメソッドの場合
const newArrayConcat = ["X", "Y", "Z"].concat(array);
console.log(newArray); // => ["X", "Y", "Z", "A", "B", "C"]
console.log(newArrayConcat); // => ["X", "Y", "Z", "A", "B", "C"]

Spread構文は、concatメソッドとは異なり、配列リテラル中の任意の位置に配列を展開できる

const array = ["A", "B", "C"];
const newArray = ["X", ...array, "Z"];
console.log(newArray); // => ["X", "A", "B", "C", "Z"]

配列から要素を削除

Array.prototype.splice

配列の任意のインデックスの要素を削除するにはArrayのspliceメソッドを利用できる

spliceメソッドを利用すると、削除した要素を自動で詰めることができる
spliceメソッドは指定したインデックスから、指定した数だけ要素を取り除き、必要ならば要素を同時に追加できる

const array = [];
array.splice(インデックス, 削除する要素数);
// 削除と同時に要素の追加もできる
array.splice(インデックス, 削除する要素数, ...追加する要素);
const array = ["a", "b", "c"];
// 1番目から1つの要素("b")を削除
array.splice(1, 1);
console.log(array); // => ["a", "c"]
console.log(array.length); // => 2
console.log(array[1]); // => "c"
// すべて削除
array.splice(0, array.length);
console.log(array.length); // => 0

lengthプロパティへの代入

配列のすべての要素を削除することはArrayのspliceで行えますが、 配列のlengthプロパティへの代入を利用した方法もある

const array = [1, 2, 3];
array.length = 0; // 配列を空にする
console.log(array); // => []

空の配列を代入

その配列の要素を削除するのではなく、新しい空の配列を変数へ代入する方法

let array = [1, 2, 3];
console.log(array.length); // => 3
// 新しい配列で変数を上書き
array = [];
console.log(array.length); // => 0

破壊的なメソッドと非破壊的なメソッド

破壊的なメソッド(Mutable Method)とは、配列オブジェクトそのものを変更し、変更した配列または変更箇所を返すメソッド
非破壊的メソッド(Immutable Method)とは、配列オブジェクトのコピーを作成してから変更し、そのコピーした配列を返すメソッド

破壊的メソッド例
const myArray = ["A", "B", "C"];
const result = myArray.push("D");
// `push`の返り値は配列ではなく、追加後の配列のlength
console.log(result); // => 4
// `myArray`が参照する配列そのものが変更されている
console.log(myArray); // => ["A", "B", "C", "D"]
const myArray = ["A", "B", "C"];
// `concat`の返り値は結合済みの新しい配列
const newArray = myArray.concat("D");
console.log(newArray); // => ["A", "B", "C", "D"]
// `myArray`は変更されていない
console.log(myArray); // => ["A", "B", "C"]
// `newArray`と`myArray`は異なる配列オブジェクト
console.log(myArray === newArray); // => false

破壊的メソッド

メソッド名	返り値
Array.prototype.pop	配列の末尾の値
Array.prototype.push	変更後の配列のlength
Array.prototype.splice	取り除かれた要素を含む配列
Array.prototype.reverse	反転した配列
Array.prototype.shift	配列の先頭の値
Array.prototype.sort	ソートした配列
Array.prototype.unshift	変更後の配列のlength
Array.prototype.copyWithin[ES2015]	変更後の配列
Array.prototype.fill[ES2015]	変更後の配列

JavaScriptにはcopyメソッドそのものは存在しないが、配列をコピーする方法としてArrayのsliceメソッドとconcatメソッドが利用される
sliceメソッドとconcatメソッドは引数なしで呼び出すと、その配列のコピーを返す

const myArray = ["A", "B", "C"];
// `slice`は`myArray`のコピーを返す - `myArray.concat()`でも同じ
const copiedArray = myArray.slice();
myArray.push("D");
console.log(myArray); // => ["A", "B", "C", "D"]
// `array`のコピーである`copiedArray`には影響がない
console.log(copiedArray); // => ["A", "B", "C"]
// コピーであるため参照は異なる
console.log(copiedArray === myArray); // => false

今まで、破壊的なメソッドしかなかった、splice、reverse、sortに対して、 非破壊的なバージョンであるtoSpliced、toReversed、toSortedが追加された

const array = ["A", "B", "C"];
// `toSpliced`は`array`を複製してから変更する
const newArray = array.toSpliced(1, 1);
console.log(newArray); // => ["A", "C"]
// コピー元の`array`には影響がない
console.log(array); // => ["A", "B", "C"]

配列の指定したインデックスの要素を非破壊的に変更するwithメソッドも追加された、array[index] = valueの代入処理は、元々の配列を変更する破壊的な処理
これに対してwithメソッドは、配列を複製してから指定したインデックスの要素を変更した配列を返す非破壊的なメソッド

const array = ["A", "B", "C"];
// `array`の1番目の要素を変更した配列を返す
const newArray = array.with(1, "B2");
console.log(newArray); // => ["A", "B2", "C"]

破壊的な方法と非破壊的な方法のまとめ

破壊的な方法	非破壊な方法
array[index] = item	Array.prototype.with[ES2023]
Array.prototype.pop	array.slice(0, -1)とarray.at(-1)[ES2022]
Array.prototype.push	[...array, item][ES2015]
Array.prototype.splice	Array.prototype.toSpliced[ES2023]
Array.prototype.reverse	Array.prototype.toReversed[ES2023]
Array.prototype.sort	Array.prototype.toSorted[ES2023]
Array.prototype.shift	array.slice(1)とarray.at(0)[ES2022]
Array.prototype.unshift	[item, ...array][ES2015]
Array.prototype.copyWithin[ES2015]	なし
Array.prototype.fill[ES2015]	なし

配列を反復処理するメソッド

Array.prototype.forEach

ArrayのforEachメソッドは配列の要素を先頭から順番にコールバック関数へ渡し、反復処理を行うメソッド

const array = [1, 2, 3];
array.forEach((currentValue, index, array) => {
    console.log(currentValue, index, array);
});
// コンソールの出力
// 1, 0, [1, 2, 3]
// 2, 1, [1, 2, 3]
// 3, 2, [1, 2, 3]

Array.prototype.map

Arrayのmapメソッドは配列の要素を順番にコールバック関数へ渡し、コールバック関数が返した値から新しい配列を返す非破壊的なメソッド

// 各要素に10を乗算した新しい配列を作成する
const newArray = array.map((currentValue, index, array) => {
    return currentValue * 10;
});
console.log(newArray); // => [10, 20, 30]
// 元の配列とは異なるインスタンス
console.log(array === newArray); // => false

Array.prototype.filter

Arrayのfilterメソッドは配列の要素を順番にコールバック関数へ渡し、コールバック関数がtrueを返した要素だけを集めた新しい配列を返す非破壊的なメソッド、配列から不要な要素を取り除いた配列を作成したい場合に利用

const array = [1, 2, 3];
// 奇数の値を持つ要素だけを集めた配列を返す
const newArray = array.filter((currentValue, index, array) => {
    return currentValue % 2 === 1;
});
console.log(newArray); // => [1, 3]
// 元の配列とは異なるインスタンス
console.log(array === newArray); // => false

Array.prototype.reduce

Arrayのreduceメソッドは累積値(アキュムレータ)と配列の要素を順番にコールバック関数へ渡し、1つの累積値を返す
配列から配列以外を含む任意の値を作成したい場合に利用、reduceメソッドの第二引数には累積値の初期値となる値を渡せる

const array = [1, 2, 3];
// すべての要素を加算した値を返す
// accumulatorの初期値は`0`
const totalValue = array.reduce((accumulator, currentValue, index, array) => {
    return accumulator + currentValue;
}, 0);
// 0 + 1 + 2 + 3という式の結果が返り値になる
console.log(totalValue); // => 6

reduceメソッドに渡したコールバック関数は配列の要素数である3回呼び出され、それぞれ次のような結果になる

accumulator	currentValue	returnした値
1回目の呼び出し	0	1	0 + 1
2回目の呼び出し	1	2	1 + 2
3回目の呼び出し	3	3	3 + 3

配列の数値の合計をforEachメソッドなど反復処理で計算すると、次のコードのようにtotalValueという変数は再代入ができるletで宣言する必要がある

const array = [1, 2, 3];
// 初期値は`0`
let totalValue = 0;
array.forEach((currentValue) => {
    totalValue += currentValue;
});
console.log(totalValue); // => 6

letで宣言した変数は再代入が可能なため、意図しない箇所で変数の値が変更され、バグの原因となることがある
そのため、できる限り変数をconstで宣言したい場合にはreduceメソッドは有用
一方で、reduceメソッドは可読性があまりよくないため、コードの意図が伝わりにくいというデメリットもある

const array = [1, 2, 3];
function sum(array) {
    return array.reduce((accumulator, currentValue) => {
        return accumulator + currentValue;
    }, 0);
}
console.log(sum(array)); // => 6

Array.prototype.flatメソッド

Arrayのflatメソッド[ES2019]を使うことで、多次元配列をフラットな配列に変換できる

const array = [[["A"], "B"], "C"];
// 引数なしは1を指定した場合と同じ
console.log(array.flat()); // => [["A"], "B", "C"]
console.log(array.flat(1)); // => [["A"], "B", "C"]
console.log(array.flat(2)); // => ["A", "B", "C"]
// すべてをフラット化するにはInfinityを渡す
console.log(array.flat(Infinity)); // => ["A", "B", "C"]

Arrayのflatメソッドは必ず新しい配列を作成して返すメソッド
そのため、これ以上フラット化できない配列をフラット化しても、同じ要素を持つ新しい配列を返す

const array = ["A", "B", "C"];
console.log(array.flat()); // => ["A", "B", "C"]

メソッドチェーンと高階関数

メソッドチェーンとは、メソッドを呼び出した返り値に対してさらにメソッド呼び出しをするパターンのこと

const array = ["a"].concat("b").concat("c");
console.log(array); // => ["a", "b", "c"]

concatメソッドの返り値は結合した新しい配列
先ほどのメソッドチェーンでは、その新しい配列に対してさらにconcatメソッドで値を結合しているということ

// メソッドチェーンを分解した例
// 一時的な`abArray`という変数が増えている
const abArray = ["a"].concat("b");
console.log(abArray); // => ["a", "b"]
const abcArray = abArray.concat("c");
console.log(abcArray); // => ["a", "b", "c"]

配列から2000年以前に発行されたECMAScriptのバージョン名の一覧を取り出すことを考える
目的の一覧を取り出すには「2000年以前のデータに絞り込む」と「データからnameを取り出す」という2つの加工処理を組み合わせる必要がある

// ECMAScriptのバージョン名と発行年
const ECMAScriptVersions = [
    { name: "ECMAScript 1", year: 1997 },
    { name: "ECMAScript 2", year: 1998 },
    { name: "ECMAScript 3", year: 1999 },
    { name: "ECMAScript 5", year: 2009 },
    { name: "ECMAScript 5.1", year: 2011 },
    { name: "ECMAScript 2015", year: 2015 },
    { name: "ECMAScript 2016", year: 2016 },
    { name: "ECMAScript 2017", year: 2017 },
];
// メソッドチェーンで必要な加工処理を並べている
const versionNames = ECMAScriptVersions
    // 2000年以下のデータに絞り込み
    .filter(ECMAScript => ECMAScript.year <= 2000)
    // それぞれの要素から`name`プロパティを取り出す
    .map(ECMAScript => ECMAScript.name);
console.log(versionNames); // => ["ECMAScript 1", "ECMAScript 2", "ECMAScript 3"]

文字列

文字列を作成する

文字列リテラルには"(ダブルクォート)、'(シングルクォート)、`(バッククォート)の3種類

const double = "文字列";
console.log(double); // => "文字列"
const single = '文字列';
console.log(single); // => '文字列'
// どちらも同じ文字列
console.log(double === single);// => true

エスケープシーケンス

文字列リテラル中にはそのままでは入力できない特殊な文字

エスケープシーケンス	意味
\'	シングルクォート
\"	ダブルクォート
\` 	バッククォート
\\	バックスラッシュ(\そのものを表示する)
\n	改行
\t	タブ
\uXXXX	Code Unit(\uと4桁のHexDigit)
\u{X} ... \u{XXXXXX}	Code Point(\u{}のカッコ中にHexDigit)

先ほどの"(ダブルクォート)の中に改行(\n)を入力できる

// 改行を\nのエスケープシーケンスとして入力している
const multiline = "1行目\n2行目\n3行目";
console.log(multiline); 
/* 改行した結果が出力される
1行目
2行目
3行目
*/

文字列を結合する

const str = "a" + "b";
console.log(str); // => "ab"

特定の書式に文字列を埋め込むには、テンプレートリテラルを使うとより宣言的に書ける

テンプレートリテラル中に${変数名}で書かれた変数は評価時に展開されます。 つまり、先ほどの文字列結合は次のように書ける

const name = "JavaScript";
console.log(`Hello ${name}!`);// => "Hello JavaScript!"

文字へのアクセス

const str = "文字列";
// 配列と同じようにインデックスでアクセスできる
console.log(str[0]); // => "文"
console.log(str[1]); // => "字"
console.log(str[2]); // => "列"

存在しないインデックスへのアクセスでは配列やオブジェクトと同じようにundefinedを返す

const str = "文字列";
// 42番目の要素は存在しない
console.log(str[42]); // => undefined

String.prototype.at

String.prototype.atメソッドが追加されている
Stringのatメソッドは、Arrayのatメソッドと同じく、相対的なインデックスを渡してその位置の文字へアクセスできる

const str = "文字列";
console.log(str.at(0)); // => "文"
console.log(str.at(1)); // => "字"
console.log(str.at(2)); // => "列"
console.log(str.at(-1)); // => "列"

文字列の分解と結合

文字列を配列へ分解するにはStringのsplitメソッドを利用でき、配列の要素を結合して文字列にするにはArrayのjoinメソッドを利用できる,
この2つを合わせれば、区切り文字を・から、へ変換する処理が可能

const strings = "赤・青・緑".split("");
console.log(strings); // => ["赤", "青", "緑"]

Stringのsplitメソッドの第一引数には正規表現も指定できる

// 文字間に1つ以上のスペースがある
const str = "a     b    c      d";
// 1つ以上のスペースにマッチして分解する
const strings = str.split(/\s+/);
console.log(strings); // => ["a", "b", "c", "d"]

文字列の長さ

文字列は3つの要素(Code Unit)が並んだものであるため、lengthプロパティは3を返す

console.log("文字列".length); // => 3

空文字列は要素数が0であるため、lengthプロパティの結果も0となる

console.log("".length); // => 0

文字列の比較

文字列の比較には===(厳密比較演算子)を利用する

console.log("文字列" === "文字列"); // => true
// 一致しなければfalseとなる
console.log("JS" === "ES"); // => false
// 文字列の長さが異なるのでfalseとなる
console.log("文字列" === "文字"); // => false

文字列の一部を取得

Stringのsliceメソッドやsubstringメソッドが利用できる

const str = "ABCDE";
console.log(str.slice(1)); // => "BCDE"
console.log(str.slice(1, 5)); // => "BCDE"
// マイナスを指定すると後ろからの位置となる
console.log(str.slice(-1)); // => "E"
// インデックスが1から4の範囲を取り出す
console.log(str.slice(1, 4)); // => "BCD"
// 第一引数 > 第二引数の場合、常に空文字列を返す
console.log(str.slice(4, 1)); // => ""

sliceメソッドと同じく第一引数に開始位置、第二引数に終了位置を指定し、その範囲を取り出して新しい文字列を返す
第二引数を省略した場合の挙動も同様で、省略した場合は文字列の末尾が終了位置となる

位置にマイナスの値を指定した場合は常に0として扱われる

const str = "ABCDE";
console.log(str.substring(1)); // => "BCDE"
console.log(str.substring(1, 5)); // => "BCDE"
// マイナスを指定すると0として扱われる
console.log(str.substring(-1)); // => "ABCDE"
// 位置:1から4の範囲を取り出す
console.log(str.substring(1, 4)); // => "BCD"
// 第一引数 > 第二引数の場合、引数が入れ替わる
// str.substring(1, 4)と同じ結果になる
console.log(str.substring(4, 1)); // => "BCD"

Stringのsliceメソッドは、indexOfメソッドなどの位置を取得するものと組み合わせて使うことが多い
次のコードでは、?の位置をindexOfメソッドで取得し、それ以降の文字列をsliceメソッドで切り出している

const url = "https://example.com?param=1";
const indexOfQuery = url.indexOf("?");
const queryString = url.slice(indexOfQuery);
console.log(queryString); // => "?param=1"

文字列の検索

文字列の検索方法として、大きく分けて文字列による検索と正規表現による検索がある

文字列によるインデックスの取得

StringのindexOfメソッドとlastIndexOfメソッドは、指定した文字列で検索し、その文字列が最初に現れたインデックスを返す
これらは配列のArrayのindexOfメソッドと同じで、厳密等価演算子(===)で文字列を検索する
一致する文字列がない場合は-1を返す

文字列.indexOf("検索文字列"): 先頭から検索し、指定された文字列が最初に現れたインデックスを返す
文字列.lastIndexOf("検索文字列"): 末尾から検索し、指定された文字列が最初に現れたインデックスを返す

// 検索対象となる文字列
const str = "にわにはにわにわとりがいる";
// indexOfは先頭から検索しインデックスを返す - "**にわ**にはにわにわとりがいる"
// "にわ"の先頭のインデックスを返すため 0 となる
console.log(str.indexOf("にわ")); // => 0
// lastIndexOfは末尾から検索しインデックスを返す- "にわにはにわ**にわ**とりがいる"
console.log(str.lastIndexOf("にわ")); // => 6
// 指定した文字列が見つからない場合は -1 を返す
console.log(str.indexOf("未知のキーワード")); // => -1

文字列にマッチした文字列の取得する

const str = "JavaScript";
const searchWord = "Script";
const index = str.indexOf(searchWord);
if (index !== -1) {
    console.log(`${searchWord}が見つかりました`);
} else {
    console.log(`${searchWord}は見つかりませんでした`);
}

真偽値の取得

String.prototype.startsWith(検索文字列)[ES2015]: 検索文字列が先頭にあるかの真偽値を返す
String.prototype.endsWith(検索文字列)[ES2015]: 検索文字列が末尾にあるかの真偽値を返す
String.prototype.includes(検索文字列)[ES2015]: 検索文字列を含むかの真偽値を返す

// 検索対象となる文字列
const str = "にわにはにわにわとりがいる";
// startsWith - 検索文字列が先頭ならtrue
console.log(str.startsWith("にわ")); // => true
console.log(str.startsWith("いる")); // => false
// endsWith - 検索文字列が末尾ならtrue
console.log(str.endsWith("にわ")); // => false
console.log(str.endsWith("いる")); // => true
// includes - 検索文字列が含まれるならtrue
console.log(str.includes("にわ")); // => true
console.log(str.includes("いる")); // => true

正規表現オブジェクト

一方で正規表現による検索では、あるパターン(規則性)にマッチするという柔軟な検索する

// 正規表現リテラルで正規表現オブジェクトを作成
const patternA = /パターン/フラグ;
// `RegExp`コンストラクタで正規表現オブジェクトを作成
const patternB = new RegExp("パターン文字列", "フラグ");

正規表現オブジェクトを作成するもうひとつの方法としてRegExpコンストラクタがある
RegExpコンストラクタでは、文字列から正規表現オブジェクトを作成できる

const pattern = new RegExp("a+");

RegExp.escapeメソッドが追加され、正規表現の特殊文字を安全にエスケープできる
RegExp.escapeメソッドを使うことで、文字列の中に正規表現として意味を持つ特殊文字が含まれていても、自動的にエスケープできる

const escaped = RegExp.escape("+");
console.log(escaped); // \+

正規表現による検索

正規表現によるインデックスの取得

StringのindexOfメソッドの正規表現版ともいえるStringのsearchメソッドがある
searchメソッドは正規表現のパターンにマッチした箇所のインデックスを返し、マッチする文字列がない場合は-1を返す

String.prototype.indexOf(検索文字列): 指定された文字列にマッチした箇所のインデックスを返す
String.prototype.search(/パターン/): 指定された正規表現のパターンにマッチした箇所のインデックスを返す

次のコードでは、数字が3つ連続しているかを検索し、該当した箇所のインデックスを返す
\dは、1文字の数字(0から9)にマッチする特殊文字

正規表現によるマッチした文字列の取得

const str = "abc123def";
// 連続した数字にマッチする正規表現
const searchPattern = /\d+/;
const index = str.search(searchPattern); // => 3
// `index` だけではマッチした文字列の長さがわからない
str.slice(index, index + マッチした文字列の長さ); // マッチした文字列は取得できない

マッチした文字列を取得するStringのmatchメソッドとmatchAllメソッドが用意されている
また、これらのメソッドは正規表現のマッチを文字列の最後まで繰り返すgフラグ

マッチした文字列の取得

"文字列".match(/パターン/);

matchメソッドで検索した結果、正規表現にマッチする文字列がなかった場合はnullを返す

console.log("文字列".match(/マッチしないパターン/)); // => null

matchメソッドは正規表現のgフラグなしのパターンで検索した場合、最初にマッチしたものが見つかった時点で検索が終了する

const str = "ABC あいう DE えお";
const alphabetsPattern = /[a-zA-Z]+/;
// gフラグなしでは、最初の結果のみを含んだ特殊な配列を返す
const results = str.match(alphabetsPattern);
console.log(results.length); // => 1
// マッチした文字列はインデックスでアクセスできる
console.log(results[0]); // => "ABC"
// マッチした文字列の先頭のインデックス
console.log(results.index); // => 0
// 検索対象となった文字列全体
console.log(results.input); // => "ABC あいう DE えお"

matchメソッドは正規表現のgフラグありのパターンで検索した場合、マッチしたすべての文字列を含んだ配列を返す

const str = "ABC あいう DE えお";
const alphabetsPattern = /[a-zA-Z]+/g;
// gフラグありでは、すべての検索結果を含む配列を返す
const resultsWithG = str.match(alphabetsPattern);
console.log(resultsWithG.length); // => 2
console.log(resultsWithG[0]); // => "ABC"
console.log(resultsWithG[1]); // => "DE"
// indexとinputはgフラグありの場合は追加されない
console.log(resultsWithG.index); // => undefined
console.log(resultsWithG.input); // => undefined

Stringのmatchメソッドの挙動

マッチしない場合は、nullを返す
マッチした場合は、マッチした文字列を含んだ特殊な配列を返す
正規表現のgフラグがある場合は、マッチしたすべての結果を含んだただの配列を返す

matchAllメソッドは、マッチした結果をIteratorで返す

const str = "ABC あいう DE えお";
const alphabetsPattern = /[a-zA-Z]+/g;
// matchAllはIteratorを返す
const matchesIterator = str.matchAll(alphabetsPattern);
for (const match of matchesIterator) {
    // マッチした要素ごとの情報を含んでいる
    console.log(`match: "${match[0]}", index: ${match.index}, input: "${match.input}"`);
}
// 次の順番でコンソールに出力される
// match: "ABC", index: 0, input: "ABC あいう DE えお"
// match: "DE", index: 8, input: "ABC あいう DE えお"

マッチした文字列の一部を取得

const [マッチした全体の文字列, キャプチャ1, キャプチャ2] = 文字列.match(/パターン(キャプチャ1)(キャプチャ2)/);

// "ECMAScript (数字+)"にマッチするが、欲しい文字列は数字の部分のみ
const pattern = /ECMAScript (\d+)/;
// 返り値は0番目がマッチした全体、1番目がキャプチャの1番目というように対応している
// [マッチした全部の文字列, キャプチャの1番目, キャプチャの2番目 ....]
const [all, capture1] = "ECMAScript 6".match(pattern);
console.log(all); // => "ECMAScript 6"
console.log(capture1); // => "6"

matchAllの例

// "ES(数字+)"にマッチするが、欲しい文字列は数字の部分のみ
const pattern = /ES(\d+)/g;
// iteratorを返す
const matchesIterator = "ES2015、ES2016、ES2017".matchAll(pattern);
for (const match of matchesIterator) {
    // マッチした要素ごとの情報を含んでいる
    // 0番目はマッチした文字列全体、1番目がキャプチャの1番目である数字
    console.log(`match: "${match[0]}", capture1: ${match[1]}, index: ${match.index}, input: "${match.input}"`);
}
// 次の順番でコンソールに出力される
// match: "ES2015", capture1: 2015, index: 0, input: "ES2015、ES2016、ES2017"
// match: "ES2016", capture1: 2016, index: 7, input: "ES2015、ES2016、ES2017"
// match: "ES2017", capture1: 2017, index: 14, input: "ES2015、ES2016、ES2017"

RegExpのexecメソッドはgフラグなしのパターンで検索した場合、マッチした最初の結果のみを含む特殊な配列を返す
このときのexecメソッドの返り値である配列がindexプロパティとinputプロパティが追加された特殊な配列となるのは、Stringのmatchメソッドと同様

const str = "ABC あいう DE えお";
const alphabetsPattern = /[a-zA-Z]+/;
// gフラグなしでは、最初の結果のみを持つ配列を返す
const results = alphabetsPattern.exec(str);
console.log(results.length); // => 1
console.log(results[0]); // => "ABC"
// マッチした文字列の先頭のインデックス
console.log(results.index); // => 0
// 検索対象となった文字列全体
console.log(results.input); // => "ABC あいう DE えお"

真偽値を取得

正規表現オブジェクトを使って、そのパターンにマッチするかをテストするには、RegExpのtestメソッドを利用する

// 検索対象となる文字列
const str = "にわにはにわにわとりがいる";
// ^ - 検索文字列が先頭ならtrue
console.log(/^にわ/.test(str)); // => true
console.log(/^いる/.test(str)); // => false
// $ - 検索文字列が末尾ならtrue
console.log(/にわ$/.test(str)); // => false
console.log(/いる$/.test(str)); // => true
// 検索文字列が含まれるならtrue
console.log(/にわ/.test(str)); // => true
console.log(/いる/.test(str)); // => true

正規表現は柔軟で便利ですが、コード上から意図が消えてしまいやすい
そのため、正規表現を扱う際にはコメントや変数名で具体的な意図を補足したほうがよい

文字列の置換/削除

文字列の一部を置換したり削除するにはStringのreplaceメソッドを利用する

delete演算子は文字列に対して利用できない
strict modeでは、delete演算子で削除できないプロパティを削除しようとするとエラーが発生する
strict modeでない場合は、エラーも発生せず単に無視される

"use strict";
const str = "文字列";
// 文字列の0番目の削除を試みるがStrict modeでは例外が発生する
delete str[0]; // => TypeError: property 0 is non-configurable and can't be deleted

Stringのreplaceメソッドで、削除したい文字を取り除いた新しい文字列を返すことで削除を表現する
replaceメソッドは、文字列から第一引数の検索文字列または正規表現にマッチする部分を、第二引数の置換文字列へ置換する

文字列.replace("検索文字列", "置換文字列");
文字列.replace(/パターン/, "置換文字列");

replaceメソッドで削除したい部分を空文字列へ置換することで、文字列を削除できる

const str = "文字列";
// "文字"を""(空文字列)へ置換することで"削除"を表現
const newStr = str.replace("文字", "");
console.log(newStr); // => "列"

replaceメソッドには正規表現も指定できる
gフラグを有効化した正規表現を渡すことで、文字列からパターンにマッチするものをすべて置換する

// 検索対象となる文字列
const str = "にわにはにわにわとりがいる";
// 文字列を指定した場合は、最初に一致したものだけが置換される
console.log(str.replace("にわ", "niwa")); // => "niwaにはにわにわとりがいる"
// `g`フラグなし正規表現の場合は、最初に一致したものだけが置換される
console.log(str.replace(/にわ/, "niwa")); // => "niwaにはにわにわとりがいる"
// `g`フラグあり正規表現の場合は、繰り返し置換を行う
console.log(str.replace(/にわ/g, "niwa")); // => "niwaにはniwaniwaとりがいる"

replaceメソッドでは、最初に一致したものだけが置換されるがreplaceAllメソッドでは一致したものがすべて置換される

// 検索対象となる文字列
const str = "???";
// replaceメソッドに文字列を指定した場合は、最初に一致したものだけが置換される
console.log(str.replace("?", "!")); // => "!??"
// replaceAllメソッドに文字列を指定した場合は、一致したものがすべて置換される
console.log(str.replaceAll("?", "!")); // => "!!!"
// replaceメソッドに正規表現を渡す場合、正規表現の特殊文字はエスケープが必要
console.log(str.replace(/\?/g, "!")); // => "!!!"
// RegExp.escapeとreplaceメソッドを使った場合(ES2025)
console.log(str.replace(new RegExp(RegExp.escape("?"), "g"), "!")); // => "!!!"
// replaceAllメソッドにも正規表現を渡せるが、この場合もエスケープが必要(replaceと同様)
console.log(str.replaceAll(/\?/g, "!")); // => "!!!"

replaceメソッドとreplaceAllメソッドでは、キャプチャした文字列を利用して複雑な置換処理

const 置換した結果の文字列 = 文字列.replace(/(パターン)/, (all, ...captures) => {
    return 置換したい文字列;
});

例として、2017-03-01を2017年03月01日に置換する処理

function toDateJa(dateString) {
    // パターンにマッチしたときのみ、コールバック関数で置換処理が行われる
    return dateString.replace(/(\d{4})-(\d{2})-(\d{2})/g, (all, year, month, day) => {
        // `all`には、マッチした文字列全体が入っているが今回は利用しない
        // `all`が次の返す値で置換されるイメージ
        return `${year}${month}${day}日`;
    });
}
// マッチしない文字列の場合は、そのままの文字列が返る
console.log(toDateJa("本日ハ晴天ナリ")); // => "本日ハ晴天ナリ"
// マッチした場合は置換した結果を返す
console.log(toDateJa("今日は2017-03-01です")); // => "今日は2017年03月01日です"

文字列の組み立て

構造的な文字列を扱う場合は、専用の関数や専用のオブジェクトを作ることで安全に文字列を処理

// ベースURLとパスを結合した文字列を返す
function baseJoin(baseURL, pathname) {
    // 末尾に / がある場合は、/ を削除してから結合する
    const stripSlashBaseURL = baseURL.replace(/\/$/, "");
    return stripSlashBaseURL + pathname;
}
// `baseURL`と`pathname`にあるリソースを取得する
function getResource(baseURL, pathname) {
    const url = baseJoin(baseURL, pathname);
    // baseURLの末尾に / があってもなくても同じ結果となる
    console.log(url); // => "http://example.com/resources/example.js"
    // 省略) リソースを取得する処理...
}
const baseURL = "http://example.com/resources/";
const pathname = "/example.js";
getResource(baseURL, pathname);

サロゲートペア

サロゲートペアでは、2つのCode Unitの組み合わせ(合計4バイト)で1つの文字(1つのCode Point)を表現

// 上位サロゲート + 下位サロゲートの組み合わせ
console.log("\uD867\uDE3D"); // => "𩸽"
// Code Pointでの表現
console.log("\u{29e3d}"); // => "𩸽"
// 内部的にはCode Unitが並んでいるものとして扱われている
console.log("\uD867\uDE3D"); // => "𩸽"
// インデックスアクセスもCode Unitごととなる
console.log("𩸽"[0]); // => "\uD867"
console.log("𩸽"[1]); // => "\uDE3D"

Stringのlengthプロパティは文字列におけるCode Unitの要素数を数えるため、"🍎".lengthの結果は2となる

console.log("🍎".length); // => 2

正規表現の.とUnicode

const [all, fish] = "𩸽のひらき".match(/(.)のひらき/);
console.log(all); // => "\ude3dのひらき"
console.log(fish); // => "\ude3d"

正規表現にuフラグをつける
uフラグがついた正規表現は、文字列をCode Pointごとに扱う
そのため、任意の1文字にマッチする.が𩸽という文字(Code Point)にマッチする

const [all, fish] = "𩸽のひらき".match(/(.)のひらき/u);
console.log(all); // => "𩸽のひらき"
console.log(fish); // => "𩸽"

Code Pointの数を数える

// Code Unitの個数を返す
console.log("🍎".length); // => 2
console.log("\uD83C\uDF4E"); // => "🍎"
console.log("\uD83C\uDF4E".length); // => 2

JavaScriptには、文字列におけるCode Pointの個数を数えるメソッドは用意されていない
これを行うには、文字列をCode Pointごとに区切った配列へ変換して、配列の長さを数えるのが簡潔

// Code Pointごとの配列にする
// Array.fromメソッドはIteratorを配列にする
const codePoints = Array.from("リンゴ🍎");
console.log(codePoints); // => ["リ", "ン", "ゴ", "🍎"]
// Code Pointの個数を数える
console.log(codePoints.length); // => 4

for...ofによる反復処理も文字列をCode Pointごとに扱える
これは、for...of文が対象をIteratorとして列挙するため

// 指定した`codePoint`の個数を数える
function countOfCodePoints(str, codePoint) {
    let count = 0;
    for (const item of str) {
        if (item === codePoint) {
            count++;
        }
    }
    return count;
}
console.log(countOfCodePoints("🍎🍇🍎🥕🍒", "🍎")); // => 2

ラッパーオブジェクト

プリミティブ型のデータのうち、真偽値(Boolean)、数値(Number) 、BigInt、文字列(String)、シンボル(Symbol)にはそれぞれ対応するオブジェクトが存在

// "input value"の値をラップしたStringのインスタンスを生成
const str = new String("input value");
// StringのインスタンスメソッドであるtoUpperCaseを呼び出す
str.toUpperCase(); // => "INPUT VALUE"

ラッパーオブジェクトとプリミティブ型の対応

ラッパーオブジェクト プリミティブ型
Boolean 真偽値 true / false
Number 数値 1 / 2
BigInt BigInt 1n / 2n
String 文字列 "文字列"
Symbol シンボル Symbol("説明")

注記: undefinedとnullに対応するラッパーオブジェクトはありません。

typeof演算子でラッパーオブジェクトを見ると"object"

// プリミティブの文字列は"string"型
const str = "文字列";
console.log(typeof str); // => "string"
// ラッパーオブジェクトは"object"型
const stringWrapper = new String("文字列");
console.log(typeof stringWrapper); // => "object"

プリミティブ型の値からラッパーオブジェクトへの自動変換

プリミティブ型の値に対してプロパティアクセスするとき、自動で対応するラッパーオブジェクトに変換される

const str = "string";
// プリミティブ型の値に対してメソッド呼び出しを行う
str.toUpperCase();
// `str`へアクセスする際に"string"がラッパーオブジェクトへ変換され、
// ラッパーオブジェクトはStringのインスタンスなのでメソッドを呼び出せる
// つまり、上のコードは下のコードと同じ意味である
(new String(str)).toUpperCase();

リテラルを使ったプリミティブ型の文字列とラッパーオブジェクトを使った文字列オブジェクトがある(真偽値や数値についても同様)。 この2つを明示的に使い分ける利点はないため、常にリテラルを使うことを推奨

// OK: リテラルを使う
const str = "文字列";
// NG: ラッパーオブジェクトを使う
const stringWrapper = new String("文字列");

関数とスコープ

スコープとは変数や関数の引数などを参照できる範囲を決めるもの
JavaScriptでは、新しい関数を定義するとその関数にひもづけられた新しいスコープが作成され
関数を定義するということは処理をまとめるというだけではなく、変数が有効な範囲を決める新しいスコープを作っていると言える

スコープとは

スコープとは変数の名前や関数などの参照できる範囲を決めるもの、スコープの中で定義された変数はスコープの内側でのみ参照でき、スコープの外側からは参照できない

fn関数のブロック({と})内で変数xを定義している
この変数xはfn関数のスコープに定義されているため、fn関数の内側では参照できる
一方、fn関数の外側から変数xは参照できないためReferenceErrorが発生する

function fn() {
    const x = 1;
    // fn関数のスコープ内から`x`は参照できる
    console.log(x); // => 1
}
fn();
// fn関数のスコープ外から`x`は参照できないためエラー
console.log(x); // => ReferenceError: x is not defined

関数は仮引数を持てますが、仮引数は関数のスコープにひもづけて定義される
そのため、仮引数はその関数の中でのみ参照が可能で、関数の外からは参照できない

function fn(arg) {
    // fn関数のスコープ内から仮引数`arg`は参照できる
    console.log(arg); // => 1
}
fn(1);
// fn関数のスコープ外から`arg`は参照できないためエラー
console.log(arg); // => ReferenceError: arg is not defined

関数によるスコープのことを関数スコープと呼ぶ

letやconstは同じスコープ内に同じ名前の変数を二重に定義できない、
これは、各スコープには同じ名前の変数は1つしか宣言できないため

// スコープ内に同じ"a"を定義すると SyntaxError となる
let a;
let a;

スコープが異なれば同じ名前で変数を宣言できる

// 異なる関数のスコープには同じ"x"を定義できる
function fnA() {
    let x;
}
function fnB() {
    let x;
}

スコープの仕組みがないと、グローバルな空間内で一意な変数名を考える必要があります。 スコープがあることで同じ名前の変数をスコープごとに定義できるため、スコープの役割は重要

ブロックスコープ

{と}で囲んだ範囲をブロックと呼びます。 ブロックもスコープを作成する、ブロック内で宣言された変数は、スコープ内でのみ参照でき、スコープの外側からは参照できない

// ブロック内で定義した変数はスコープ内でのみ参照できる
{
    const x = 1;
    console.log(x); // => 1
}
// スコープの外から`x`を参照できないためエラー
console.log(x); // => ReferenceError: x is not defined

ブロックによるスコープのことをブロックスコープと呼ぶ
if文やwhile文などもブロックスコープを作成する
単独のブロックと同じく、ブロックの中で宣言した変数は外から参照できない

// if文のブロック内で定義した変数はブロックスコープの中でのみ参照できる
if (true) {
    const x = "inner";
    console.log(x); // => "inner"
}
console.log(x); // => ReferenceError: x is not defined

for文は、ループごとに新しいブロックスコープを作成

const array = [1, 2, 3, 4, 5];
// ループごとに新しいブロックスコープを作成する
for (const element of array) {
    // forのブロックスコープの中でのみ`element`を参照できる
    console.log(element);
}
// ループの外からはブロックスコープ内の変数は参照できない
console.log(element); // => ReferenceError: element is not defined

スコープチェーン

スコープもネストできる
このとき外側のブロックスコープのことをOUTER、内側のブロックスコープのことをINNERと呼ぶとする

{
    // OUTERブロックスコープ
    {
        // INNERブロックスコープ
    }
}
{
    // OUTERブロックスコープ
    const x = "x";
    {
        // INNERブロックスコープからOUTERブロックスコープの変数を参照できる
        console.log(x); // => "x"
    }
}

次のようなステップで参照したい変数を探索する

INNERブロックスコープに変数xyzがあるかを確認 => ない
ひとつ外側のOUTERブロックスコープに変数xyzがあるかを確認 => ない
一番外側のスコープにも変数xyzは定義されていない => ReferenceErrorが発生
この内側から外側のスコープへと順番に変数が定義されているか探す仕組みのことをスコープチェーンと呼ぶ

 {
    // OUTERブロックスコープ
    const x = "outer";
    {
        // INNERブロックスコープ
        const x = "inner";
        // 現在のスコープ(INNERブロックスコープ)にある`x`を参照する
        console.log(x); // => "inner"
    }
    // 現在のスコープ(OUTERブロックスコープ)にある`x`を参照する
    console.log(x); // => "outer"
}

グローバルスコープ

グローバルスコープとは名前のとおりもっとも外側にあるスコープで、プログラム実行時に暗黙的に作成される

// プログラム直下はグローバルスコープ
const x = "x";
console.log(x); // => "x"

グローバルスコープで定義した変数はグローバル変数と呼ばれ、グローバル変数はあらゆるスコープから参照できるなぜなら、スコープチェーンの仕組みにより、最終的にもっとも外側のグローバルスコープに定義されている変数を参照できるため

// グローバル変数はどのスコープからも参照できる
const globalVariable = "グローバル";
// ブロックスコープ
{
    // ブロックスコープ内には該当する変数が定義されてない -> 外側のスコープへ
    console.log(globalVariable); // => "グローバル"
}
// 関数スコープ
function fn() {
    // 関数ブロックスコープ内には該当する変数が定義されてない -> 外側のスコープへ
    console.log(globalVariable); // => "グローバル"
}
fn();

ビルトインオブジェクトは、プログラム開始時にグローバルスコープへ自動的に定義されているためどのスコープからも参照できる

// ビルトインオブジェクトは実行環境が自動的に定義している
// どこのスコープから参照してもReferenceErrorにはならない
console.log(isNaN); // => isNaN
console.log(Array); // => Array

自分で定義したグローバル変数とビルトインオブジェクトでは、グローバル変数が優先して参照される
つまり次のようにビルトインオブジェクトと同じ名前の変数を定義すると、定義した変数が参照される

// "Array"という名前の変数を定義
const Array = 1;
// 自分で定義した変数がビルトインオブジェクトより優先される
console.log(Array); // => 1

ビルトインオブジェクトと同じ名前の変数を定義したことにより、ビルトインオブジェクトを参照できなくなる

このように内側のスコープで外側のスコープと同じ名前の変数を定義することで、外側の変数が参照できなくなることを変数の隠蔽(shadowing)と呼ぶ

この問題を回避する方法としては、むやみにグローバルスコープへ変数を定義しないこと
グローバルスコープでビルトインオブジェクトと名前が衝突するとすべてのスコープへ影響を与えるが、関数のスコープ内では影響範囲がその関数の中だけにとどまる

ビルトインオブジェクトと同じ名前を避けることは難しい
なぜならビルトインオブジェクトには実行環境(ブラウザやNode.jsなど)がそれぞれ独自に定義したものが多く存在するため
関数などを活用して小さなスコープを中心にしてプログラムを書くことで、ビルトインオブジェクトと同じ名前の変数があっても影響範囲を限定できる

変数を参照できる範囲を小さくする

function doHeavyTask() {
    // 計測したい処理
}
const startTime = Date.now();
doHeavyTask();
const endTime = Date.now();
console.log(`実行時間は${endTime - startTime}ミリ秒`);
// 実行時間を計測したい関数をコールバック関数として引数に渡す
const measureTask = (taskFn) => {
    const startTime = Date.now();
    taskFn();
    const endTime = Date.now();
    console.log(`実行時間は${endTime - startTime}ミリ秒`);
};
function doHeavyTask() {
    // 計測したい処理
}
measureTask(doHeavyTask);

関数スコープとvarの巻き上げ

varとletの挙動

let let_x;
var var_x;
// 宣言後にそれぞれの変数を参照すると`undefined`となる
console.log(let_x); // => undefined
console.log(var_x); // => undefined
// 宣言後に値を代入できる
let_x = "letのx";
var_x = "varのx";

letとvarで異なる動作

letでは、変数を宣言する前にその変数を参照するとReferenceErrorの例外が発生して参照できません

console.log(x); // => ReferenceError: can't access lexical declaration `x' before initialization
let x = "letのx";

varでは、変数を宣言する前にその変数を参照してもundefinedとなる

// var宣言より前に参照してもエラーにならない
console.log(x); // => undefined
var x = "varのx";

varによる変数宣言は、宣言部分が暗黙的にもっとも近い関数またはグローバルスコープの先頭に巻き上げられ、代入部分はそのままの位置に残るという特殊な動作

// 解釈されたコード
// スコープの先頭に宣言部分が巻き上げられる
var x;
console.log(x); // => undefined
// 変数への代入はそのままの位置に残る
x = "varのx";
console.log(x); // => "varのx"

var変数の宣言の巻き上げは、ブロックスコープを無視してもっとも近い関数またはグローバルスコープに変数をひもづける
そのため、次のようにブロック{}でvarによる変数宣言を囲んでも、もっとも近い関数スコープであるfn関数の直下に宣言部分が巻き上げられる(if文やfor文におけるブロックスコープも同様に無視される)。

function fn() {
    // 内側のスコープにあるはずの変数`x`が参照できる
    console.log(x); // => undefined
    {
        var x = "varのx";
    }
    console.log(x); // => "varのx"
}
fn();

つまり、先ほどのコードは実際の実行時には、次のように解釈されて実行されていると考えられる

// 解釈されたコード
function fn() {
    // もっとも近い関数スコープの先頭に宣言部分が巻き上げられる
    var x;
    console.log(x); // => undefined
    {
        // 変数への代入はそのままの位置に残る
        x = "varのx";
    }
    console.log(x); // => "varのx"
}
fn();

この変数の宣言部分がもっとも近い関数またはグローバルスコープの先頭に移動しているように見える動作のことを変数の巻き上げ(hoisting)と呼ぶ

関数宣言と巻き上げ

functionキーワードを使った関数宣言もvarと同様に、もっとも近い関数またはグローバルスコープの先頭に巻き上げられる

// `hello`関数の宣言より前に呼び出せる
hello(); // => "Hello"

function hello(){
    return "Hello";
}

関数宣言は宣言そのものであるため、hello関数そのものがスコープの先頭に巻き上げられる

// 解釈されたコード
// `hello`関数の宣言が巻き上げられる
function hello(){
    return "Hello";
}

hello(); // => "Hello"

varで宣言された変数へ関数を代入した場合はvarのルールで巻き上げられる

// `hello`変数は巻き上げられ、暗黙的に`undefined`となる
hello(); // => TypeError: hello is not a function

// `hello`変数へ関数を代入している
var hello = function(){
    return "Hello";
};

クロージャー

クロージャーとは「外側のスコープにある変数への参照を保持できる」という関数が持つ性質のこと

// `increment`関数を定義して返す関数
function createCounter() {
    let count = 0;
    // `increment`関数は`count`変数を参照
    function increment() {
        count = count + 1;
        return count;
    }
    return increment;
}
// `myCounter`は`createCounter`が返した関数を参照
const myCounter = createCounter();
myCounter(); // => 1
myCounter(); // => 2
// 新しく`newCounter`を定義する
const newCounter = createCounter();
newCounter(); // => 1
newCounter(); // => 2
// `myCounter`と`newCounter`は別々の状態を持っている
myCounter(); // => 3
newCounter(); // => 3

静的スコープ

printX関数内で変数xを参照していますが、変数xはグローバルスコープと関数runの中で、それぞれ定義されている
このときprintX関数内のxという識別子がどの変数xを参照するかは静的に決定される。

const x = 10; // *1

function printX() {
    // この識別子`x`は常に *1 の変数`x`を参照する
    console.log(x); // => 10
}

function run() {
    const x = 20; // *2
    printX(); // 常に10が出力される
}

run();

スコープチェーンの仕組みを思い出すと、この識別子xは次のように名前解決されてグローバルスコープの変数xを参照することがわかる

printXの関数スコープに変数xが定義されていない
ひとつ外側のスコープ(グローバルスコープ)を確認する
ひとつ外側のスコープにconst x = 10;が定義されているので、識別子xはこの変数を参照する

クロージャーの用途

  • 関数に状態を持たせる手段として
  • 外から参照できない変数を定義する手段として
  • グローバル変数を減らす手段として
  • 高階関数の一部分として
const createCounter = () => {
    // 外のスコープから`privateCount`を直接参照できない
    let privateCount = 0;
    return () => {
        privateCount++;
        return `${privateCount}回目`;
    };
};
const counter = createCounter();
console.log(counter()); // => "1回目"
console.log(counter()); // => "2回目"

関数を返す関数のことを高階関数と呼ぶが、クロージャーの性質を使うことで次のようにnより大きいかを判定する高階関数を作れる
最初からgreaterThan5という関数を定義すればいいが高階関数を使うことで条件を後から定義できるなどの柔軟性がある

function greaterThan(n) {
    return function(m) {
        return m > n;
    };
}
// 5より大きな値かを判定する関数を作成する
const greaterThan5 = greaterThan(5);
console.log(greaterThan5(4)); // => false
console.log(greaterThan5(5)); // => false
console.log(greaterThan5(6)); // => true

関数とthis

thisの参照先は主に次の条件によって変化する

  • 実行コンテキストにおけるthis
  • コンストラクタにおけるthis
  • 関数とメソッドにおけるthis
  • Arrow Functionにおけるthis

実行コンテキストとthis

スクリプトにおけるthis

実行コンテキストが"Script"である場合、トップレベルのスコープに書かれたthisはグローバルオブジェクトを参照、グローバルオブジェクトは、実行環境ごとに異なるものが定義される
ブラウザのグローバルオブジェクトはwindowオブジェクト、Node.jsのグローバルオブジェクトはglobalオブジェクトとなる

ブラウザでは、script要素のtype属性を指定していない場合は、実行コンテキストが"Script"として実行される
このscript要素の直下に書いたthisはグローバルオブジェクトであるwindowオブジェクトとなる

<script>
// 実行コンテキストは"Script"
console.log(this); // => window
</script>

モジュールにおけるthis

実行コンテキストが"Module"である場合、そのトップレベルのスコープに書かれたthisは常にundefinedとなる

ブラウザで、script要素にtype="module"属性がついた場合は、実行コンテキストが"Module"として実行されるこのscript要素の直下に書いたthisはundefinedとなる

<script type="module">
// 実行コンテキストは"Module"
console.log(this); // => undefined
</script>

実行環境のグローバルオブジェクトを参照するglobalThis

// ブラウザでは`window`オブジェクト、Node.jsでは`global`オブジェクトを参照する
console.log(globalThis);

関数とメソッドにおけるthis

関数を定義する方法として、functionキーワードによる関数宣言と関数式、Arrow Functionなどがある
thisが参照先を決めるルールは、Arrow Functionとそれ以外の関数定義の方法で異なる

そのため、まずは関数定義の種類について振り返ってから、それぞれのthisについて見ていく

関数の種類

関数を定義する場合には、次の3つの方法を利用する

// `function`キーワードからはじめる関数宣言
function fn1() {}
// `function`を式として扱う関数式
const fn2 = function() {};
// Arrow Functionを使った関数式
const fn3 = () => {};

それぞれ定義した関数は関数名()と書くことで呼び出せる

// 関数宣言
function fn() {}
// 関数呼び出し
fn();

メソッドの種類

JavaScriptではオブジェクトのプロパティが関数である場合にそれをメソッドと呼ぶ
一般的にはメソッドも含めたものを関数と言い、関数宣言などとプロパティである関数を区別する場合にメソッドと呼ぶ

const obj = {
    // `function`キーワードを使ったメソッド
    method1: function() {
    },
    // Arrow Functionを使ったメソッド
    method2: () => {
    }
};

メソッドを定義する場合には、オブジェクトのプロパティに関数式を定義するだけ

const obj = {
    // `function`キーワードを使ったメソッド
    method1: function() {
    },
    // Arrow Functionを使ったメソッド
    method2: () => {
    }
};

これに加えてメソッドには短縮記法がある、 オブジェクトリテラルの中で メソッド名(){ /メソッドの処理/ }と書くことで、メソッドを定義できる

const obj = {
    // メソッドの短縮記法で定義したメソッド
    method() {
    }
};

これらのメソッドは、オブジェクト名.メソッド名()と書くことで呼び出せる

const obj = {
    // メソッドの定義
    method() {
    }
};
// メソッド呼び出し
obj.method();

Arrow Function以外の関数におけるthis

Arrow Function以外の関数(メソッドも含む)におけるthisは、実行時に決まる値となる

// 疑似的な`this`の値の仕組み
// 関数は引数として暗黙的に`this`の値を受け取るイメージ
function fn(暗黙的に渡されるthisの値, 仮引数) {
    console.log(this); // => 暗黙的に渡されるthisの値
}
// 暗黙的に`this`の値を引数として渡しているイメージ
fn(暗黙的に渡すthisの値, 引数);

関数宣言や関数式におけるthis

"use strict";
function fn1() {
    return this;
}
const fn2 = function() {
    return this;
};
// 関数の中の`this`が参照する値は呼び出し方によって決まる
// `fn1`と`fn2`どちらもただの関数として呼び出している
// メソッドとして呼び出していないためベースオブジェクトはない
// ベースオブジェクトがない場合、`this`は`undefined`となる
console.log(fn1()); // => undefined
console.log(fn2()); // => undefined
"use strict";
function outer() {
    console.log(this); // => undefined
    function inner() {
        console.log(this); // => undefined
    }
    // `inner`関数呼び出しのベースオブジェクトはない
    inner();
}
// `outer`関数呼び出しのベースオブジェクトはない
outer();

メソッド呼び出しにおけるthis

const obj = {
    // 関数式をプロパティの値にしたメソッド
    method1: function() {
        return this;
    },
    // 短縮記法で定義したメソッド
    method2() {
        return this;
    }
};
// メソッド呼び出しの場合、それぞれの`this`はベースオブジェクト(`obj`)を参照する
// メソッド呼び出しの`.`の左にあるオブジェクトがベースオブジェクト
console.log(obj.method1()); // => obj
console.log(obj.method2()); // => obj
const person = {
    fullName: "Brendan Eich",
    sayName: function() {
        // `person.fullName`と書いているのと同じ
        return this.fullName;
    }
};
// `person.fullName`を出力する
console.log(person.sayName()); // => "Brendan Eich"
const person = {
    fullName: "Brendan Eich",
    sayName: function() {
        // `person.fullName`と書いているのと同じ
        return this.fullName;
    }
};
// `person.fullName`を出力する
console.log(person.sayName()); // => "Brendan Eich"

const obj1 = {
    obj2: {
        obj3: {
            method() {
                return this;
            }
        }
    }
};
// `obj1.obj2.obj3.method`メソッドの`this`は`obj3`を参照
console.log(obj1.obj2.obj3.method() === obj1.obj2.obj3); // => true

thisが問題となるパターン

問題: thisを含むメソッドを変数に代入した場合

メソッドを 別の変数に代入した瞬間に「元のオブジェクトとの紐づき」は切れる

対処法: call、apply、bindメソッド

callメソッドは第一引数にthisとしたい値を指定し、残りの引数には呼び出す関数の引数を指定する
暗黙的に渡されるthisの値を明示的に渡せるメソッド

関数.call(thisの値, ...関数の引数);
"use strict";
function say(message) {
    return `${message} ${this.fullName}!`;
}
const person = {
    fullName: "Brendan Eich"
};
// `this`を`person`にして`say`関数を呼びだす
console.log(say.call(person, "こんにちは")); // => "こんにちは Brendan Eich!"
// `say`関数をそのまま呼び出すと`this`は`undefined`となるため例外が発生
say("こんにちは"); // => TypeError: Cannot read property 'fullName' of undefined

applyメソッドは第一引数にthisとする値を指定し、第二引数に関数の引数を配列として渡す

関数.apply(thisの値, [関数の引数1, 関数の引数2]);
"use strict";
function say(message) {
    return `${message} ${this.fullName}!`;
}
const person = {
    fullName: "Brendan Eich"
};
// `this`を`person`にして`say`関数を呼びだす
// callとは異なり引数を配列として渡す
console.log(say.apply(person, ["こんにちは"])); // => "こんにちは Brendan Eich!"
// `say`関数をそのまま呼び出すと`this`は`undefined`となるため例外が発生
say("こんにちは"); // => TypeError: Cannot read property 'fullName' of undefined

callメソッドとapplyメソッドの違いは、関数の引数への値の渡し方が異なるだけ、 また、どちらのメソッドもthisの値が不要な場合はnullを渡すのが一般的

function add(x, y) {
    return x + y;
}
// `this`が不要な場合は、nullを渡す
console.log(add.call(null, 1, 2)); // => 3
console.log(add.apply(null, [1, 2])); // => 3

このbindメソッドをただの関数で表現すると次のように書ける、 bindはthisや引数を束縛した関数を作るメソッド

function say(message) {
    return `${message} ${this.fullName}!`;
}
const person = {
    fullName: "Brendan Eich"
};
// `this`を`person`に束縛した`say`関数をラップした関数を作る
//  say.bind(person, "こんにちは"); は次のようなラップ関数を作る
const sayPerson = () => {
    return say.call(person, "こんにちは");
};
console.log(sayPerson()); // => "こんにちは Brendan Eich!"

基本的には「メソッドとして定義されている関数はメソッドとして呼ぶこと」でこの問題を回避するほうがよい

クラス

クラス定義方法

class MyClass {
    constructor() {
        // コンストラクタ関数の処理
        // インスタンス化されるときに自動的に呼び出される
    }
}


const MyClass = class MyClass {
    constructor() {}
};

const AnonymousClass = class {
    constructor() {}
};
class MyClassA {
    constructor() {
        // コンストラクタの処理が必要なら書く
    }
}
// コンストラクタの処理が不要な場合は省略できる
class MyClassB {
}

クラスのインスタンス化

あるインスタンスが指定したクラスから作成されたものかを判定するにはinstanceof演算子が使われる

class MyClass {
}
// `MyClass`をインスタンス化する
const myClass = new MyClass();
// 毎回新しいインスタンス(オブジェクト)を作成する
const myClassAnother = new MyClass();
// それぞれのインスタンスは異なるオブジェクト
console.log(myClass === myClassAnother); // => false
// クラスのインスタンスかどうかは`instanceof`演算子で判定できる
console.log(myClass instanceof MyClass); // => true
console.log(myClassAnother instanceof MyClass); // => true
class Point {
    // コンストラクタ関数の仮引数として`x`と`y`を定義
    constructor(x, y) {
        // コンストラクタ関数における`this`はインスタンスを示すオブジェクト
        // インスタンスの`x`と`y`プロパティにそれぞれ値を設定する
        this.x = x;
        this.y = y;
    }
}

// 1. コンストラクタを`new`演算子で引数とともに呼び出す
const point = new Point(3, 4);
// 4. `Point`のインスタンスである`point`の`x`と`y`プロパティには初期化された値が入る
console.log(point.x); // => 3
console.log(point.y); // => 4

クラスからインスタンスを作成するには必ずnew演算子を使う

class MyClass {
    constructor() {}
}
// クラスは関数として呼び出すことはできない
MyClass(); // => TypeError: class constructors must be invoked with |new|

クラス名は大文字ではじめる
クラス名を大文字にしておき、そのインスタンスは小文字で開始すれば名前が被らないという合理的な理由

class Thing {}
const thing = new Thing();

class構文と関数でのクラスの違い

クラスのプロトタイプメソッドの定義

class クラス {
    メソッド() {
        // ここでの`this`はベースオブジェクトを参照
    }
}

const インスタンス = new クラス();
// メソッド呼び出しのベースオブジェクト(`this`)は`インスタンス`となる
インスタンス.メソッド();

Counterクラスにincrementメソッドを定義

class Counter {
    constructor() {
        this.count = 0;
    }
    // `increment`メソッドをクラスに定義する
    increment() {
        // `this`は`Counter`のインスタンスを参照する
        this.count++;
    }
}
const counterA = new Counter();
const counterB = new Counter();
// `counterA.increment()`のベースオブジェクトは`counterA`インスタンス
counterA.increment();
// 各インスタンスの持つプロパティ(状態)は異なる
console.log(counterA.count); // => 1
console.log(counterB.count); // => 0
class Counter {
    constructor() {
        this.count = 0;
    }
    increment() {
        this.count++;
    }
}
const counterA = new Counter();
const counterB = new Counter();
// 各インスタンスオブジェクトのメソッドは共有されている(同じ関数を参照している)
console.log(counterA.increment === counterB.increment); // => true

プロトタイプメソッドの型

class クラス {
    メソッド() {
        // このメソッドはプロトタイプメソッドとして定義される
    }
}

クラスのアクセッサプロパティの定義

クラスでは、プロパティの参照(getter)、プロパティへの代入(setter)時に呼び出される特殊なメソッドを定義できる

「メソッド」と「プロパティ」の違い

メソッド:呼び出すときに () がいる
例)user.getName()

プロパティ:見る/入れるだけで () はいらない
例)user.name、user.name = "rai"

定義の型

class クラス {
    // getter
    get プロパティ名() {
        return ;
    }
    // setter
    set プロパティ名(仮引数) {
        // setterの処理
    }
}
const インスタンス = new クラス();
インスタンス.プロパティ名; // getterが呼び出される
インスタンス.プロパティ名 = ; // setterが呼び出される

class NumberWrapper {
    constructor(value) {
        this._value = value;
    }
    // `_value`プロパティの値を返すgetter
    get value() {
        console.log("getter");
        return this._value;
    }
    // `_value`プロパティに値を代入するsetter
    set value(newValue) {
        console.log("setter");
        this._value = newValue;
    }
}

const numberWrapper = new NumberWrapper(1);
// "getter"とコンソールに表示される
console.log(numberWrapper.value); // => 1
// "setter"とコンソールに表示される
numberWrapper.value = 42;
// "getter"とコンソールに表示される
console.log(numberWrapper.value); // => 42

_(アンダーバー)から始まるプロパティ名

外から直接読み書きしてほしくないプロパティを_(アンダーバー)から始まる名前にするのはただの習慣であるため、構文としての意味はない

Array.prototype.lengthをアクセッサプロパティで再現する

Arrayのlengthプロパティへ値を代入すると、そのインデックス以降の要素は自動的に削除される仕様になっている

const array = [1, 2, 3, 4, 5];
// 要素数を減らすと、インデックス以降の要素が削除される
array.length = 2;
console.log(array.join(", ")); // => "1, 2"
// 要素数だけを増やしても、配列の中身は空要素が増えるだけ
array.length = 5;
console.log(array.join(", ")); // => "1, 2, , , "

クラスフィールド構文

class クラス {
    プロパティ名 = プロパティの初期値;
}

カウンターをインクリメントする構文

class Counter {
    count = 0;
    increment() {
        this.count++;
    }
}
const counter = new Counter();
counter.increment();
console.log(counter.count); // => 1

クラスフィールドでの初期化処理→そのあとconstructorでのプロパティの定義という処理順
そのため、同じプロパティ名への定義がある場合は、constructorメソッド内での定義でプロパティは上書きされる

// 同じプロパティ名の場合は、constructorでの代入が後となる
class OwnClass {
    publicField = 1;
    constructor(arg) {
        this.publicField = arg;
    }
}
const ownClass = new OwnClass(2);
console.log(ownClass.publicField); // => 2

クラスフィールドを使ってプロパティの存在を宣言する
クラスフィールドでは、プロパティの初期値は省略可能

class MyClass {
    // myPropertyはundefinedで初期化される
    myProperty;
}
class Loader {
    loadedContent;
    load() {
        this.loadedContent = "読み込んだコンテンツ内容";
    }
}

JavaScriptでは、オブジェクトのプロパティは初期化時に存在していなくても、後から代入すれば作成できる、
そのため、次のようにLoaderクラスを実装しても意味は同じ

class Loader {
    load() {
        this.loadedContent = "読み込んだコンテンツ内容";
    }
}

クラスフィールドでのthisはクラスのインスタンスを示す

class Counter {
    count = 0;
    // upはincrementメソッドを参照している
    up = this.increment;
    increment() {
        this.count++;
    }
}
const counter = new Counter();
counter.up(); // 結果的にはincrementメソッドが呼び出される
console.log(counter.count); // => 1

Privateクラスフィールド

クラスフィールド構文で次のように書くと、定義したプロパティはクラスをインスタンス化した後に外からも参照d系る、そのため、Publicクラスフィールドと呼ばれている

class クラス {
    プロパティ名 = プロパティの初期値;
}

一方で外からアクセスされたくないインスタンスのプロパティも存在する、そのようなプライベートなプロパティを定義する構文

class クラス {
    // プライベートなプロパティは#をつける
    #フィールド名 = プロパティの初期値;
}

定義したPrivateクラスフィールドは、this.#フィールド名で参照できる

class PrivateExampleClass {
    #privateField = 42;
    dump() {
        // Privateクラスフィールドはクラス内からのみ参照できる
        console.log(this.#privateField); // => 42
    }
}
const privateExample = new PrivateExampleClass();
privateExample.dump();

静的メソッド

インスタンスメソッドは、クラスをインスタンス化して利用する
一方、クラスをインスタンス化せずに利用できる静的メソッド

class クラス {
    static メソッド() {
        // 静的メソッドの処理
    }
}
// 静的メソッドの呼び出し
クラス.メソッド();

プロトタイプに定義したメソッドとインスタンスに定義したメソッド

この場合はインスタンスオブジェクトに定義したmethodが呼び出される、このときインスタンスのmethodプロパティをdelete演算子で削除すると、今度はプロトタイプメソッドのmethodが呼び出される

class ConflictClass {
    // インスタンスオブジェクトに`method`を定義
    method = () => {
        console.log("インスタンスオブジェクトのメソッド");
    };

    method() {
        console.log("プロトタイプメソッド");
    }
}

const conflict = new ConflictClass();
conflict.method(); // "インスタンスオブジェクトのメソッド"
// インスタンスの`method`プロパティを削除
delete conflict.method;
conflict.method(); // "プロトタイプメソッド"

プロトタイプメソッドとインスタンスオブジェクトのメソッドは上書きされずにどちらも定義されている
インスタンスオブジェクトのメソッドがプロトタイプオブジェクトのメソッドよりも優先して呼ばれている

非同期処理:Promise/Async Function

同期処理

上から順番に1つずつ、終わるまで次に進まない

非同期処理

時間がかかる処理を「後回し」にして先に進む

Promise:非同期処理の状態や結果を表現するビルトインオブジェクト

Map

マップの作成と初期化

const map = new Map();
console.log(map.size); // => 0

次のコードでは、Mapに初期値となるエントリーの配列を渡す

const map = new Map([["key1", "value1"], ["key2", "value2"]]);
// 2つのエントリーで初期化されている
console.log(map.size); // => 2

要素の追加と取り出し

Mapには新しい要素をsetメソッドで追加でき、追加した要素をgetメソッドで取り出せる

setメソッドは特定のキーと値を持つ要素をマップに追加する
ただし、同じキーで複数回setメソッドを呼び出した際は、後から追加された値で上書きされる

const map = new Map();
// 新しい要素の追加
map.set("key", "value1");
console.log(map.size); // => 1
console.log(map.get("key")); // => "value1"
// 要素の上書き
map.set("key", "value2");
console.log(map.get("key")); // => "value2"
// キーの存在確認
console.log(map.has("key")); // => true
console.log(map.has("foo")); // => false

deleteメソッドはマップから要素を削除すされる、deleteメソッドに渡されたキーと、そのキーにひもづいた値がマップから削除される、マップが持つすべての要素を削除するためのclearメソッドがある

const map = new Map();
map.set("key1", "value1");
map.set("key2", "value2");
console.log(map.size); // => 2
map.delete("key1");
console.log(map.size); // => 1
map.clear();
console.log(map.size); // => 0

マップの反復処理

マップが持つ要素を列挙するメソッドとして、forEach、keys、values、entriesがある
forEachメソッドはマップが持つすべての要素を、マップへの挿入順に反復処理
コールバック関数には引数として値、キー、マップの3つが渡される

const map = new Map([["key1", "value1"], ["key2", "value2"]]);
const results = [];
map.forEach((value, key) => {
    results.push(`${key}:${value}`);
});
console.log(results); // => ["key1:value1","key2:value2"]

keysメソッドはマップが持つすべての要素のキーを挿入順に並べたIteratorオブジェクトを返す
valuesメソッドはマップが持つすべての要素の値を挿入順に並べたIteratorオブジェクトを返す
これらの返り値はIteratorオブジェクトであって配列ではない、そのため、次の例のようにfor...of文で反復処理を行ったり、Array.from静的メソッドに渡して配列に変換して使ったりする

const map = new Map([["key1", "value1"], ["key2", "value2"]]);
const keys = [];
// keysメソッドの返り値(Iterator)を反復処理する
for (const key of map.keys()) {
    keys.push(key);
}
console.log(keys); // => ["key1","key2"]
// keysメソッドの返り値(Iterator)から配列を作成する
const keysArray = Array.from(map.keys());
console.log(keysArray); // => ["key1","key2"]

entriesメソッドはマップが持つすべての要素をエントリーとして挿入順に並べたIteratorオブジェクトを返す
エントリーは[キー, 値]の配列
配列の分割代入を使うとエントリーからキーと値を簡単に取り出せる

エントリーって何?

Mapに入っている1組のデータのこと。

const map = new Map([
  ["name", "rai"],
  ["age", 28],
]);
const map = new Map([["key1", "value1"], ["key2", "value2"]]);
const entries = [];
for (const [key, value] of map.entries()) {
    entries.push(`${key}:${value}`);
}
console.log(entries); // => ["key1:value1","key2:value2"]

マップ自身もiterableなオブジェクトなので、for...of文で反復処理できる
マップをfor...of文で反復したときは、すべての要素をエントリーとして挿入順に反復処理する

const map = new Map([["key1", "value1"], ["key2", "value2"]]);
const results = [];
for (const [key, value] of map) {
    results.push(`${key}:${value}`);
}
console.log(results); // => ["key1:value1","key2:value2"]

iterableなオブジェクトはSpread構文での展開も可能であるため、次のようにMapを配列に展開できる

const map = new Map([["key1", "value1"], ["key2", "value2"]]);
// MapオブジェクトをSpread構文で展開すると[キー, 値]のペア(エントリー)からなる配列になる
console.log([...map]); // => [["key1", "value1"], ["key2", "value2"]]

// キーだけや値だけを取り出すこともできる
console.log([...map.keys()]); // => ["key1", "key2"]
console.log([...map.values()]); // => ["value1", "value2"]

Map.groupBy静的メソッド

Map.groupBy静的メソッドでは、配列からグループ分けしたマップを作成できる
Map.groupBy静的メソッドは、第一引数に配列などのiterableオブジェクト、第二引数にグループ分けの条件を返すコールバック関数を渡す

const votes = [
    { id: 1, vote: "yes" },
    { id: 2, vote: "no" },
    { id: 3, vote: "yes" },
    { id: 4, vote: "yes" },
    { id: 5, vote: "no" },
];
const groupedVotes = Map.groupBy(votes, (vote) => vote.vote);
console.log(groupedVotes.get("yes")); // => [{ id: 1, vote: "yes" }, { id: 3, vote: "yes" }, { id: 4, vote: "yes" }]
console.log(groupedVotes.get("no")); // => [{ id: 2, vote: "no" }, { id: 5, vote: "no" }]

マップとしてのObjectとMap

const map = {};
// マップがキーを持つことを確認する
function has(key) {
    return typeof map[key] !== "undefined";
}
console.log(has("foo")); // => false
// Objectのプロパティが存在する
console.log(has("constructor")); // => true

イテレータとジェネレータ

イテレータはfor...ofの背後で動く「順に値を取り出す」仕組み、ジェネレータは順番に値を返せる関数を定義するための構文

// 配列を使った場合:すべての数値を一度にメモリに作成
const numbers = [];
for (let i = 1; i <= 5000; i++) {
    numbers.push(i);
}
// 配列のサイズ
console.log(numbers.length); // => 5000

// すべてのデータがメモリに存在している
console.log(numbers.slice(0, 5)); // => [1, 2, 3, 4, 5]

配列とイテレータの違い

1から5000までの数値を処理する場合

配列だとまずすべての数値をメモリに格納してから処理を開始するため、5000個の要素を持つ配列が必要になる

// 配列を使った場合:すべての数値を一度にメモリに作成
const numbers = [];
for (let i = 1; i <= 5000; i++) {
    numbers.push(i);
}
// 配列のサイズ
console.log(numbers.length); // => 5000

// すべてのデータがメモリに存在している
console.log(numbers.slice(0, 5)); // => [1, 2, 3, 4, 5]

イテレータを使う場合は、必要になったタイミングで値を生成します。 そのため、最初から5000個の要素をメモリに格納する必要はなく、必要な分だけを生成して処理する

イテレータを使う場合

// イテレータを使った場合:必要な時に値を生成
function* numberGenerator() {
    for (let i = 1; i <= 5000; i++) {
        yield i; // 値を一つずつ生成
    }
}

const iterator = numberGenerator();
// 最初の値
console.log(iterator.next().value); // => 1
// 次の値
console.log(iterator.next().value); // => 2

このような必要なタイミングで値を評価できる仕組みを遅延評価と呼ぶ

遅延評価とは

遅延評価とは、値が実際に要求されるまで計算を遅らせる仕組み。
配列の場合は「先行評価(eager evaluation)」で、すべての値を即座に計算してメモリに保存する
一方、イテレータは「遅延評価(lazy evaluation)」で、nextメソッドを呼び出したタイミングで初めて値を計算する

const numbers = [1, 2, 3];
console.log(numbers[0]); // => 1
console.log(numbers[1]); // => 2
console.log(numbers[2]); // => 3

// イテレータ(遅延評価): 値は必要な時に計算
function* numberGenerator() {
    yield 1; // 初めてnext()が呼ばれた時に評価される
    yield 2; // 次のnext()が呼ばれた時に評価される
    yield 3; // さらに次のnext()が呼ばれた時に評価される
}
const iterator = numberGenerator();
console.log(iterator.next().value); // => 1
console.log(iterator.next().value); // => 2
console.log(iterator.next().value); // => 3

IterableプロトコルとIteratorプロトコル

JavaScriptにおけるイテレーション(反復処理)は、IterableプロトコルとIteratorプロトコルという2つのプロトコルによって定義されている

この2つのプロトコルは、for...ofループの背後で動作する仕組みとなっています。

Iterableプロトコル

Iterableプロトコルは、オブジェクトが反復可能であることを示すプロトコル、オブジェクトがIterableになるためには、Symbol.iteratorというメソッドを持つ必要がある

Symbol.iteratorは、JavaScriptの組み込みSymbolの1つで、オブジェクトがIterableであることを示す特殊なキー

const iterableObject = {
    // Symbol.iteratorメソッドの定義
    [Symbol.iterator]() {
        // Iteratorオブジェクトを返す
        return {
            // Iterator プロトコルを実装
        };
    },
};

Iteratorプロトコル

Iteratorプロトコルは、反復処理時に次の値を取得するためのプロトコル
Iteratorプロトコルを実装するオブジェクトは、nextメソッドを持つ必要がある
nextメソッドは、{ value: 次の値, done: 完了かどうか }という形式のオブジェクトを返す

// nextメソッドの戻り値の形式
const result = iterator.next();
console.log(result.value); // 現在のIteratorが生成する値
console.log(result.done); // 反復が完了したかどうか(true: 完了、false: 継続)

IterableとIteratorの違い

・Iterable: [Symbol.iterator]メソッドでIteratorを返す
・Iterator: nextメソッドで値を1つずつ取り出せるオブジェクト

const iterableObject = {
    // Iteratorプロトコルを実装
    next() {
        // { value: 次の値, done: false } または { value: undefined, done: true } を返す
    },
    // Iterableプロトコルを実装
    [Symbol.iterator]() {
        return this; // 自分自身を返す(= 自分もIterable)
    },
};
// Iteratorを取得
const iterator = iterableObject[Symbol.iterator]();
// IteratorもIterable(自分自身を返す)
console.log(iterator === iterator[Symbol.iterator]()); // => true

指定範囲の数値を生成するIterable/Iteratorの実装

// 範囲の数値を生成するIterable Iteratorの実装
function createRange(start, end) {
    let current = start;
    return {
        // `current`が`end`以下の間、次の値を返し、`current`をインクリメントする
        // `end`を超えた場合は、doneをtrueにして終了
        next() {
            if (current <= end) {
                return { value: current++, done: false };
            } else {
                return { value: undefined, done: true };
            }
        },
        // Iterableプロトコル: Symbol.iteratorメソッドを実装
        [Symbol.iterator]() {
            return this;
        }
    };
}

// Iterable Iteratorを取得
const range = createRange(1, 3);
// Iteratorを取得
const iterator = range[Symbol.iterator]();
// Iteratorを使って、値を順番に取得
console.log(iterator.next().value); // => 1
console.log(iterator.next().value); // => 2
console.log(iterator.next().value); // => 3
console.log(iterator.next().value); // => undefined

for...ofループでIterable Iteratorを使用する

Iteratorのnextメソッドを直接呼び出すことで、値を1つずつ取得できますが、通常はfor...ofループを使ってIterable Iteratorを反復処理する

Iterableなビルトインオブジェクト

配列(Array)、文字列(String)、Map、Set

ジェネレータ関数:Iterable Iteratorを定義するための構文

function* generatorFunction() {
    yield 1; // 最初の値を生成
    yield 2; // 次の値を生成
    yield 3; // 最後の値を生成
}

ジェネレータ関数から返されるGeneratorオブジェクトは、IterableプロトコルとIteratorプロトコルの両方を実装している
つまり、GeneratorオブジェクトもIterable Iteratorであり、for...ofループで反復処理が可能

function* simpleGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

// for...ofループでIterable Iteratorとして利用
for (const num of simpleGenerator()) {
    console.log(num); // 1, 2, 3
}

ジェネレータ関数の利点

関数の途中で実行を一時停止し、値を返すことができる、 yield式は、関数の実行を一時停止し、値を呼び出し元に返す、nextメソッドが呼ばれると、次のyieldまで関数の実行が再開される

function* createGenerator() {
    console.log("a. 開始");
    yield 1;
    console.log("b. 中間");
    yield 2;
    console.log("c. 終了");
    yield 3;
}

const generator = createGenerator();
// ここではまだ何も出力されない
console.log(generator.next()); // "a. 開始" が出力され、{ value: 1, done: false }が返される
console.log(generator.next()); // "b. 中間" が出力され、{ value: 2, done: false }が返される
console.log(generator.next()); // "c. 終了" が出力され、{ value: 3, done: false }が返される
// 列挙が完了したので、次のnext()ではdoneがtrueになる
console.log(generator.next()); // => { value: undefined, done: true }

イテレータのメソッド

Iterable = 本棚
Iterator = 本棚から1冊ずつ取る人
value = 取られた本

処理イメージ

Iterable に格納されている value を
Iterator が 1つずつ取り出して、
取り出された value を扱う

Iterator.from静的メソッド
Iterator.fromメソッドは、GeneratorオブジェクトやIterableオブジェクトからイテレータを作成する

配列に対して利用したい場合は、まずイテレータへ変換する必要がある

Iterator.fromを使って配列からイテレータを作成し、nextメソッドでイテレータから要素を取り出している

// 配列からIteratorを作成
const iterator = Iterator.from([1, 2, 3, 4, 5]);

console.log(iterator.next()); // => { value: 1, done: false }
console.log(iterator.next()); // => { value: 2, done: false }
// Iterator.from を使って配列からイテレータを作成
const iterator = Iterator.from([1, 2, 3]);
console.log(iterator.map((x) => x * 2).toArray()); // => [2, 4, 6]
// Array.prototype.values メソッドはイテレータを返す
const array = [1, 2, 3];
console.log(array.values().map((x) => x * 2).toArray()); // => [2, 4, 6]
// Map.prototype.values メソッドはイテレータを返す
const map = new Map([["a", 1], ["b", 2], ["c", 3]]);
console.log(map.values().map((x) => x * 2).toArray()); // => [2, 4, 6]
// Set.prototype.values メソッドはvalueのイテレータを返す
const set = new Set([1, 2, 3]);
console.log(set.values().map((x) => x * 2).toArray()); // => [2, 4, 6]

Iterator.prototype.toArrayメソッド

const iterator = Iterator.from([1, 2, 3]).map((x) => x * 2);
// Iteratorを配列に変換
const array = iterator.toArray();
console.log(array); // => [2, 4, 6]

Iterator.prototype.takeメソッド

// 無限に数値を生成するジェネレータ
function* infiniteNumbers() {
    let num = 1;
    while (true) {
        yield num++;
    }
}
// 無限に数値を生成するジェネレータから最初の5つの数値を取得
const first5 = infiniteNumbers().take(5);
console.log(first5.toArray()); // => [1, 2, 3, 4, 5]

Iterator.prototype.mapメソッド

Iterator.prototype.mapメソッドは、各要素を変換した新しいIteratorを返す

const numbers = Iterator.from([1, 2, 3, 4, 5]);
// 各数値を2倍にする
const doubled = numbers.map((x) => x * 2);
console.log(doubled.toArray()); // => [2, 4, 6, 8, 10]

Iterator.prototype.filterメソッド

Iterator.prototype.filterメソッドは、条件に一致する要素のみを含む新しいIteratorを返す

const numbers = Iterator.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
// 偶数のみを抽出
const evenNumbers = numbers.filter((x) => x % 2 === 0);
console.log(evenNumbers.toArray()); // => [2, 4, 6, 8, 10]

Iterator.prototype.dropメソッド

Iterator.prototype.dropメソッドは、指定した数の要素をスキップした新しいIteratorを返します。

const numbers = Iterator.from([1, 2, 3, 4, 5]);
// 最初の2つの要素をスキップ
const skipped = numbers.drop(2);
console.log(skipped.toArray()); // => [3, 4, 5]

Iterator.prototype.flatMapメソッド

Iterator.prototype.flatMapメソッドは、各要素をマップしてから結果をフラット化
このメソッドは、配列のflatMapメソッドと同じように動作

const words = Iterator.from(["hi", "fi"]);
// 各文字列を文字の配列に分割してフラット化
const chars = words.flatMap((word) => word.split(""));
console.log(chars.toArray()); // => ["h", "i", "f", "i"]

Iterator.prototype.reduceメソッド

Iterator.prototype.reduceメソッドは、すべての要素を単一の値に集約
このメソッドは、配列のreduceメソッドと同じように動作

const numbers = Iterator.from([1, 2, 3, 4, 5]);
// 合計を計算
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // => 15
// 文字列を連結
const words = Iterator.from(["Hello", "Iterator", "Methods"]);
const sentence = words.reduce((acc, word) => acc + " " + word);
console.log(sentence); // => "Hello Iterator Methods"

JSON

JavaScriptのオブジェクトリテラルをベースに作られた軽量なデータフォーマット

{
    "object": {
        "number": 1,
        "string": "js-primer",
        "boolean": true,
        "null": null,
        "array": [1, 2, 3]
    }
}

JSONオブジェクト

JavaScriptでJSONを扱うには、ビルトインオブジェクトであるJSONオブジェクトを利用する
JSONオブジェクトはJSON形式の文字列とJavaScriptのオブジェクトを相互に変換するためのparseメソッドとstringifyメソッドがある

JSON文字列をオブジェクトに変換する

パース(parse) = 「ルールに沿って読み取って、構造を作る」って意味

// JSONはダブルクォートのみを許容するため、シングルクォートでJSON文字列を記述
const json = '{ "id": 1, "name": "js-primer" }';
const obj = JSON.parse(json);
console.log(obj.id); // => 1
console.log(obj.name); // => "js-primer"

文字列がJSONの配列を表す場合は、JSON.parse静的メソッドの返り値も配列

const json = "[1, 2, 3]";
console.log(JSON.parse(json)); // => [1, 2, 3]

オブジェクトをJSON文字列に変換する

JSON.stringifyメソッドは第一引数に与えられたオブジェクトをJSON形式の文字列に変換して返す関数

const obj = { id: 1, name: "js-primer", bio: null };
console.log(JSON.stringify(obj)); // => '{"id":1,"name":"js-primer","bio":null}'

シリアライズ : オブジェクト → 文字列
デシリアライズ : 文字列 → オブジェクト

JSON.stringify() // シリアライズ
JSON.parse()     // デシリアライズ

JSONにシリアライズできないオブジェクト

シリアライズ前の値 シリアライズ後の値
文字列・数値・真偽値 対応する値
null null
配列 配列
オブジェクト オブジェクト
関数 変換されない(配列のときは null)
undefined 変換されない(配列のときは null)
Symbol 変換されない(配列のときは null)
RegExp {}
Map, Set {}
BigInt 例外が発生する

ECMAScriptモジュール

ECMAScriptモジュールの構文
ECMAScriptモジュールは、export文によって変数や関数などをエクスポートできる
また、import文を使って別のモジュールからエクスポートされたものをインポートできる、インポートとエクスポートはそれぞれに 名前つき と デフォルト という2種類の方法がある

名前つきエクスポート/インポート

名前つきエクスポートは、モジュールごとに複数の変数や関数などをエクスポートできる
エクスポートされたものがインポートできる

named-export.js
const foo = "foo";
// 宣言済みのオブジェクトを名前つきエクスポートする
export { foo };
named-export-declare.js
// 宣言と同時に名前つきエクスポートする
export function bar() { };

名前つきインポートは、指定したモジュールから名前を指定して選択的にインポートでき
my-module.jsから名前つきエクスポートされたオブジェクトの名前を指定して名前つきインポートしている
import文のあとに続けて{}を書き、その中にインポートしたい名前つきエクスポートの名前を入れる
複数の値をインポートしたい場合は、それぞれの名前をカンマで区切りにする

my-module.js
export const foo = "foo";
export function bar() { }
named-import.js
// 名前つきエクスポートされたfooとbarをインポートする
import { foo, bar } from "./my-module.js";
console.log(foo); // => "foo"
console.log(bar); // => function bar()

名前つきエクスポート/インポートのエイリアス

名前つきエクスポート/インポートにはエイリアスの仕組みがある
エイリアスを使うと、宣言済みの変数を違う名前で名前つきエクスポートできる

named-export-alias.js
const internalFoo = "foo";
// internalFoo変数をfooとして名前つきエクスポートする
export { internalFoo as foo };

名前つきインポートしたオブジェクトにも別名をつけることができる

named-import-alias.js
// fooとして名前つきエクスポートされた変数をmyFooとしてインポートする
import { foo as myFoo } from "./named-export-alias.js";
console.log(myFoo); // => "foo"

デフォルトエクスポート/インポート

デフォルトエクスポートは、モジュールごとに1つしかエクスポートできない特殊なエクスポート
「何も考えずに import したらこれ」という意図を持たせることができる

my-module.js
export default {
    baz: "baz"
};
default-import.js
// デフォルトエクスポートをmyModuleとしてインポートする
import myModule from "./my-module.js";
console.log(myModule); // => { baz: "baz" }

ECMAScriptモジュールを実行する

作成したECMAScriptモジュールを実行するためには、起点となるJavaScriptファイルをECMAScriptモジュールとしてウェブブラウザに読み込ませる必要

<!-- my-module.jsをECMAScriptモジュールとして読み込む -->
<script type="module" src="./my-module.js"></script>
<!-- インラインでも同じ -->
<script type="module">
import { foo } from "./my-module.js";
</script>

Ajax通信

エントリーポイント

エントリーポイントとは、アプリケーションの中で一番最初に呼び出される部分のこと、アプリケーションを作成するにあたり、まずはエントリーポイントを用意しなければならない

Webアプリケーションにおいては、常にHTMLドキュメントがエントリーポイントとなる
ウェブブラウザによりHTMLドキュメントが読み込まれたあとに、HTMLドキュメント中で読み込まれたJavaScriptが実行される

HTMLの用意

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>Ajax Example</title>
  </head>
  <body>
    <h2>GitHub User Info</h2>
    <script src="index.js"></script>
  </body>
</html>

index.jsにはスクリプトが正しく読み込まれたことを確認できるよう、コンソールにログを出力する処理だけを書く

index.js
console.log("index.js: loaded");
ここでのajaxappディレクトリのファイル配置は次のようになっていれば問題ありません
ajaxapp
├── index.html
└── index.js

難しかったこと

下記においては、実装のイメージができなかったので難しいと感じた。
・thisの扱い方
・非同期処理
ただPython、Djangoの時のようにアプリを作成すると理解が深まるので
今後は実際にアプリを作成しながらJSのコードに慣れていきたい。

0
0
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?