Closed10

JavaScript Primer の読書メモ

おたきおたき

第1部: 基本文法

JavaScriptとは

ブラウザの場合、UIを操作するための機能を提供して、Node.jsの場合、サーバー側の機能を提供するってこと。

「ECMAScript」はどの実行環境でも共通の部分、「JavaScript」はECMAScriptと実行環境の固有機能も含んだ範囲

strict mode

  • ファイル先頭に use strictを置くこと専用モードで実行される
  • 開発者が安全にコードを書けるようになっている

JSの仕様であるECMAScriptは毎年更新されているが、後方互換性が考慮されているので古いコードでも動かなくなることはほぼない。

コメント

  • HTML-likeコメントは初めて聞いた

変数と宣言

  • 変数には、参照透過性(変数の値は最初に定義した値と常に同じである)と呼ばれるルールがある

  • letは初期値なしで変数を宣言可能。デフォルト値はundefinedとなる

変数名のルールは以下

変数名の名前(識別子)には、次のルールがあります。

  1. 半角のアルファベット、_(アンダースコア)、$(ダラー)、数字を組み合わせた名前にする
  2. 変数名は数字から開始できない
  3. 予約語と被る名前は利用できない
    https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Lexical_grammar#予約語
  • constはあくまで「再代入できない変数宣言」であり定数を定義するわけではない

値の評価と表示

  • ブラウザやNode.jsといった実行環境には、REPL(read-eval-print loop)という機能がある

  • LFはたまに聞くけどよく知らなかった。 \nみたいなやつなのか

必ず文字コード(エンコーディング)はUTF-8、改行コードはLFにしてファイルを保存してください。
LFとは?https://developer.mozilla.org/ja/docs/Glossary/CRLF

おたきおたき

データ型とリテラル

  • オブジェクトは、値そのものではなく値への参照を経由して操作されるため、参照型のデータとも言われている
  • typeof nullobjectとなるのは、歴史的経緯のある仕様のバグなのか笑
  • typeof演算子でオブジェクトの詳細な種類を判定できない
  • 変数名の先頭に数字はダメだとなんとなく覚えていたけど、こういうことか

変数名を数字からはじめることができないのは、数値リテラルと衝突してしまうからです。

  • 文字列の中に同じ記号が出現した場合は、\でエスケープする
    • 文字列内部にある記号と異なるクオート記号であればOK
// シングルクォートを使う場合
'8 o\'clock'; // => "8 o'clock"

// ダブルクオートを使う場合
"8 o'clock"; // => "8 o'clock"
  • 改行をするときは改行記号(\n)を使う
"hogeの\nほげは\nふがだ"
  • テンプレートリテラルを使えば複数行の文字列を直感的に書けるのか
    • `(バッククォート)で囲んだ範囲を文字列とするリテラル
`複数行の
文字列を
入れたい`; // => "複数行の\n文字列を\n入れたい"
  • undefinedは単なるグローバル変数らしい、値に undefinedを持っている
    • 変数名としても使用可能
    • まああんまり変数として使ってるの見たことないしね、そうであってほしい

説明のためにundefinedというローカル変数を宣言しましたが、undefinedの再定義は非推奨です。 無用な混乱を生むだけなので避けるべきです。

  • オブジェクトのプロパティの参照方法は、.(ドット)[](ブラケット)がある
const obj = {
    "key": "value"
};
// ドット記法
console.log(obj.key); // => "value"
// ブラケット記法
console.log(obj["key"]); // => "value"
  • プリミティブ型はリテラルで表現する以外に、ラッパーオブジェクトとしても表現できる
    • typeof演算子の結果は、objectになる
    • ただ使う場面はなさそう?

明示的にラッパーオブジェクトを使うべき理由はありません。 なぜなら、JavaScriptではプリミティブ型のデータに対してもオブジェクトのように参照できる仕組みがあるため

プリミティブ型のデータでもプロパティアクセスできるのは、ラッパーオブジェクトに変換されているから。つまり、プロパティはラッパーオブジェクト固有のものなのか。

プリミティブ型のデータのプロパティへアクセスする際に、対応するラッパーオブジェクトへ暗黙的に変換してからプロパティへアクセスするためです。 また、ラッパーオブジェクトを明示的に作成するには、リテラルに比べて冗長な書き方が必要になります。 このように、ラッパーオブジェクトを明示的に作成する必要はないため、常にリテラルでプリミティブ型のデータを表現することを推奨します。

おたきおたき

演算子

  • NaNは、Not-a-Numberの略称である。数値ではないがNumber型を表現している
  • Number.isNanメソッドで判定可能
  • インクリメント演算子(++)をオペランドの前後どちらに置くかで処理順が異なる
    • num++の場合
      • 先にnumが評価される。そのあと+1する
    • ++num
      • 先に+1する。そのあとnumが評価される
  • 2つのオペランドを比較するときは、基本的に厳密等価演算子(===)を使う
    • ただし、null と undefinedの比較のみ等価演算子(==)を使ったほうが楽なのか
      const value = undefined; /* または null */
        // === では2つの値と比較しないといけない
        if (value === null || value === undefined) {
            console.log("valueがnullまたはundefinedである場合の処理");
        }
        // == では null と比較するだけでよい
        if (value == null) {
            console.log("valueがnullまたはundefinedである場合の処理");
        }
      
  • 分割代入と短絡評価の理解は大丈夫そうかな
  • Nullish coalescing演算子(??
    • nullishとは、評価結果がnullまたはundefinedになる値のこと
    • 左辺がnullisihならば、右辺の評価結果を返す
  • カンマ演算子なるものがあるのか、初めて知った

暗黙的な型変換

  • ===では暗黙的型変換をせずに、値同士の比較ができる。
  • ==の場合、異なるデータ型比較時に、同じ型になるように暗黙的型変換を行う

暗黙的な型変換とは次のことを言います。
ある処理において、その処理過程で行われる明示的ではない型変換のこと

明示的な型変換がある

  • Booleanコンストラクタ関数で真偽値に変換

  • Stringコンスタラクタ関数で文字列に変換

    Stringコンストラクタ関数での変換は、あくまでプリミティブ型に対してのみに留めるべきです。

  • Numberコンストラクタ関数で数値に変換

    • Number.parseInt(文字列, 基数)で文字列から数値を取り出せる
    • 数値以外を渡すと、変換結果がNaNとなるため、Number.isNan(x)で判定必要あり
  • NaNの性質上、自身と一致しない

function isNaN(x) {
    // NaNは自分自身と一致しない
    return x !== x;
}
console.log(isNaN(1)); // => false
console.log(isNaN("str")); // => false
console.log(isNaN({})); // => false
console.log(isNaN([])); // => false
console.log(isNaN(NaN)); // => true
おたきおたき

関数と宣言

  • 関数内でreturn 文の返り値を省略すると、undefinedが返される
    • 基本的に関数は返り値を持っている認識でいいかな
function fn() {
}
console.log(fn()); // => undefined
  • 仮引数を定義した関数を、引数を渡さずに呼ぶと undefinedが仮引数に入る
  • 仮引数に対して、引数が多い場合あふれたやつは無視される
function add(x, y) {
    return x + y;
}
add(1, 3); // => 4
add(1, 3, 5); // => 4
  • 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");
  • argumentsの扱いがむずいな、仮引数を定義しなくても実引数がすべて含まれるのか。。

Arrow Functionでは利用できない(Arrow Functionについては後述)
Array-likeオブジェクトであるため、Arrayのメソッドを利用できない
関数が可変長引数を受けつけるのかを仮引数だけを見て判断できない

基本はRest parametersで対応すればよさそう

可変長引数が必要な場合はarguments変数よりも、Rest parametersでの実装を推奨します。

関数が値として扱えることを、ファーストクラスファンクションと呼ぶ

関数式の右辺に関数名をつけるメリットは無いと思っていたが、再帰処理で使えるのか

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

これは慣れてるから大丈夫

関数の仮引数が1つのときは()を省略できる
関数の処理が1つの式である場合に、ブロックとreturn文を省略して、その式の評価結果をreturnの返り値とする

アロー関数の特徴はあまり意識してなかったから学び

名前をつけることができない(常に無名関数)
thisが静的に決定できる(詳細は「関数とスコープ」の章で解説します)
functionキーワードに比べて短く書くことができる
newできない(コンストラクタ関数ではない)
arguments変数を参照できない

間違いない、アロー関数使ってこう

Arrow Functionはfunctionキーワードの関数式に比べて、できることとできないことがはっきりしています。 たとえば、functionキーワードでは非推奨としていたarguments変数を参照できますが、Arrow Functionでは参照できなくなっています。 Arrow Functionでは、人による解釈や実装の違いが生まれにくくなります。

おたきおたき

文と式

  • if文は文のため、変数に代入できない
  • 式は文になれる(式文)が、文は式になれない
  • {}で囲った部分をブロックとい、ブロック文は例外として末尾のセミコロンが不要
おたきおたき

条件分岐

falsyな値

false
undefined
null
0
0n
NaN
""(空文字列)

switch文は、式の評価結果が ===で一致するラベルを探索しているのか。ラベルがないとdefaul節が実行される。

switch () {
    // if (式 === "ラベル1")
    case "ラベル1":
        break;
    // else if (式 === "ラベル2")
    case "ラベル2":
        break;
    // else
    default:
        break;
}
おたきおたき

ループと反復処理

do-while文初めて知った。while文と少し違い、初回の実行処理(do{}中の処理)が必ず実行される

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

break文は、while, do-while, forの中で使う。処理中のループ抜けて次の文へ移る
continue文は、次のループに移る

配列のsomeメソッドは、コールバック関数が trueを返す時点で繰り返し処理を終了して、trueを返す

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

下記メソッドは正直あんまり使ったことないので、ちゃんと理解しておきたい。MDNでたまに見る

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

const obj = {
    "a": 1,
    "b": 2,
    "c": 3
};
Object.keys(obj).forEach(key => {
    const value = obj[key];
    console.log(`key:${key}, value:${value}`);
});
// "key:a, value:1"
// "key:b, value:2"
// "key:c, value:3"
おたきおたき

オブジェクト

オブジェクトのプロパティのキーには、文字列・シンボルが利用できる
キーの指定方法はこんなかんじなのか

const obj = {
    "key": "value"
}

// keyはクォートを省略できる
const obj = {
    key: "value"
}

// ただし、変数名として利用できないプロパティ名はクォートで囲む必要あり
const obj = {
    "my-prop": "value"
}

ブラケット記法[]でプロパティに変数を利用できる

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

まあたしかに可読性わるいよね

プロパティを初期化時以外に追加してしまうと、そのオブジェクトがどのようなプロパティを持っているかがわかりにくくなります。 そのため、できる限り作成後に新しいプロパティは追加しないほうがよいでしょう。 オブジェクトの作成時のオブジェクトリテラルの中でプロパティを定義することを推奨します。

delete演算子でオブジェクトのプロパティを削除できる

const obj = { key1: "hoge" }
delete obj.key1

Object.freezeメソッドでオブジェクトのプロパティ変更を防止できる

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

const obj = { key: "hoge" };
if (Object.hasOwn(obj, "key")) {
    console.log("keyプロパティを持っています");
}

オプショナルチェイニング(?.)はよく使う

Optional chaining演算子(?.)は左辺のオペランドがnullish(nullまたはundefined)の場合は、それ以上評価せずにundefinedを返します。一方で、プロパティが存在する場合は、そのプロパティの評価結果を返します。

オブジェクトのプロパティを列挙する静的メソッド3選
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]]

ネストしたプロパティをもつオブジェクトのdeepコピーの例、、、これはすごいな

// 引数の`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
おたきおたき

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

  • Objectprototypeオブジェクトは、すべてのオブジェクト作成時に自動的に追加される特殊なもの
  • toStringvalueOfをはじめとするプロトタイプメソッドが組込まれている

Objectのインスタンスは、このObject.prototypeオブジェクトに定義されたメソッドやプロパティを継承します。 つまり、オブジェクトリテラルやnew Objectでインスタンス化したオブジェクトは、Object.prototypeに定義されたものが利用できるということです。

  • この参照できる仕組みをプロトタイプチェーンと呼ぶのか
  • Object.hasOwn静的メソッドは、対象のオブジェクトに指定したプロパティを持っているかどうかを判定。
    in演算子はプロトタイプオブジェクトまで遡って、存在するか判定する。
  • これはなるほどすぎる、すべてのオブジェクトが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も同じような Array.prototypeというのがあるのね。しかも Object.prototypeを継承してるという。

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

  • プロパティやメソッドを持たない空のオブジェクトを作成可能
// Object.prototypeを継承しない
const obj = Object.create(null);
このスクラップは2025/02/19にクローズされました