はじめに
JavaScript初学者です。
今回は、JavaScript Primerを読んで学んだことをアウトプットしていきます!
良かったところ
- JavaScriptの基本的な文法が網羅されていること
- 例でコードを実行して出力させて、どんな挙動をするか分かること
- この文法はこういう理由で非推奨、というように実務でどう使われているか、書いていること
悪かったところ
- 網羅的に書いてあるので、通読するのに大変
- 当たり前だが、その文法について調べたいときに読み返すのがいいと思った
学んだこと
基本文法を1から学びました。
特に気になったことを以下に列挙していきます。
Pythonで基本文法をすでに学習しているので、JavaScript固有のところが中心になります。
ECMAScriptという仕様
- ECMAScript
- 共通な動作
- 言語の核となる仕様
- ECMAScriptの後方互換性
- 過去に書いたJavaScriptのコードが動かなくなる変更はほとんど入らない
- JavaScript
- ECMAScript + 実行環境の固有機能も含む
- 例えば、Node.jsはUIを操作する機能は不要
JavaScriptとはどういう言語か
- 元々Netscape Navigatorというブラウザのための言語
- 大部分がオブジェクトである
- プロパティ: 値;の集合
- オブジェクト同士の関連で成立している言語
strict mode
- 厳格な実行モード
- 古く安全でない構文や機能が一部禁止される
- レガシー構文の禁止
- "use strict";で宣言する
- 開発者が安全にコードを書けるようにJavaScriptの落とし穴を一部塞いでくれる。
- 常にstrict modeで実行できるコードを書くことが安全なコードにつながる
実行コンテキスト
- ScriptとModuleがある
- コードを書く場合、2つの実行コンテキストの違いを意識することはない
- Script
- 多くの実行環境でデフォルトの実行コンテキスト
- Module
- デフォルトがstrict mode
- module機能を利用したい時
- Script
変数と宣言
- varを使わない理由
- 変数への再代入は、変数の値は最初に定義した値と同じである、という参照透過性と呼ばれるルールを壊し、バグを発生させやすい要因として知られている。
- 意図せず既存の変数を書き換える可能性がある
- varは後方互換性の維持のため、残されている
- 変数への再代入は、変数の値は最初に定義した値と同じである、という参照透過性と呼ばれるルールを壊し、バグを発生させやすい要因として知られている。
- let
- 初期値を指定しない変数はデフォルト値としてundefinedという値で初期化される
-
var bookTitleでbookTitleがundefinedという値が入る
- const
- 後から再代入できない変数を定義する。
- 定数ではないので、const objとしてオブジェクトを定義すると、プロパティの値は変更可能。
- ループ処理などは変数への再代入が可能なletを使おう。
データ型
- プリミティブ型
- 性質
- イミュータブルな特性を持つ
- 一度作成したらその値自体を変更できないこと
- BigInt
- ES2020から追加
- 任意制度の整数データ
- Symbol
- ES2015から追加
- 一意で不変な値のデータ型
- 性質
- オブジェクト型
- 性質
- ミュータブルな特性
- プリミティブ以外のデータ
- プリミティブ型とオブジェクト
- リテラルではなく、オブジェクトとして表現する方法もある
- ラッパーオブジェクトと呼ばれる
- ラッパーオブジェクトはリテラルのデータ型ではなく、objectである
- ラッパーオブジェクトをnewで生成する意味はない
- プリミティブ型であってもオブジェクトのように参照できるから
- プリミティブ型のデータのプロパティへアクセスするとき、暗黙的にラッパーオブジェクトへ変換するから
- プリミティブ型であってもオブジェクトのように参照できるから
- 性質
- 分割代入
const array = [1, 2];
// aには`array`の0番目の値、bには1番目の値が代入される
const [a, b] = array;
console.log(a); // => 1
console.log(b); // => 2
- 暗黙的な型変換によってfalseに変換される値をfalsyという
- falsyではない値はtrueに変換
- false
- undefined
- null
- 0
- 0n
- NaN
- ""
- AND演算子の評価
// 左辺はfalsyではないため、評価結果として右辺を返す
console.log("文字列" && "右辺の値"); // => "右辺の値"
console.log(42 && "右辺の値"); // => "右辺の値"
// 左辺がfalsyであるため、評価結果として左辺を返す
console.log("" && "右辺の値"); // => ""
console.log(0 && "右辺の値"); // => 0
console.log(null && "右辺の値"); // => null
- OR演算子の評価
// 左辺がfalsyなので、右辺の値が返される
console.log(0 || "左辺はfalsy"); // => "左辺はfalsy"
console.log("" || "左辺はfalsy"); // => "左辺はfalsy"
console.log(null || "左辺はfalsy"); // => "左辺はfalsy"
// 左辺はfalsyではないため、左辺の値が返される
console.log(42 || "右辺の値"); // => 42
console.log("文字列" || "右辺の値"); // => "文字列"
- NaN
- Not a Numberだが、Number型
- Number型と互換性のない性質のデータをNumber型へ変換した結果
関数
- 関数の引数
- 呼び出し時の引数が少ない時、余ったところにundefinedが代入される
- エラーにならない
- デフォルト引数を指定できる
- 呼び出し時の引数が多い時
- 引数の個数が多くても無視される
- 関数の分割代入
- 複数の変数へ同時に代入できる短縮記法
// 第1引数のオブジェクトから`id`プロパティを変数`id`として定義する
function printUserId({ id }) {
console.log(id); // => 42
}
const user = {
id: 42
};
printUserId(user);
- 分割代入を使わない場合
function printUserId(user) {
console.log(user.id);
}
- 使う場合
function printUserId({ id }) {
console.log(id);
}
{ id }は引数で受け取ったオブジェクトからidプロパティだけ取り出す
userのどのプロパティが必要か引数ですぐわかる
- 無名関数
- 名前を持たない関数のこと
- 関数式でも名前をつけることができる
- 再帰関数で使う時に名前をつける
- Arrow Function
// 仮引数の数と定義
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とブロックを省略できる
- Arrow Functionの特徴
- 常に無名関数
- functionと比べて短くかける
- newできない
- arguments変数を参照できない
- thisが静的に決定できる
- 基本的にArrow Functionで書く。そうでない場合はfunctionキーワードを使う
配列
- 配列のforEachメソッド
- 引数のコールバック関数には、配列の要素が先頭から順番に渡されて実行される
- 配列の全ての要素を反復処理する
- 配列のsomeメソッド
- 配列の各要素をテストする処理をコールバック関数として受け取る
- 配列に偶数が含まれているかテストする処理
function isEven(num) {
return num % 2 === 0;
}
const numbers = [1, 5, 10, 15, 20];
console.log(numbers.some(isEven)); // => true
- for ... in文
- 有用そうに見えるが多くの問題がある
- オブジェクトは何らかのオブジェクトを継承している
- for ... in文でプロパティを列挙する場合に親オブジェクトまで探索して列挙する
- 安全にオブジェクトのプロパティを列挙するには
- Object.keys
- Object.values
- Object.entries
- 配列を入れた場合はインデックスが文字化した文字列が...に入る
- 配列に対して反復処理をする場合はfor ... of文
const numbers = [5, 10];
let total = 0;
for (const num in numbers) {
// 0 + "0" + "1" という文字列結合が行われる
total += num;
}
console.log(total); // => "001"
- for ... of 文
const array = [1, 2, 3];
for (const value of array) {
console.log(value);
}
// 1
// 2
// 3
オブジェクト
-
プロパティの集合
-
プロパティはキーとバリューで対応付されているもの
-
配列や関数もオブジェクトで、あらゆるオブジェクトの元にObjectというビルトインオブジェクトがある
-
const obj = {}; -
プロパティ名と値に指定する変数名が同じ場合、省略して書ける
const name = "namae";
const obj = {
name
};
console.log(obj); // => {name: "namae";}が出力
- 分割代入でオブジェクトからよく使うプロパティを取り出す
const languages = {
ja: "日本語",
en: "英語"
};
const { ja, en } = languages;
console.log(ja); // => "日本語"
console.log(en); // => "英語"
-
const ja = languages.jaと解釈される
-
キーに変数を使う
const key = "key-string";
// Computed Propertyで`key`の評価結果 "key-string" をプロパティ名に利用
const obj = {
[key]: "value"
};
console.log(obj[key]); // => "value"
- keyだと文字列としての"key"
-
[key]は変数keyを評価した結果 - オブジェクトのプロパティはできる限り作成後に新しいプロパティは追加しない方が良い
- オブジェクトがどのようなプロパティを持っているかがわかりにくくなる
- オブジェクトリテラルの中で定義するのが推奨
- プロパティの存在確認
- プロパティ名を間違えた時、存在しないプロパティにアクセスした時、例外がでない
- undefinedという値が返ってくるだけ
- プロパティをネストして間違えてアクセスした時に初めて例外出る
- in演算子を使ってkey: valueが定義されているか調べる
-
Object.hasOwn静的メソッドを使う- プロパティ名を確認
- プロパティ名を間違えた時、存在しないプロパティにアクセスした時、例外がでない
- プロパティへアクセスする時、undefinedとの比較が冗長
-
?.演算子を使ってプロパティへアクセスする - 存在しないプロパティへアクセスした場合でも例外はでない、undefinedを返す
- A?.B
- Aがnull、undefinedだったらundefinedを返す
-
const obj = {
a: {
b: "objのaプロパティのbプロパティ"
}
};
// obj.a.b は存在するので、その評価結果を返す
console.log(obj?.a?.b); // => "objのaプロパティのbプロパティ"
// 存在しないプロパティのネストも`undefined`を返す
// ドット記法の場合は例外が発生してしまう
console.log(obj?.notFound?.notFound); // => undefined
// undefinedやnullはnullishなので、`undefined`を返す
console.log(undefined?.notFound?.notFound); // => undefined
console.log(null?.notFound?.notFound); // => undefined
- オブジェクトのプロパティへアクセスする際に、指定したプロパティ名は暗黙的に文字列に変換される
- オブジェクトであるkeyObject1をプロパティ名として指定するとkeyObject1が暗黙的に文字列化して
[object Object]がkey名になる。 - 例外的にSymbolは文字列化されない
- Mapオブジェクトはオブジェクトをキーとして扱える。
- オブジェクトであるkeyObject1をプロパティ名として指定するとkeyObject1が暗黙的に文字列化して
const obj = {};
const keyObject1 = { a: 1 };
const keyObject2 = { b: 2 };
// どちらも同じプロパティ名("[object Object]")に代入している
obj[keyObject1] = "1";
obj[keyObject2] = "2";
console.log(obj); // { "[object Object]": "2" }
- メソッドの短縮記法
const obj = {
method() {
return "this is method";
}
};
console.log(obj.method()); // => "this is method"
- オブジェクトの静的メソッド
- インスタンスの元となるオブジェクトから呼び出せるメソッド
- オブジェクトの列挙
- Object.keys
- Object.values
- Object.entries
- オブジェクトのマージと複製
- Object.assign
- あるオブジェクトを別のオブジェクトに代入できる
Object.assign(target, ...sources)- 1つ以上のsourcesオブジェクトをtargetオブジェクトへコピー
- Object.assign
- spread構文でのマージ
- 配列を展開するように、オブジェクトのプロパティを展開
const objectA = { a: "a" };
const objectB = { b: "b" };
const merged = {
...objectA,
...objectB
};
console.log(merged); // => { a: "a", b: "b" }
-
全てのオブジェクトはObject.prototypeプロパティに定義されたprototypeオブジェクトを継承している
- このオブジェクトに組み込まれているメソッドをプロトタイプメソッド
- インスタンスからprototypeオブジェクト上に定義されたメソッドを呼べることをプロトタイプチェーン
-
Object.hasOwnとinの違い
const obj = {};
// `obj`というオブジェクト自体に`toString`メソッドが定義されているわけではない
console.log(Object.hasOwn(obj, "toString")); // => false
// `in`演算子は指定されたプロパティ名が見つかるまで親をたどるため、`Object.prototype`まで見にいく
console.log("toString" in obj); // => true
- 隙間のある配列
- 値を省略することで、未定義の要素を含めることができる
- 隙間のあるものを疎な配列
- ないものを密な配列
// 未定義の箇所が1つ含まれる疎な配列
// インデックスが1の値を省略しているので、カンマが2つ続いていることに注意
const sparseArray = [1, , 3];
console.log(sparseArray.length); // => 3
// 1番目の要素は存在しないため undefined が返る
console.log(sparseArray[1]); // => undefined
- 配列の要素に相対的なインデックスを指定してアクセスする
- pythonでは末尾にアクセスするには
array[-1]でよかった - 同じようにするには.atメソッドを使う
- この-1ってlength-1を表してたのか
- pythonでは末尾にアクセスするには
- hasOwnを使ってundefinedなのか、未定義の要素なのかを判定する
- 要素自体が存在していれば、undefinedという値
- 存在していなければ未定義
- ここでundefinedと結果が出力されるが、要素は存在していないことに注意
- 破壊的なメソッドと非破壊的なメソッド
- 破壊的なメソッドは、配列オブジェクトそのものを変更してそのまま返す
- 非破壊的なメソッドは、配列オブジェクトのコピーを作成してから変更を加えて、そのコピーしたものを返す
- pushは破壊的
- concatは非破壊的
- 破壊的なメソッドは元の配列も変更してしまうので、意図しない副作用が発生し、バグの原因となるので、非破壊
ラッパーオブジェクト
-
プリミティブ型の値をインスタンス化したオブジェクト
- Boolean
- Number
- BigInt
- String
- Symbol
-
ラッパーオブジェクトはobject型
-
自動変換
- "string"等文字列は自動で
new String("string")のようなラッパーオブジェクトへ変換される - またメソッドを呼び出すときにリテラルがラッパーオブジェクトへ変換される
- 明示的にラッパーオブジェクトを扱う利点はない
- "string"等文字列は自動で
スコープ
-
スコープチェーン
- ネストされたブロックで外側のブロックはOUTER, 内側のブロックはINNERと呼ぶ
- INNERブロックからOUTERブロックスコープの変数を参照できる
- 変数を参照する際には現在のスコープから外側のスコープへと順番に変数が定義されているかを確認していく。
- どんどん上のスコープへ順番に変数が定義されているか探す仕組みをスコープチェーン
-
グローバルスコープ
- プログラム直下にある最も外側のスコープ
- ここで定義した変数はグローバル変数
- それ以外に、プログラム実行時に自動で定義されるビルトインオブジェクトがある
- undefinedなど
-
変数の隠蔽
- 内側のスコープで外側のスコープと同じ名前の変数を定義すること
- 避けるために無闇にグローバルスコープへ変数を定義しないこと
- 内側のスコープで外側のスコープと同じ名前の変数を定義すること
-
関数宣言と巻き上げ
- functionキーワードの関数宣言もvarと同じく、グローバルスコープの先頭に巻き上げられる。
// `hello`関数の宣言より前に呼び出せる
hello(); // => "Hello"
function hello(){
return "Hello";
}
// 解釈されたコード
// `hello`関数の宣言が巻き上げられる
function hello(){
return "Hello";
}
hello(); // => "Hello"
クロージャー
- 外側のスコープにある変数への参照を保持できる、という関数が持つ性質
- 関数内から特定の変数を参照し続けることで関数がまるで関数が状態を持っているように振る舞う
- myCounter -> increment -> count
- のようにcount変数を参照するものがあるので、count変数は自動的に解放されずに、値は保持し続けられる
- 静的スコープとメモリ管理という2つの性質で実現している
静的スコープ
- JavaScriptのスコープにはどの識別子がどの変数を参照するかが静的に決定される
const x = 10; // *1
function printX() {
// この識別子`x`は常に *1 の変数`x`を参照する
console.log(x); // => 10
}
function run() {
const x = 20; // *2
printX(); // 常に10が出力される
}
run();
- メモリ管理の仕組み
- ガベージコレクション
- どこからも参照されなくなったデータを不要なデータと判断して自動的にメモリ上から解放する仕組み
- ガベージコレクション
関数とthis
- thisは読み取り専用のグローバル変数のようなもの
- thisの参照先は条件で異なる
- 実行コンテキスト
- コンストラクタ
- 関数とメソッド
- Arrow Function
- thisが実際に使われるのはメソッド
クラス
- メソッドの定義
const obj = {
// メソッドの短縮記法で定義したメソッド
method() {
}
};
-
class構文
- クラスを作るための関数定義や継承をパターン化した書き方
- classキーワードを使う
- クラスは必ずコンストラクタを持ち、constructorという名前のメソッドを定義する
- クラスからインスタンスを作成する際に、インスタンスに関する状態の初期化を行う
- コンストラクタの処理が不要の場合は省略OK
-
クラス式
- クラスを値として定義する方法
- 無名関数と同じ
-
インスタンス化
- newを使ってインスタンス化
- コンストラクタ関数におけるthisはインスタンスを示すオブジェクト
- xとyプロパティに値を設定
this.x = x;
this.y = y;
-
コンストラクタにreturnでオブジェクトを返すのは避けるべき
-
プロトタイプチェーン
- インスタンスからプロトタイプオブジェクトに定義されたメソッドやプロパティを参照できる
- オブジェクトの継承元となるプロトタイプオブジェクトを内部プロパティに保持する
- インスタンス自身がプロパティを持っていなければプロトタイプオブジェクトに探索を広げる
-
クラスのプロトタイプメソッド
- クラスに対して定義したメソッドはクラスの各インスタンスから共有されるメソッドとなる
-
クラスのアクセッサプロパティの定義
- プロパティの参照 getter
- get プロパティ名とつける
- プロパティの代入 seter
- set プロパティ名とつける
- プロパティ名へアクセスするだけで、getterが呼び出される
- プロパティに値を代入するとsetterが呼び出される
- プロパティの参照 getter
-
クラスフィールド
- constructorメソッドを書かないとプロパティの初期化ができない問題
- クラスのインスタンスが持つプロパティを定義
- クラスフィールド -> constructorという順番
- クラスの外からアクセスできるプロパティ、Publicクラスフィールド
- Privateクラスフィールド
- #をフィールド名につける
- 実装したクラスの意図しない使われ方を防ぐ
- クラスの外からプロパティを直接書き換えることを防ぐ
-
インスタンスメソッド
- クラスフィールドによるメソッドの定義
- インスタンス自身にプロパティとして定義される
-
プロトタイプメソッド
- メソッド名と()を使って定義
- プロトタイプオブジェクトのプロパティとして保存される
-
どちらのメソッドが優先されるか
- インスタンスを作り、同名のメソッドを実行するとインスタンスメソッドが優先される
- プロトタイプチェーンによる仕様
-
継承
- extendsキーワードを使う
-
コンストラクタの処理は親クラスから子クラス
イテレータとジェネレータ
-
イテレータ
- 順に値を取り出す仕組み
-
ジェネレータ
- 順に値を返せる関数を定義するための構文
-
配列とイテレータの仕組み
- 配列の場合、全ての数値をメモリに格納してから処理を開始する
- イテレータは必要なタイミングで値を生成する
- 遅延評価
-
イテレーションの仕組み
- iterableプロトコル
- オブジェクトが反復可能であることを示すプロトコル
- Symbol.iteratorというメソッドでiteratorを返す
- iteratorプロトコル
- 反復処理時に次の値を取得するためのプロトコル
- nextメソッドで値を1つずつ取り出す
- iterableプロトコル
-
ジェネレータ関数
- 関数の途中で実行を一時停止し、値を返すことができる関数
- yield式は関数の実行を一時停止して、値を呼び出し元に返す
- nextメソッドが呼ばれると関数の実行が再開される
非同期処理
-
同期処理
- コードを順番に処理していく
- 一つの処理が終わるまで次の処理は終わらない
- 同期的にブロックする処理があると、ブラウザでは問題がある
- JavaScriptはブラウザのメインスレッドで実行されるため、表示の更新やスクロールもブロックされる
-
非同期処理
- 同時に実行している処理
- 非同期処理はメインスレッド(UIスレッド)で実行される
- JavaScriptの大部分の非同期処理は非同期的なタイミングで実行される処理
- メインスレッドで処理を切り替えながら実行する並行処理(Concurrent)
- 処理を一定の単位ごとにわけて処理を切り替えながら実行する
-
非同期処理の例外処理
- コールバック関数の中で同期的なエラーとしてキャッチする
- 非同期処理の外からは非同期処理の中で例外が発生したかわからないので、エラーを伝える必要がある
- コールバック関数の中で同期的なエラーとしてキャッチする
-
Promise
- 非同期処理の状態や結果を表現するビルトインオブジェクト
- 非同期処理はPromiseのインスタンスを返す
- Promiseインスタンスには状態変化をした際に呼び出されるコールバック関数を登録
// asyncPromiseTask関数は、Promiseインスタンスを返す
function asyncPromiseTask() {
return new Promise((resolve, reject) => {
// さまざまな非同期処理を行う
// 非同期処理に成功した場合は、resolveを呼ぶ
// 非同期処理に失敗した場合は、rejectを呼ぶ
});
}
// asyncPromiseTask関数の非同期処理が成功した時、失敗した時に呼ばれる処理をコールバック関数として登録する
asyncPromiseTask().then(()=> {
// 非同期処理が成功したときの処理
}).catch(() => {
// 非同期処理が失敗したときの処理
});
-
Promiseと例外
- Promiseではコンストラクタの処理で例外が発生した場合に自動的に例外がキャッチされる
-
Async Function
- 必ずPromiseインスタンスを返す関数を定義する構文
- 関数の前に
asyncをつけることで定義 -
await式という非同期処理が完了するまで待つ構文が使える
エラー
- エラーファーストコールバック
- 非同期処理中に例外を扱う方法を定めたルール
- Node.jsで広く使われている
- コールバック関数の第一引数にエラーオブジェクトまたはnullを入れ、それ以降の引数にデータを返すというルール
難しかったこと
月並みな感想ですが、非同期処理のPromise, async, awaitのイメージが難しかったです。
終わりに
実際にJavaScriptのコードを書く→JavaScript Primerを読む、を繰り返して身につけていきたいところです。
JavaScriptのまとめ資料として今後も参照していきます!