JavaScript Primerを読んでの感想
はじめに
JavaScriptの文法の勉強にJavaScript Primerという本を読みました。
今回はJavaScript Primerを読んでの感想をまとめていきたいと思います。
学んだこと
JavaScriptとECMAScript
ECMASctiptはどの実行環境でも共通な動作のみが定義されており、JavaScriptはECMAScriptと実行環境の固有機能を含んだものを表す。
変数と宣言
varの問題点
const
,let
→同じ変数名で再定義しようとすると構文エラー
var
→同じ変数名で再定義可能
var
は同じ変数名で再定義可能であるため、意図せずに再定義した場合もエラーとならず、上書きされてしまう点が問題点。
データ型とリテラル
データ型
データ型を大きく分けると、プリミティブ型とオブジェクトの2つに分類される。
プリミティブ型(基本型):真偽値や数値等の基本的な値の型のこと。
オブジェクト型:プリミティブ型以外のデータ。配列や関数も該当。オブジェクトはミュータブル特性をもつ。
typeof
演算子を用いることでデータ型を調べることが可能。
リテラル
プログラム上で数値や文字列等、データ型の値を直接記述できるように構文として定義されたもの。
以下の5つのプリミティブ型はリテラル表現を持っている。
- 真偽値
- 数値
- BigInt
- 文字列
- null
オブジェクトの中でも以下3つはリテラル表現が用意されている。
- オブジェクト
- 配列
- 正規表現
演算子
演算子はよく利用する演算処理を記号などで表現したもの。
比較演算子
// 厳密等価演算子(===) console.log(1 === 1); // 厳密不等価演算子(!==) console.log(1 !== 1); // 等価演算子(==) // 型が同じになるよう暗黙的な型変換を行ってから比較する console.log(1 == "1"); // => true // 不等価演算子(!=) console.log(1 != "1"); // => false
分割代入
分割代入を使うことで、配列やオブジェクトの値を複数の変数に同時代入可能。
const array = [1, 5]; // aとbにそれぞれ代入される const [a, b] = array; const obj = { "hoge": "fuga" }; // hogeに"huga"が代入される const { hoge } = obj;
Nullish coalescing演算子(??)
左辺の値がnullishであれば右辺の評価結果を返す。
ここでnullishとは、評価結果がnull
またはundefined
となることを指す。
const tmp = null; // tmpがnullであれば、resultに1が代入される const result = tmp ?? 1;
暗黙的な型変換と明示的な型変換
暗黙的な型変換
ある処理において、その処理過程で行われる明示的ではない型変換のこと。
1 + "2"; // => "12" 1 - "2"; // => -1
暗黙的な型変換は意図しない結果となりやすいため、比較には、等価演算子ではなく、厳密等価演算子を用いる。
関数と宣言
デフォルト引数
仮引数に対応する引数が渡されていない場合にデフォルトで代入される値を指定可能。
function double(a = 1){ }
可変長引数
任意の個数の引数を受け取ることが可能。
function fn(...args){ // 処理 }
arguments
を使って可変長引数を扱うことも可能。
function fn(){ console.log(arguments[0]); // => "a" } fn("a","b","c");
分割代入
関数においても分割代入可能。
function fn({ id }){ console.log(id); // => 1 } const user = { id: 1 }; fn(user);
アロー関数
以下特徴的なものを記載
const hogeA = arg => { /* 処理 */ }; const hogeB = arg => arg * arg;
アロー関数の特徴
- 名前をつけることができない
this
が静的に決定できる- 短く記述可能
new
できないarguments
を使用できない
コールバック関数
引数として渡される関数。
呼び出し元が用意した別の関数を、呼び出し先の処理の中から、呼び出し返す形になるため「コールバック」と呼ばれる。
高階関数
コールバック関数を引数として受けとる関数やメソッドのこと。
オブジェクト
optional changing演算子(?.)
左辺のオペランドがnullish(null or undefined)の場合に、それ以上評価せずにundefined
を返す
オブジェクトの列挙
const obj = { "one": 1, "two": 2, "three": 3 }; console.log(Object.keys(obj)); // => ["one","two","three"] console.log(Object.values(obj)); // => [1,2,3] console.log(Object.entries(obj)); // => [["one", 1], ["two", 2], ["three", 3]]
プロトタイプオブジェクト
Objectはすべての元
他のオブジェクトは全てObject
を継承している。
prototype:全てのオブジェクトの作成時に自動的に追加される特殊なオブジェクト。
プロトタイプメソッド:prototype
に組み込まれているメソッドをと呼ぶ。
プロトタイプチェーン:インスタンスからprototype
オブジェクト上に定義されたメソッドを参照できる仕組み。
Object.hasOwn
とin
の違い
Object.hasOwn:指定したオブジェクト自体が指定したプロパティを持っているかを判定。
in:オブジェクト自身が持っていなければ、オブジェクトの継承元であるprototype
オブジェクトまで探索して持っているかを判定する。
Object.prototype
を継承しないオブジェクト
Object.create(null)
とすることで、Object.prototype
を継承しないオブジェクトを作成可能。
配列
Array.prototype.at
const array = ["a", "b", "c"]; console.log(array.at(0)); // => "a" console.log(array.at(-1)); // => "c"
配列と分割代入
配列の指定したインデックスの値を変数として定義し直す場合には、分割代入が利用可能。
const array = ["one", "two", "three"]; const [first, second, third] = array; console.log(first); // => "one" console.log(second); // => "two" console.log(thrid); // => "three"
インデックスを取得
インデックス:配列の要素位置のこと
const array = ["Java", "JavaScript", "Ruby", "JavaScript"]; const indexOfJS = array.indexOf("JavaScript"); console.log(indexOfJS); // => 1 const lastindexOfJS = array.lastIndexOf("JavaScript"); console.log(lastIndexOfJS); // => 3 console.log(array.indexOf("JS")); // => -1
異なるオブジェクトだが、値は同じものを見つけたい場合は、ArrayのfindIndex
メソッドを使用する。また、末尾から検索した結果を得る場合にはfindLastIndex
を使用する。
配列から要素自体が欲しい場合は、find
メソッドを使用する。
const colors = [ { "color": "red" } { "color": "blue" }, { "color": "green" }, { "color": "blue" } ]; const indexOfBlue = colors.findIndex((obj) => { return obj.color === "blue"; }); const indexLastOfBlue = colors.findLastIndex((obj) => { return obj.color === "blue"; }); const redColor = colors.find((obj) => { return obj.color === "red"; }); console.log(indexOfBlue); // => 1 console.log(colors[indexOfBlue]); // => { "color": "blue" } console.log(indexLastOfBlue); // => 3 console.log(colors[indexOfBlue]); // => { "color": "blue" } console.log(blueColor); // => { "color": "red" }
指定範囲の要素取得
const array = ["A", "B", "C", "D", "E"]; console.log(array.slice(1, 4)); // => ["B", "C", "D"] console.log(array.slice(1)); // => ["B", "C", "D", "E"]
配列に指定要素が含まれれているかを判定する
includes
メソッドは配列に指定要素が含まれているかを判定する。
const array = ["Java", "JavaScript", "Ruby"]; if (array.includes("JavaScript")) { console.log("配列にJavaScriptが含まれている"); }
追加と削除
push
を使用することで、配列の末尾に要素を追加することが可能。また、pop
を使用することで末尾から要素を削除することが可能。
const array = ["A", "B", "C"]; array.push("D"); console.log(array); // => ["A", "B", "C", "D"] const poppedItem = array.pop(); console.log(poppedItem); // => "D" console.log(array); // => ["A", "B", "C"]
配列同士の結合
concat
メソッドを使用することで配列と配列を結合した新しい配列を作成可能。
const array = ["A", "B", "C"]; const newArray = array.concat("新しい要素"); console.log(newArray); // => ["A", "B", "C", "新しい要素"]
配列から要素を削除
splice
メソッドを使用することで、配列の任意のインデックスの要素を削除可能。
const array = ["a", "b", "c"]; array.splice(1, 1); console.log(array); // => ["a", "c"] array.splice(0, array.length); console.log(array); // => 0
配列の全ての要素を削除するにはlength
プロパティへの代入を利用した方法もある。
配列のlength
プロパティへ要素数を代入すると、その要素数に配列が切り詰められる。
const array = [1, 2, 3]; array.length = 0; console.log(array); // => []
文字列
文字列の分解と結合
split
メソッドを使用することで、文字列を配列へ分解することが可能。
また、join
メソッドを使用することで、配列の要素を結合して文字列にすることが可能。
const strings = "赤・青・緑".split("・"); console.log(strings); // => ["赤", "青", "緑"] const str = "赤・青・緑".split("・").join("、"); console.log(str); // => "赤、青、緑"
文字列の一部を取得
substring
メソッドは、第一引数に開始位置、第二引数に終了位置を指定し、その範囲を取り出して新しい文字列を返す。
const str = "ABCDE"; console.log(str.substring(1)); // => "BCDE" console.log(str.substring(1, 5)); // => "BCDE" console.log(str.substring(1, 4)); // => "BCD"
正規表現オブジェクト
正規表現オブジェクトを作成するには、正規表現リテラルとRegExp
コンストラクタを使用する方法がある。
const patternA = /パターン/フラグ; const patternB = RegExp("パターン文字列", "フラグ");
正規表現による検索
const str = "abc123def"; const searchPattern = /\d+/; const index = str.search(searchPattern); console.log(index); // => 3 const results = str.match(searchPattern); console.log(results[0]); // => 123 console.log(results.index); // => 3
matchメソッドの挙動
- マッチしない場合は、
null
を返す - マッチした場合は、マッチした文字列を含んだ特殊な配列を返す
- 正規表現の
g
フラグがある場合は、マッチした全ての結果を含んだただの配列を返す
文字列の置換/削除
Stringのreplace
メソッドを使用することで、削除したい文字を取り除いた新しい文字列を返し、削除を表現する。
const str = "文字列"; const newStr = str.replace("文字", ""); console.log(newStr); // => "列"
関数とスコープ
スコープチェーン:内側から外側のスコープへと順番に変数が定義されているかを探す仕組みのこと。
変数の隠蔽:内側のスコープで外側のスコープと同じ名前の変数を定義することで、外側の変数を参照できなくなること。
関数スコープとvarの巻き上げ
function fn() { console.log(x); // => undefined { var x = "varのx"; } console.log(x); // => "varのx" } fn();
上記コードは以下のように解釈されて実行されている。
function fn() { var x; console.log(x); { x = "varのx"; } console.log(x); // => "varのx" } fn();
変数の宣言部分が最も近い関数またはグローバルスコープの先頭に移動しているように見える動作のことを変数の巻き上げと呼ぶ。
クロージャー
ガーベージコレクション:どこからも参照されなくなったデータを不要なデータと判断して自動的にメモリ上から解放する仕組み。
クロージャー:「静的スコープ」と「参照され続けている変数のデータが保持される」という2つの性質によって成立する。
関数とthis
アロー関数以外の関数におけるthis
関数におけるthisの基本的な参照先はベースオブジェクトとなる。
ここで、ベースオブジェクトとは「メソッドを呼ぶ際に、そのメソッドのドット演算子またはブラケット演算子のひとつ左にあるオブジェクト」のことをいう。
ベースオブジェクトがない場合のthisはundefinedとなる。
thisが問題となるパターン
アロー関数以外の関数において、thisは実行したときに決定される。
→関数にthisを含んでいる場合、その関数は意図した呼ばれ方がされないと間違った結果となる。
対処法
- メソッドとして定義されている関数はメソッドとして呼ぶ。
- thisの値を指定して関数を実行する
call,apply,bindメソッド
関数オブジェクトには、call,apply,bindといった明示的にthisを指定して関数を実行するメソッドが用意されている。
call
メソッド
関数.call(thisの値, ...関数の引数);
apply
メソッド
関数.apply(thisの値、 [関数の引数1, 関数の引数2]);
bind
メソッド
関数.bind(thisの値, ...関数の引数);
コールバック関数とthis
コールバック関数ではthisの参照先が変わるため、例外が発生してしまう。
対処法
- thisを一時変数へ代入する
- アロー関数でコールバック関数を扱う
→アロー関数内のthisはスコープチェーンの仕組みと同様に外側の関数を探索する。
アロー関数とthis
アロー関数で定義された関数やメソッドにおけるthis→関数定義時に決まる。
アロー関数でない関数におけるthis→呼び出し元に依存するため関数の実行時に決まる。
アロー関数はthisを暗黙的な引数として受け付けないので、外側のスコープのthisを参照する。
クラス
クラスの定義
クラスは必ずコンストラクタをもち、constructor
という名前のメソッドとして定義する。
クラス宣言文による定義方法
class MyClass { constructor() { // コンストラクタ関数の処理。インスタンス化される時に自動的に呼び出される // 省略可能。 } }
クラス式による宣言方法
cosnt MyClass = class MyClass { constructor() {} }; const AnonymousClass = class { constructor() {} }
クラスのアクセッサプロパティの定義
クラスでは、プロパティの参照(getter)、プロパティへの代入(setter)時に呼び出される特殊なメソッドを定義可能。このメソッドはプロパティのように振る舞うためアクセッサプロパティと呼ばれる。
class NumberWrapper { constructor(value) { this._value = value; } get value() { return this._value; } set value(newValue) { this._value = newValue; } } const numberWrapper = new NumberWrapper(1); console.log(numberWrapper.value); // => 1 numberWrapper.value = 42; console.log(numberWrapper.value); // => 42
Publicクラスフィールド
クラス外からアクセスできるプロパティを定義するクラスフィールド。
class クラス { プロパティ名 = プロパティの初期値; }
Privateクラスフィールド
#
をフィールド名の前につけることで、外からアクセスができないPrivateクラスフィールドを定義可能。
定義したPrivateクラスフィールドは、this.#フィールド名
で参照可能。
class クラス { #フィールド名 = プロパティの初期値 }
静的メソッド
静的メソッド(クラスメソッド):クラスをインスタンス化せずに利用できるメソッド
静的メソッドの定義方法はメソッド名の前にstatic
をつける。
class クラス { static メソッド() { // 処理 } } クラス.メソッド();
静的クラスフィールド
静的クラスフィールドは、フィールドの前にstatic
をつける。
静的クラスフィールドで定義したプロパティは、クラス自体のプロパティとして定義される。
clas Colors { static GREEN = "緑"; static RED = "赤"; static BLUE = "青" } console.log(Colors.GREEN); // => "緑"
プロトタイプオブジェクト
プロトタイプオブジェクト:JavaScriptの関数オブジェクトのprototype
プロパティが自動的に作成される特殊なオブジェクト。
プロトタイプチェーン
プロトタイプチェーン:プロパティを参照する際に、オブジェクト自身からPrototype内部プロパティへと順番に探す仕組み。
プロトタイプチェーンは以下2つの処理から成り立つ。
- インスタンス作成時に、インスタンスの
[[Prototype]]
内部プロパティへプロトタイプオブジェクトの参照を保存する処理 - インスタンスからプロパティ(またはメソッド)を参照するときに、
[[Prototype]]
内部プロパティまで探索する処理
プロパティの参照とプロトタイプチェーン
オブジェクトがプロパティを探索するときは以下の順番で、オブジェクトを調べる。
instance
オブジェクト自身instance
オブジェクトの[[Prototype]]
の参照先- どこにもなかった場合は
undefined
継承
継承したクラスの定義
extends
を用いることでクラスを継承することが可能。
class 子クラス extends 親クラス { }
子クラスから親クラスを参照するにはsuper
を使用する。
class Parent { constructor(...args) { console.log("Parent", ...args); } } class Child extends Parents { constructor(...args) { super(...args); console.log("Child", ...args); } } const child = new Child("引数", "引数2");
コンストラクタの処理順は親クラスから子クラスの順番で行う。
クラスフィールドの継承
Publicクラスフィールドは、親クラスで定義したフィールドも子クラスに定義されるが、 Privateクラスフィールドは、親クラスで定義したフィールドは子クラスに定義されない。
例外処理
try...catch構文
tryブロック内で例外が発生すると、tryブロック内のそれ以降の処理は実行されず、catch節に処理が移行する。 catch節は、tryブロック内で例外が発生すると、発生したエラーオブジェクトともに呼び出される。 finally節は、tryブロック内で例外が発生したかどかには関係なく、必ずtry文の最後に実行される。
try { // 処理 } catch (error) { // 処理 } finally { // 処理 例外に関係なく必ず実行される。 }
throw文
throw文を使うとユーザーが例外を投げることができる。
try { throw new Error("例外"); } catch (error) { console.log(error.message); // => "例外" }
非同期処理:Promise/Async関数
同期処理
同期処理:コードを順番に処理していき、ひとつの処理が終わるまで次の処理は行わない。
非同期処理
非同期処理:コードを順番に処理していくが、ひとつの非同期処理が終わるのを待たずに、次の処理を評価する。メインスレッドで実行されることが多い。
並行処理:処理を一定の単位ごとに分けて処理を切り替えながら実行すること。一部の例外を覗き非同期処理は並行処理として扱われる。
Promise
非同期処理の状態や結果を表現するビルトインオブジェクト。
Promiseインスタンスの作成
PrmiseインスタンスのthenメソッドでPromiseがresolve、rejectしたときに呼ばれるコールバック関数を登録する。
const executor = (resolve, reject) => { }; const promise = new Promise(executor); const onFulfilled = () => { console.log("resolveされたときに呼ばれる"); }; const onRejected = () => { console.log("rejectされたときに呼ばれる"); }; promise.then(onFulfilled, onRejected);
Promiseの状態
Promise
インスタンスには、以下3つの状態が存在する。
- Fulfilled:resolveした時の状態。このときonFulfilledが呼ばれる
- Rejected:rejectまたは例外が発生した時の状態。このときonRejectedが呼ばれる
- Pending:FulfilledまたはRejectedではない状態 or
new Promise
でインスタンスを作成した時の初期状態
Async関数
Async関数は通常の関数とは異なり、必ずPromiseインスタンスを返す関数を定義する構文。
async function doAsync() { return "値"; } // doAsync関数はPromiseを返す。 doAsync().then(value => { console.log(value); // => "値" });
await式
await関数は以下の箇所で利用できる式となっている。
- Async Functionの関数の直下
- ECMAScriptモジュールの直下
await式は右辺のPromiseインスタンスがFulfilledまたはRejectedになるまでその場で非同期処理の完了を待つ。Promiseインスタンスの状態が変わると、次の行を再開する。
async function doAsync() { // 非同期処理 } async function asyncMain() { // doAsyncの非同期処理が完了するまで待つ await doAsync(); // 次の行はdoAsyncの非同期処理が完了されるまで、実行されない console.log("この行は非同期処理が完了後に実行される"); }
Map
マップの作成と初期化
コンストラクタに初期値を渡せるが、コンストラクタ引数として渡せるのはエントリーの配列。
エントリー:1つのキーと値の組み合わせを[キー, 値]
という形式の配列で表現したもの。
const map1 = new Map(); console.log(map1.size); // => 0 const map2 = new Map([["key1", "value1"], ["key2", "value2"]]); console.log(map2.size); // => 2
要素の追加と取り出し
メソッド | 内容 |
---|---|
set |
特定のキーと値を持つ要素をマップに追加 |
get |
特定のキーに紐づいた値を取り出す |
has |
特定のキーにひもづいた値を持っているかを確認する |
delete |
マップから要素を削除する |
clear |
マップが持つすべての要素を削除する |
マップの反復処理
const map = new Map([["key1", "value1"], ["key2", "value2"]]); const results = []; map.forEach((value, key) => { results.push(`${key}:${value}`); }); console.log(results); // => ["key1:value1", "key1:value2"] const keys = []; for (const key of map.keys()) { keys.push(key); } console.log(keys); // => ["key1", "key2"] const kyesArray = Array.from(map.keys()); console.log(keysArray); // => ["key1", "key2"] const entries = []; for (const [key, value] of map.entries()) { entries.push(`${key}:${value}`); } console.log(entries); // => ["key1:value1", "key2:value2"]
WeakMap
Mapと同じくマップを扱うためのビルトインオブジェクト。相違点は、キーを弱い参照でもつこと。
ここで、弱い参照とは、がベー時コレクションによるオブジェクトの解放を妨げないための特殊な参照のことである。弱い参照は例外的に、該当するオブジェクトへの弱い参照があったとしても、そのオブジェクトを解放することが可能。
→メモリリークを防ぐために使われる。
Set
Setは重複する値がないことを保証したコレクションを指す。
セットの作成と初期化
const set = new Set(); console.log(set.size); // => 0 const set = new Set(["value1", "value2", "value2"]); // value2が重複するので、片方は無視される。 console.log(set.size); // => 2
値の追加と取り出し
メソッド | 内容 |
---|---|
add |
作成したセットに値を追加する |
has |
セットが特定の値を持っているかどうかを確認する |
delete |
セットから値を削除する |
clear |
セットが持つ全ての値を削除する |
セットの反復処理
const set = new Set(["a", "b"]); const results = []; set.forEach((value) => { results.push(value); }); console.log(results); // => ["a", "b"] cosnt keysResults = []; for (const value of set.keys()){ keysResults.push(value); } console.log(keysResults); const entryResults = []; for (const entry of set.entries()) { entryResults.push(entry); } console.log(entryResults); // => [["a", "a"], ["b", "b"]]
感想
- 非同期処理に関してなんとなく知っている程度であったが、本書を読んで細かい動き等を理解することができた
- 文法に関してもとても細かい部分まで解説されており、より理解を深めることができた
- 第2章で実際にJavaScriptを使ってTodoリストの作成を行い、JavaScriptの実践的な使い方をイメージすることができ良かったと感じた
参考
付録
第2章で使用したDockerfileならびにdocker-compose.yml
Dockerfile
FROM node:lts WORKDIR /usr/src/app
docker-compose.yml
version: "3.9" services: app: container_name: app build: context: . dockerfile: Dockerfile volumes: - type: bind source: ./todoapp target: /usr/src/app command: npx --yes @js-primer/local-server ports: - "3000:3000" tty: true stdin_open: true