useEffectを極める:依存配列・非同期処理・レースコンディション完全理解
初めに
実務中にuseEffectを使用してコードを記述したのですが、コードレビュー時にuseEffectをなるべく使わないでほしいとのことで依頼された。 私自身useEffect自体そこまで理解していなかったし、なぜ使わないでほしいかわからなかったので、調べてまとめてしました。
useEffectの構造と使用タイミング
- useEffectはレンダー中には実行されず、画面反映された後に実行される
流れの整理
state/propsが変わる
Reactがコンポーネントを再レンダー(関数を呼び出す)
ReactがDOMを更新する
その後にEffectの中身が実行される
だからEffectの中でsetStateすると1~4が回る
function Counter() { const [count, setCount] = useState(0); console.log("render: ", count); useEffect(() => { console.log("effect: ", count); }); return ( <button onClick={() => setCount((c) => c + 1)}> {count} </button> ); }
- 上記のようなコードがあったとき、初回のレンダリングで表示されるのは
redner: 0 effect: 0が表示される
これがuseEffectはレンダー中には実行されず、画面に反映された後に実行される根拠となる
依存配列の正しい設計
依存配列に入れるべきものはeffect の中で使ってる “変わりうる値” は全部依存配列に入れる
- props、state、関数(fetchUsers みたいな)、外側で定義されてる変数で、レンダーごとに変わる可能性があるもの
function UserList({ userId, token }: { userId: string; token: string }) { const [users, setUsers] = useState<User[]>([]); useEffect(() => { const fetchUsers = async () => { const res = await fetch(`/api/users?userId=${userId}`, { headers: { Authorization: `Bearer ${token}` }, }); const data = await res.json(); setUsers(data); }; fetchUsers(); }, [/* ここに何を入れる? */]);
- 今回の場合はusers, setUsersを依存配列に入れてあげる。
useEffect(() => { console.log("effect: ", count); }, []);
- このコードでは依存配列が
空になっているので初回のみuseEffectが処理される。
ここで依存配列のルールについてまとめてみる
| 依存配列の状態 | 意味 | 実行タイミング |
|---|---|---|
なし(useEffect(() => {...})) |
依存関係を指定しない | 毎回レンダー後に実行 |
空配列(useEffect(() => {...}, [])) |
依存関係が何もない | マウント時のみ実行(最初の1回だけ) |
値あり(useEffect(() => {...}, [count])) |
countが変化した時だけ |
countが変わった時に実行 |
useEffect(() => { console.log("effect: ", count); }, [count]);
このようなコードがあった場合は依存配列に
countが含まれているのでcountの値が変わるたびにuseEffectが実行されるより専門的に言うとReactは前回の依存配列と今回の依存配列を'==='(Object is)してどれが1つでも変わっていたらuseEffectの実行を行う。
number / string / booleanのようなプリミティブ値の変化ならば値そのものが変わったか?で判定するobject / array / functionのような参照型 は中身ではなく参照(アドレス)が変わったか?で判定するの → 再レンダーされる度に参照が変化したと判断され新しい参照(メモリアドレス)が作られ、毎回effectが発火するこの部分がバグとパフォーマンス劣化の温床となる
useEffect(() => { console.log("effect"); }, [{ a: 1 }]);
このコードの場合は依存配列に{a:1}のような配列(参照型)が格納されているので際レンダーのたびに参照が変わってしまうので値が変わったと判断され、毎回useEffectの処理が走ってしまう。
そこでこれを防ぐためにuseMemoを使用する
const obj = useMemo(() => ({ a: 1 }), []); useEffect(() => { console.log("effect"); }, [obj]);
- useMemoによってオブジェクトのメモ化を行い、参照が固定されるので依存配列に入れても実行されない
// コンポーネントの外 function fetchUsersApi(userId: string, token: string) { return fetch(`/api/users?userId=${userId}`, { headers: { Authorization: `Bearer ${token}` }, }); } function UserList({ userId, token }: Props) { useEffect(() => { fetchUsersApi(userId, token).then(/* ... */); }, [userId, token]);
fetchUsersApi 関数を コンポーネントの外で定義してそれを呼び出すとする。
この場合はコンポーネントの外で定義されたただの関数
fetchUsersApiはprops/stateをキャプチャしていない、ただのユーティリティ関数、レンダーごとに中身が変わらないので依存配列に入れる必要がない。
const fetchUsers = useCallback(async () => { const res = await fetch(`/api/users?userId=${userId}`, { headers: { Authorization: `Bearer ${token}` }, }); const data = await res.json(); setUsers(data); }, [userId, token]); useEffect(() => { fetchUsers(); }, [fetchUsers]);
etchUsersはuserIdやtokenによって中身がわかるので依存配列に入れる
- useCallbackの依存が変わると新しい関数になるので依存配列にfetchUsersまたは userIdやtokenを入れるでも良い
クリーンアップ関数の役割
Reactがかた付け(クリーンアップ)のタイミングで呼ばれる関数
クリーンアップ関数は
useEffect(() => { ...return () => {...} }, [])におけるreturn () => {...}の処理のこと
useEffect(() => { const handleScroll = () => { console.log(window.scrollY); }; window.addEventListener("scroll", handleScroll); console.log("🟢 スクロール監視スタート"); return () => { window.removeEventListener("scroll", handleScroll); console.log("🔴 スクロール監視ストップ"); }; }, []);
非同期処理とuseEffectの罠
useEffect(async () => { const res = await fetch("/api/users"); const data = await res.json(); setUsers(data); }, []);
これは一見すると動きそうだが、おすすめはされない。
useEffectは同期処理であり、返り値をクリーンアップ関数を返すことを想定されている。
一方でasync()とするとで必ずPromise関数を返すことを期待され、先ほどのクリーンアップ関数を返すことを期待されることと衝突してしまう
useEffect(() => { const fetchUsers = async () => { const res = await fetch("/api/users"); const data = await res.json(); setUsers(data); }; fetchUsers(); }, []);
これが代表的な回避の仕方
中身は同じでもuseEffect自体は同期関数であり、中でasync関数を呼び出している。
この形にすることでReactの想定通りに動くようになる。
まとめ
useEffectは外界との橋渡し
useMemoは内部の計算
asyncを正しく扱うと無限ループやリークを防げる
useMemoについて
初めに
実務中に躓いた部分を中心にアウトプットして学習効率を上げてくためにブログを書きます
useMemoとは
Reactにおけるパフォーマンスの最適化フックの1つ
無駄な再計算や再レンダリングを防ぐためのもの
function Example() { console.log("レンダーされた!"); const total = heavyCalculation(); // ← これ毎回実行される! return <div>{total}</div>; }
レンダリングされるたびに重い計算を持つ関数が処理に走っているとCPUに負担がかかったり、画像遅延につながってしまう
Reactは関数コンポーネント、つまり何度でも呼び出される関数だから値が変わらなくても何度同じ計算が走る。
問題を解決するには・・・
- 親コンポーネントの更新のたびに再計算されないようにuseMemoを使用する。
const total = useMemo(() => heavyCalculation(), []);
- 依存配列を指定することによっていつ計算するのかを制御することができる。
const total = useMemo(() => heavyCalculation(value), [value]);
- valueが変わった時のみ再計算される仕組みとなっている。
再計算とはどういうこと?
- 再計算とは単純に数学的な式を計算するだけでなく、重たい計算の再計算はもちろんのこと、配列やオブジェクトの再生を防ぐなどの使用される。
function Example({ users }) { console.log("レンダーされた!"); const activeUsers = useMemo(() => { console.log("activeUsers 再計算!"); return users.filter((u) => u.isActive); }, [users]); return <div>{activeUsers.length} 人がアクティブです</div>; }
- useMemoの計算とは入力(props・state)から出力(描画用の値・配列・オブジェクト)を導き出す純粋な処理のこと
useMemoが適さない場合
- 計算が軽い場合
const doubled = useMemo(() => count * 2, [count]);
- 依存配列が頻繁に変わってしまう場合
const filtered = useMemo(() => users.filter(u => u.id === target.id), [target]);
実務で感じたuseMemoを使用するタイミング
この処理は、毎回の再レンダーで再計算する価値があるか?を問うことが重要
もし答えが"いいえ"であるならばuseMemoを使用する。
配列・オブジェクト生成
props/stateに依存しているデータ変換
子コンポーネントへのpropsとして渡すデータ構築
重い処理(検索・集計・フィルタなど
まとめ
useMemoはレンダリングのたびに重い再計算をしないようにして最適化を行うためのフック
Reactは関数コンポーネントであるので何度も再レンダリングされる →その度に入力(props・state)から出力(値)という計算が起こる
再計算は数値演算のみならず、propsや配列・オブジェクト・描画用データを導き出す純粋な処理全般のことを指す
useMemoを使用するタイミングはシンプルで
この処理は毎回再計算する必要があるか?という問いにNoの時使うべきケース
重い計算処理
配列やオブジェクトの生成
propsや配列に依存したデータ変換
子コンポーネントへpropsを安定させたい時
避けるべきケース
軽い数値演算
依存配列が頻繁に変わる時(効果が薄い)
お気に入りコード
初めに
Reactのチュートリアルを進める中で、「これは良いパターンだ!」と気づいたコードがありました。 三項演算子を変数に抽出するだけで、JSXの見通しが驚くほど良くなります。
同じように学習中の方の参考になればと思い、記事にまとめました。
参考コード
function ProductRow({ product }) { const name = product.stocked ? product.name : <span style={{ color: 'red' }}> {product.name} </span>; return ( <tr> <td>{name}</td> <td>{product.price}</td> </tr> ); }
コードの解説
{ name: "apple" price: "200円" stocked: true }
三項演算子を使用して
const name = product.stocked ? product.name : <span style={{ color: 'red'}}>と記述していて、product.stockedがtrueならば product.nameつまりappleと表示され、falseであればfalseであればappleと表示される今回なぜnameという変数に代入しているのか?JSXを記述しないのかというと可読性が上がるから
- もし直接JSXにコードを記述すると以下のような読みにくいコードになる
変数nameなし
return ( <tr> <td> {product.stocked ? product.name : <span style={{color:'red'}}>{product.name}</span>} </td> <td>{product.price}</td> </tr> )
変数nameあり
return ( <tr> <td>{name}</td> <td>{product.price}</td> </tr> )
まとめ
シンプルなコードだが、少し工夫を加えるだけで可読性が上がるだけではなく、JSXが構造だけに集中できるので把握しやすくなる。
コードの感受性を上げていき自分がコードを実装する際に生かしていきたい。
分割代入について
初めに
- 学習中に何度も検索や調べることをアウトプットすることで知識の定着を図ります。
普通のオブジェクトを分割代入で取り出す
通常の書き方
const person = { name: "太郎", age: 25, city: "東京" }; // 普通の取り出し方 const name = person.name; //一度変数にオブジェクトのプロパティを格納するという手間が必要 const age = person.age; const city = person.city; console.log(name); // "太郎" console.log(age); // 25 console.log(city); // "東京"
分割代入を使用した書き方
const person = { name: "太郎", age: 25, city: "東京" }; // 分割代入で一気に取り出す! const { name, age, city } = person; //先ほどの一度変数に格納する手間が省ける console.log(name); // "太郎" console.log(age); // 25 console.log(city); // "東京"
必要なものだけ取り出すことができる。
const person = { name: "太郎", age: 25, city: "東京" }; // nameとageだけを取り出す場合 const { name, age } = person; //先ほどの一度変数に格納する手間が省ける console.log(name); // "太郎" console.log(age); // 25
関数のパラメータを使用する
普通の書き方
function introduce(person) { //personオブジェクトにname: "太郎" age: 25を追加している。 console.log("私は" + person.name + "です"); console.log(person.age + "歳です"); } introduce({ name: "太郎", age: 25 }); // 出力: // 私は太郎です // 25歳です
値を取り出す際にはperson.nameやperson.ageを使用しなくてはいけない
分割代入を使った書き方
function introduce({ name, age }) { console.log("私は" + name + "です"); console.log(age + "です"); } introduce({ name:"太郎",age:25 }); // 出力: // 私は太郎です // 25歳です
- 直接nameやageを使用することができる
Reactでの使用
分割代入を使用しない
const props = { color: "red", value: "X" } function Square(props) { return ( <button style={{ color: props.color }}> {props.value} </button> ); } export default function Board() { return ( <> <Square value="X" color="red"/> </> ) }
- 毎回props.colorやprops.valueと記述するのは大変
#### 分割代入を使用
const props = { color: "red", value: "X" }
function Square({ color, value }) { //分割代入を使用する return ( ); }
export default function Board() {
return (
<>
### まとめ * 分割代入を使用することでプロパティ名を呼ぶだけで値を取り出すことができる * 何度もObject.propertyと記述する必要はなくなる。 * 必要なプロパティを取り出すことができる。 * 分割する変数名はオブジェクトのプロパティ名と合わせる必要がある。
関数オブジェクトとthisについての関連問題の復習
初めに
Udemyのガチで学びたい人のためのJavaScriptメカニズム
を学習しています。
関連問題で間違い箇所を復習を兼ねてアウトプットしていいます。
問題
const person = { name: "Tom", hello: function() { setTimeout(function() { console.log("Hello " + this.name);F }, 1000); } }; person.hello();
自分の回答
Hello Tomが出力されると考えた。thisはpersonオブジェクトを指すと考えたのでthis.nameはTomを指すと考えた。
回答
Hello undefined
実行の流れ
person.hello() を呼ぶ。するとhello内で
setTimeoutが登録される。(1秒後にコールバック関数が実行される)1秒経過後にタイマーがコールバック関数
(function(){...})を単体で実行する。この単体での呼び出しには呼び出し元のオブジェクトは存在しないのでthisはpersonを示さない
なぜthisがpersonを示さないのか?
thisは定義される場所ではなく、呼び出し方によって決まってくるから
ssetTimeout(function(){...})は後で単なる関数としてコールバック関数を呼び出している。 (object.method())の形で呼び出しているわけではない。object.method()の形で呼び出された時はthisはobjectを指す今回は単にfunction()と呼び出されているのでthisはpersonを示すことはない。
自分が足りないと思われること
自分が足りなかった部分としてはthisは定義場所ではなく、呼び出し方で決まるという部分の理解が足りなかった
setTimeout関数が呼び出すのは単なる関数呼び出しで、オブジェクトを直接経由はしていない。
- ただのfunction()の呼び出しを行っているだけ。
もしコールバック関数がアロー関数ならば
const person = { name: "Tom", hello: function() { setTimeout(() => { console.log("Hello " + this.name); // this = 外側の hello メソッドの this }, 1000); } };
アロー関数は独自のthisを持たないので、thisは定義時の外側を参照する。
function(){...}は独自のthisを持つ
まとめ
thisは定義された場所ではなく、呼び出し方で決まる
setTimeout関数が呼び出すのは「単なる関数呼び出し」であり、オブジェクトを経由しない
通常の function(){...} は独自の this を持ち、呼び出し方によって this が決まる
アロー関数は独自の this を持たず、外側スコープの this を参照する
JavaScript Primer - 迷わないための入門書を読んだ感想
初めに
初学者である私が、JavaScript Primer - 迷わないための入門書を読んでの感想やわかったこと、難しかったことなどを記述していこうと思います。
良かったこと
体系的に学べる
Javascriptの基礎(配列操作・イベントリスナー・関数定義など)を学び、その知識をTodoアプリなどのアプリケーション作成で活用できたのが良かったです。
単に構文やメソッドを学ぶだけでは「どこで使うのか」がわかりにくいですが、実際にTodoアプリで「配列操作がタスクの追加・削除に使われる」「イベントリスナーがチェックボックスの切り替えに使われる」など、基礎がどのように生きるかを体験できました。
学んだこと
関数と宣言
デフォルト引数とは?
- 仮引数に対応する引数が渡されていない場合に渡しておく値を指定することができる。
function 関数名(仮引数1 = デフォルト値1, 仮引数2 = デフォルト値2){ }
例文
function echo(x = "default") { return x; } console.log(echo(1)); // =>1 console.log(echo()); // => dafault
Rest parametersとは?
複数の引数を1つにまとめて受け取る構文
主に関数の引数が可変の時に使用する。
基本構文
function 関数名(...引数){ //引数名は配列として使用できる}
spread構文とは?
配列の前に...をつけた構文のこと。
関数には配列の値が展開された状態で渡される。
分割代入とは?
- オブジェクトや配列から特定の要素を変数に直接取り出すことができる構文
function printUserId({ id }) { console.log(id); // => 42 } const user = { id: 42 }; printUserId(user);
オブジェクトを受け取りその中からidのみを受け取る。
printUserId({ id }) の { id } は「分割代入」の構文
関数内でidという変数が使用できるようになる。
関数はオブジェクト
JavaScriptでは関数をオブジェクト関数とも呼ばれ、オブジェクトの一種として扱われるが、ただのオブジェクトとは異なり、関数名に()をつけることによって呼び出すことができる。
呼び出さなければ関数をオブジェクトとして参照できる。
- 他の値と同じように変数に代入したり、関数の引数として渡すこともできる。
function fn() { console.log("fnが呼び出されました"); } // 関数`fn`を`myFunc`変数に代入している const myFunc = fn; myFunc(); => //fnが呼び出されました console.log(myFunc); //console.log("fnが呼び出されました");
関数が値として扱えることをファーストクラスファンクションと呼ぶ(const myFunc = fn;)
myFuncは関数を代入したので、関数という性質を受けつでいるので関数を呼び出すような形で呼び出すことができる。
関数式
- 関数式はfunction () {...}のような関数リテラル(関数を値として定義したもの)を変数に代入したもの。
関数式の例
const greet = function() { console.log("Hello"); };
- 名前を持たない関数のことを無名関数という。
ファンクション関数(アロー関数)
- 短くかける関数定義の技法であり、=>を使用して無名関数を定義する構文
アロー関数の書き方
引数なし
() => {...}引数1つ
x = > {...}引数複数
(x,y) => {...}処理が1行のみ
x => x * x(returnと{}は必要ない)
特徴
名前をつけることができない(常に無名関数)
外側のスコープのthisを引き継ぐ。(thisは静的)
- 外側のthisを使用する。
コンストラクタとして使用することはできない。
newできない。argumetnsを使用することはできない
コールバック関数
引数として渡す関数のことをコールバック関数という
一方でコールバック関数を引数として受け取っている関数を高級関数という。
コールバック関数は非同期処理でも多く使用される
コールバック関数
- オブジェクトのプロパティとして定義されている関数をメソッドと呼ぶ
const obj = { method: function() { return "this is method"; } }; console.log(obj.method()); // => "this is method"
メソッドの短縮記法
- ES2015からはメソッドを短縮して記述する方法がとられている。
const obj = { method() { return "this is method"; } }; console.log(obj.method()); // => "this is method"
条件分岐
if文
- プログラム内に条件分岐を行うことができる。
if (条件式) { 実行する文; }
条件式がtrueであれば処理を実行する。
条件式にはtrueまたはfalse以外にも値が入り、その場合は真偽値へ変更してからtrue or falseを判定する。
else if文
- 複数の条件分岐を行う際にはif文に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");
}
//ECMAScript 2015が出力される。
else 文
- 条件に一致しなかった文の処理を記述する。
const number = 6; if (number > 10) { console.log(`numberは10より大きいです: ${number}`); } else if (7 <= number && number < 10) { console.log(`numberは7以上10未満: ${number}`); } else { console.log(`numberは7以下です: ${number}`); }
switch文
- 式の評価結果が指定した値になる場合に処理する
switch (式) { case ラベル1: // `式`の評価結果が`ラベル1`と一致する場合に実行する文 break; case ラベル2: // `式`の評価結果が`ラベル2`と一致する場合に実行する文 break; default: // どのcaseにも該当しない場合の処理 break; } // break; 後はここから実行される
break文はswitch文から抜け出して次の文に移行するための文
break文は省略することができるが、それがないとたとえ最初の文で処理されていても、switch文から抜けることができず、その後の処理も実行されてしまう。
ループと反復処理
- 同じ文や処理があった場合にはwhileやforなどと言ったループ処理を行ってくれる文を使用する。
while文
- 条件式がtrueであれば反復処理が実行される。
while (条件式) { 実行する文; }
条件式の評価結果がtrueならば処理の実行。falseならば終了
処理が実行されたら最初に戻り条件式の評価が始まる。 例文
console.log('ループ開始'); let z = 0; while (z < 5) { console.log(z); z += 1; } console.log('ループ終了');
出力
ループ開始
0
1
2
3
4
ループ終了
while文は無限ループを起こす可能性があるので、他の方法で同じような処理を解決できないか考えることも大切。
無限ループが起こったとしても慌てずにcontroll + cをクリックする。
for文
- 繰り返す範囲を指定した反復処理
for (初期化式; 条件式; 増分式) { 実行する文; }
function sum(num) { let total = 0; for (let i = 0; i < num.length; i++) { total += num[i]; } return total; } console.log(sum([1,2,3,4,5,]));
出力
15
配列のforEachメソッド
- for文のような反復処理を行うメソッド。
const array = [1, 2, 3]; // forEachは"コールバック関数"を受け取る高階関数 array.forEach(コールバック関数);
forEachは高級関数
コールバック関数を引数として持っている。
const array = [1,3,5];
array.forEach(value => {
console.log(value * 2 );
});
///2,6,10
function sum(nums){ let total = 0; nums.forEach(num => { total += num*2; }); return total; } sum([3,4,5]);//出力は24
- forEachはfor文に相当するiがなく、シンプルに記述することができる
配列のsomeメソッド
someメソッドは配列の各要素をテストする処理をコールバック関数として受け取る。
コールバック関数が一度でもfalseを返すとそこで反復処理を中止しsomeメソッドはtrueを返す。
配列のsomeメソッド
ある配列の中にコールバック関数を満たす要素が1つでもある場合はtureを返す。
全てfalseの場合はfalseを返す。
条件はコールバック関数で定義を行う。
function isEven(num) { return num % 2 === 0; } const numbers = [1, 5, 10, 15, 20]; console.log(numbers.some(isEven));
コールバック関数はisEven
isEvenに引数を渡すタイミングとしてはnumbers.some(isEven)が呼ばれた時に渡されている。
continue文
ループの中で次の繰り返しへスキップするために使用される処理。
主に繰り返し処理のwhile, for, do...while で使える。
// `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]
条件に合わないものをスキップしたい時に使用する。
if文でネストが多くなった時に使用することでネストを浅くできる可能性がある。
配列のfilterメソッド
配列の中の特定の値のみを抽出して配列にしたい時に使用するメソッド。
各要素の処理をコールバック関数を実行することによって行う。
trueを返した要素だけ配列に残す。
function isEven(num) { return num % 2 === 0; } const array = [1, 5, 10, 15, 20]; const result = array.filter(isEven); // [10, 20]
for + if + contineを使用しないので可読性が上がる。
配列を変更なしで取り出すことができる。
今回はコールバック関数を用いて処理を行った。
for...in文
- オブジェクトのプロパティに対して反復処理を行う。
for (プロパティ in オブジェクト) { 実行する文; }
for (const key in obj) {
const value = obj[key];
console.log(`key:${key}, value:${value}`);
}
問題としては継承されたプロパティも列挙してしまう。
- 親オブジェクトのプロパティも列挙してまう可能性があり、意図しない挙動になる。
配列に使用してしまうと、配列のインデックスが列挙されて、数値の加算にならない。
- 配列に対して反復処理を行う場合はforEachメソッドやfor文を使用する。
for...of文
for (const value of iterable) { // 実行する処理 }
interableオブジェクトに対して1つずつ値を取り出して反復処理を行う構文
interableとは反復処理の動作を定義したオブジェクト。
const arrays = [2,3,4]; for (const value of arrays) { console.log(value); } //2 //3 //4
オブジェクト
- オブジェクトとはキーとバリューが1対1になっている集合体のようなもの
オブジェクトの作成
- プロパティ名と定義する変数名が同じならば、キーとバリューの1対1で記述しなくても省略してかける。
const name = "名前"; // `name`というプロパティ名で`name`の変数を値に設定したオブジェクト const obj = { name }; console.log(obj); // => { name: "名前" }
{}はObjectのインスタンスオブジェクト
プロパティ名へのアクセス
オブジェクトのプロパティ名にアクセスする方法として2つあり、1つ目はドット記法、2つ目がブランケット記法
([])ドット記法は命名規則を守る必要がある。
ブランケット記法は命名規則はなく任意のプロパティ名を記述することができる。
- プロパティ名が暗黙的に文字列に変換されるという注意点が存在する。
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"
- ブランケット記法ならばプロパティ名には変数も指定することができる。
const languages = { ja: "日本語", en: "英語" }; const myLang = "ja"; console.log(languages[myLang]); // => "日本語"
プロパティの存在確認: in演算子を使う
- in演算子は指定したオブジェクト上に指定したプロパティが存在するかをtrueまたはfalseで返す。
"プロパティ名" in オブジェクト; // true or false
- 継承されたオブジェクトも判定の結果に影響を及ぼす。
プロパティの存在確認: Object.hasOwn静的メソッド
- オブジェクト自身が指定されたプロパティが存在するかtrueまたはfalseで返す。
const obj = {}; // objが"プロパティ名"を持っているかを確認する Object.hasOwn(obj, "プロパティ名"); // true or false
- プロパティの値に関係なくオブジェクトに指定したプロパティが存在するかを判定する。
const obj = {};
// objが"プロパティ名"を持っているかを確認する
Object.hasOwn(obj, "プロパティ名"); // true or false
const obj = {key: undefined }; if (obj.hasOwnProperty("key")) { console.log("`obj`は`key`プロパティを持っている"); }
Optional chaining演算子(?.)
- ifを使用せずに安全にコードを減らしてプロパティがあるかを確認することができる。
function printWidgetTitle(widget){ const title = widget?.window?.title ?? "未定義"; console.log(`ウェジェットタイトルは${title}です`); } printWidgetTitle({ window: { title: "BV" } }); printWidgetTitle({ }); ///出力:ウィジェットのタイトルはBook Viewerです :ウィジェットのタイトルは未定義です
?.で一度undefineまたはnullでないか確認してくれる。そこundefineまたはnullとなると
undefineを返す??がundefineの場合にデフォルト値として返してくれる。
Optional chaining演算子(?.)はブランケット記法と組み合わせておくこともできる。
const languages = { ja: { hello: "こんにちは" }, en: { hello: "Hello" }, }; const langJapanese = "ja"; const langKorea = "ko"; const messagekey = "hello"; console.log(languages?.[langJapanese]?.[messagekey]); console.log(languages?.[langKorea]?.[messagekey]); ///出力 こんにちは undefined
ブラケット記法だとの中は文字列化されるので直接プロパティ名を記述するときは文字列にするために""が必要。
今回は変数を使用しているので内は""が必要なくなる。
toStringメソッド
オブジェクト自身を文字列化するメソッド。
文字列化するとはオブジェクトを1つの説明的に文にすること
stringコンストラクタを使用することで文字列化を行うことができる。
const obj = { key: "value" }; console.log(obj.toString()); // => "[object Object]" // `String`コンストラクタ関数は`toString`メソッドを呼んでいる console.log(String(obj)); // => "[object Object]"
- オブジェクトを使用するために、stringコンストラクタも使用することができる。
オブジェクトの静的メソッド
- インスタンスの元となるオブジェクトから呼び出されるメソッドのこと。
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]]
Object.assign静的メソッド[ES2015]
1つ以上のオブジェクトから別のオブジェクトにコピーすることができる。
オブジェクトの複数やマージを行うことができる。
マージやコピーをする際に同じキーがある場合、後のもので上書きされる。
const obj = Object.assign(target, ...sources);
オブジェクトのspread構文でのマージ
オブジェクトのマージを行う際にはspreed構文を使用することでぼジェクトリテラルの中に指定したプロパティを展開することができる。
spreed構文は新しいオブジェクトリテラルの中でしか記述できない。
const obj1 = {a: 'a'}; const obj2 = {b: 'b'}; merge = { ...obj1, ...obj2 }; console.log(merge); ///{a: 'a', b: 'b'}
オブジェクトの複製
新しく空のオブジェクトを作成してそこに既存のオブジェクトをコピーすることで複製することができる。
オブジェクトを複製しているので元のオブジェクトとは異なる。
const shallowClone = () => { return Object.assign({}, obj) } const obj = {a: 'a'}; const cloneObj = shallowClone(obj); console.log(cloneObj); console.log(obj === cloneObj) ///{a: 'a'} ///false
Object.assign静的メソッドは浅くコピーすることでる
浅いコピー(shallow copy)はsourceオブジェクト直下のにあるプロパティのみコピーするといこと
深いコピー(deep copy)は一番内側までコピーを行うこと
プロトタイプオブジェクト
- オブジェクトの中に自動的に実装されるメソッドをビルトインメソッドと呼ぶ。
Objectはすべての元
ObjectはFunction,Array,Stringに最初から組み込まれている標準オブジェクト
ほとんど全てのオブジェクトはObject.prototypeプロパティに定義されたprototypeオブジェクトが継承されている。
prototypeオブジェクトはオブジェクトを作成する際にほぼ全てのオブジェクトに自動で追加されるオブジェクト。
prototypeオブジェクトに組み込まれているオブジェクトをプロトタイプメソッドという。
オブジェクトにないプロパティを親から自動的に探してくる仕組みをプロトタイプチェーンという。
プロトタイプメソッドとインスタンスメソッドの優先順位
Object.hasOwn静的メソッドとin演算子との違い
Object.hasOwn静的メソッドは自分自身がメソッドを持っているか、持っていないかを判別するメソッド。
- 継承元まで遡らないので注意。
in演算子はオブジェクト自身が持っていれば、その継承元のprototypeオブジェクトまであるか遡って判別する。
- インスタンスに実装されたメソッドなのか、プロトタイプオブジェクトに実装されたオブジェクトなのかを判別できない。
const object = { }; console.log(Object.hasOwn(object, "toString")); //false console.log("toString" in object);// true
は{}を作成した時プロトタイプオブジェクトを継承している。toStringメソッドはそのプロトタイプ上に存在している。
Object.hasOwn(object, "toString") は自分自身がtoString を持っているかを判別するためfalse。
toString" in objectは継承されたプロパティまでに遡って判別するためtrue。
const object = { toString() { return "toString has own" } }; console.log(Object.hasOwn(object, "toString"));//=> trueを返す。
オブジェクトの継承元を明示するObject.create静的メソッド
第一引数に指定したprototypeオブジェクトを継承した新しいオブジェクトを生成することができる。
Object.create.prototyepであるオブジェクトにprototyepオブジェクトを継承するオブジェクトを作成するということ。
const parent = { role: "parent" }; const child = Object.create(parent); console.log(child.role); // => "parent"
Object.create静的メソッドを使用することで任意の順番で継承順序を変えてオブジェクトを作成することができる。
継承とは親のメソッドやオブジェクトを遡って探せる仕組みのこと。
ArrayもObjectを継承している
- ArrayのインスタンスもObjectを継承していてプロトタイプチェーンを持っている。
array → Array.prototype → Object.prototype → null
- 同名の複数のメソッドが存在する時、より手前にあるArray.prototype.toStringが優先的に使用される。
配列
配列の作成とアクセス
配列の中に隙間がある配列を疎な配列。
隙間なく、全ての配列に要素がある配列を密な配列
// 未定義の箇所が1つ含まれる疎な配列 // インデックスが1の値を省略しているので、カンマが2つ続いていることに注意 const sparseArray = [1, , 3]; console.log(sparseArray.length); // => 3 // 1番目の要素は存在しないため undefined が返る console.log(sparseArray[1]); // => undefined
Array.prototype.at
配列の末尾にアクセスするにはat(-1)を使用すれば良い。
array[array.length - 1]に比べると文字数も少なく、直感的でわかりやすい
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"
オブジェクトが配列かどうかを判定する
Array.isArray静的メソッドはオブジェクトが配列かどうかを判定するメソッド。
配列ならばtrue、配列でなければfalseを返す。
const obj = {}; const arry = []; console.log(Array.isArray(obj));//false console.log(Array.isArray(arry));//true
typeof演算子では配列なのかオブジェクトなのか判別はできない。
元々[]はobjectを元にして作成されている。Objectを継承している
TypedArray
TypedArrayは固定長かつ、型が決まっている配列。
バイナリデータを扱うための配列。
厳密には
[]とは違うオブジェクトになる。
配列から要素を検索
配列から指定した要素を検索する目的は3つ
その要素のインデックスがほしい時
その要素自体がほしい時
その要素があるという真偽値がほしい時
インデックスの取得
indexOfメソッドやfindIndexメソッドを使用する。indexOfメソッドは引数に指定する要素のインデックスを取得する。その際に===で一致する要素を先頭から検索して該当する要素のインデックスを返す。- 該当する要素がない場合は-1を返す。
lastIndexOfは末尾から検索をかけてインデックスを取得する。indexOfは先頭から検索をかけて1番最初の値のインデックスを返す
lastIndexOfはインデックス番号は逆にならないので注意
const obj = {key: "value"} const arry = ["A", "B", obj]; console.log(arry.indexOf({key: "value"}));//-1 console.log(obj === { key: "value" });//false console.log(arry.indexOf(obj));//2
オブジェクト同士はプロパティが同じでも参照先が異なれば異なるものとみなされる。
indexOfを使用してオブジェクトを探すときは同じ中身ではなく、同じ参照先(インスタンス)であるかが重要
- 異なるオブジェクトだが、同じ値を見つけたい場合は
findIndexを使用する。
- 異なるオブジェクトだが、同じ値を見つけたい場合は
const colors = [ { "color": "red" }, { "color": "green" }, { "color": "blue" } ]; const indexOfBlue = colors.findIndex((obj) => {return obj.color === "blue"; }); console.log(IndexOfBlue);//2 console.log(colors[indexOfBlue]);//{color: "blue" }
findIndexメソッドは各要素をテストする関数をコールバック関数として渡す。
- 特定の条件に合うオブジェクトを見つけることができる。
条件に一致する要素を取得
明確に要素が欲しいという時にはArrayの
findメソッドを使用する。findメソッドは引数うにコールバックを使用することができるのでfindIndex同様に柔軟に条件を変えることが可能になる。findLastメソッドは末尾から検索した最初の結果を返す。
指定範囲の要素を取得
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)); // => []
真偽値を取得
- 配列から指定した要素の真偽値を明確に取得したい時はarryの
includeメソッドを使用する。
const array = ["Java", "JavaScript", "Ruby"]; if(array.includes("Java")) { console.log("Javaあります"); }//javaあります
異なるオブジェクトから指定した要素の真偽値をえたい場合は
someメソッドを使用する。- コールバック関数を使用して真偽値を得ることができる。
const colors = [ { color: "yello" }, { color: "blue" }, { color: "red"} ] const blueColor = colors.some((color) => { return color.color === "blue" }) console.log(blueColor);//true
追加と削除
配列に要素を追加するメソッドは
pushメソッド。引数に追加する値を渡すことで、末尾に追加される。配列の末尾の要素を削除するメソッドは
popメソッド。unshiftメソッドは配列の先頭に要素を追加するメソッド。shiftメソッドは入れつの先頭の要素を削除するメソッド。
const array = ["A", "B", "C", "D", "E"]; array.push("Z"); console.log(array);//['A', 'B', 'C', 'D', 'E', 'Z'] array.pop(); console.log(array);//["A", "B", "C", "D", "E"] array.unshift("S"); console.log(array);//['S', 'A', 'B', 'C', 'D', 'E'] array.shift(); console.log(array);//['A', 'B', 'C', 'D', 'E']
配列をフラット化
flatメソッドを使用することで、多次元配列をフラットな配列に変換することができる。引数に値を指定することで、フラット化する深さまで指定することができる。
完全にフラットにするには引数に
infinityを追加する。
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"]
配列から要素を削除
spliceメソッドは配列の任意のインデックス要素を削除することができる。削除した要素のインデックスを自動で詰めることができる。
要素を追加することもできる。
array.splice(開始インデックス, 削除数, 追加要素1, 追加要素2, ...);
lengthプロパティへの代入
let arry = [1,2,3]; console.log(arry.length);//3 arry.length = 0; console.log(arry.length);//0
Array-likeオブジェクト
配列のように見えるが配列ではないオブジェクト。
Array.isArray()を使用することで、引数に渡されたオブジェクトがArray-likeオブジェクトか本物のArrayオブジェクトかを判別することができる
Array.from()を使用すること、またはspread構文を使用することでArray-likeオブジェクトも本物のArrayオブジェクトに変換することができる。
メソッドチェーンと高階関数
メソッドチェーンはあるメソッドの返り値に対してさらにメソッドを呼ぶ一連のパターン。
- 処理を連続してすっきり記述することができる。
高級関数は関数の引数の中にさらに関数を入れることができる関数。
map,filter,reduceメソッドが当てはまる。
文字列
文字列を作成する
- バッククォートの特徴として複数の文字列をそのまま記述することができる。また
${}を使用して変数や式を埋め込むことができる。
エスケープシーケンス
| エスケープシーケンス | 意味 |
|---|---|
| \' | シングルクォート |
| \" | ダブルクォート |
| \` | バッククォート |
| \\ | バックスラッシュ |
| \n | 改行 |
| \t | タブ |
| \uXXXX | ユニコードユニット |
| \u{X} | ユニコードポイント |
文字列の分解と結合
splitメソッドは文字列を配列に分解するメソッド
joinメソッドは要素を分解して文字列にするメソッド
- splitメソッドとjoinメソッドを組み合わせることで新たな文字列にするパターンが多い。
文字列の比較
- 文字列の比較はCode Unit同士の比較を行っている。
文字列の一部を取得
slice、substringメソッドは文字列の一部を取り出すメソッド。sliceメソッドは第一引数には開始位置、第二引数には終了位置(終了位置の要素は含まない)を指定してその範囲で文字列を取り出す。第二引数を省略したら末尾までが選択範囲とする- 引数にマイナスを指定すると末尾からカウントされる。
正規表現オブジェクト
あるパターンにマッチするように柔軟に検索を行うことができる。
-
- 第一引数にはマッチする範囲を決めるパターンと第二引数には正規表現を検索モードを指定するフラグの2つで構成される。
\ ^ $ . * + ? ( ) [ ] { } |は特殊文字と呼ばれ特別な意味を持つ。
// 正規表現リテラルで正規表現オブジェクトを作成 const patternA = /パターン/フラグ; // `RegExp`コンストラクタで正規表現オブジェクトを作成 const patternB = new RegExp("パターン文字列", "フラグ");
正規表現リテラルとRegExpコンストラクタの違い
正規表現リテラルはソースコードをロードしたタイミングで評価される。
- コードを書いた時点で決まったパターンの正規表現オブジェクトを作成する。
RegExpコンストラクタは関数と同じようにRegExpコンストラクタが予備されてから評価される。
- 実行時に変わることがあるパターンの正規表現オブジェクトを作成する。
正規表現によるインデックスの取得
searchメソッドは正規表現にマッチした箇所のインデックス番号を返す。マッチする文字列がない場合は-1を返す。
const str = "ABC123EFG"; const regex = str.search(/\d{3}/); console.log(regex); // 3
正規表現によるマッチした文字列の取得
- 正規表現のマッチした文字列を取得するメソッドは
matchメソッドとmatchAllメソッドが存在する。
"文字列".match(/パターン/); console.log("文字列のかたまり".match(/文字列/));//['文字列', index: 0, input: '文字列のかたまり', groups: undefined]
- gフラグがあるとマッチする文字列全てを取り出すことができる。
const str = "ABC あいう DE えお"; const alphabet = /[a-zA-Z]+/g; const result = str.match(alphabet); console.log(result);//['ABC', 'DE'] console.log(result.length);//2 console.log(result.indext);//undefined console.log(result.input);//undefined
index,inputともにundefineになる。なぜなら複数マッチする場合は1つの
indexプロパティでは意味が一意にならないからmatchAllatorは複数マッチした場合でもinteraオブジェクトを取得することでどこでマッチしたのか、どの文字列で検索したのか、全て取り出すことができる。
マッチした文字列の一部を取得
const [マッチした全体の文字列, キャプチャ1, キャプチャ2] = 文字列.match(/パターン(キャプチャ1)と(キャプチャ2)/);
真偽値の取得
const str = "にわにはにわにわとりがいる"; console.log(/^にわ/.test(str));// true console.log(/$いる/.test(str));// true console.log(/には/.test(str)); // true
- /^パターン/は先頭一致、/$パターン/は後方一致、/パターン/は部分一致
文字列の置換/削除
文字列.replace("検索文字列", "置換文字列"); 文字列.replace(/パターン/, "置換文字列");
replaceAllメソッドは全ての一致箇所を置換することができる。正規表現でなく、文字列もすべて置換できる
文字列.replaceAll("検索文字列", "置換文字列")
replaceやreplaceAllメソッドはキャプチャグループを利用して複雑な置換も行うことができる。
文字列.replace(/(パターン)/g, (all, ...captures) => { return "置換後の文字列"; })
タグ付きテンプレート
Code Point
UnitCodeが文字に割り当てた一意のID
codePointAt(index)は文字列からCode Pointを取得する。String .formCodePoint(code)はCode Pointから文字列を生成する。\u{Code Pointの16進数}とすることで文字を表現可能
Code Unit
文字列をUTF-16で構成する最小単位(16ビット = 2バイト)
javascriptの配列はCode Unitの配列で扱われる。(
charCodeAtで取得)文字列によっては1つのCode Pointが2つのにCodeUnit分かれている場合がある(サロゲートペア)
正規表現の.とUnicode
正規表現にフラグ
uが追加されてCodeUnit単位ではなく、CodePoint単位で扱えるようになる。- サロゲートペアは上手くマッチできず、文字化けを起こしてhしまう。
意図しないマッチを防ぐために正規表現にはフラグ
uをつけることが望ましい
Code Pointの数を数える
lengthプロパティを使用すればCodeUnitの数を数えることができる。
CodePointを数えるには直接的なメソッドがあるわけではないが
Array.from()を使用して文字列1ずつ取り出して配列にしてlengthプロパティを使用して数えることができる。
ラッパーオブジェクト
- プリミティブ型を包み込んで、オブジェクトとして振舞わせる仕組みのこと
プリミティブ型とラッパーオブジェクトの自動変換
- プリミティブ型の値に対してプロパティアクスセスやメソッドの呼び出しを行う際には自動的に一時的にラッパーオブジェクトに変更される。
関数とスコープ
スコープ
変数や関数を参照できる範囲を示す。
スコープ内で定義された変数はスコープ内でしか参照できない。
仮引数もスコープ内でのみ参照できる。
ブロックスコープ
{}で囲まれた範囲をブロックと呼び、そこに定義された変数はその中でのみ使用することができる。constやletで定義された変数は外から呼び出せない。
スコープチェーン
- 変数を現在のスコープから外側に向かって順番に探そうとする仕組み
グローバルスコープ
プログラムの最も外側にあるプログラム
変数をプログラム直下に書くことで自動的にグローバルスコープに定義される。
全てのスコープから参照可能
- 無闇にグローバルスコープに変数を定義しないことが重要。
関数などの小さいスコープ内に変数を閉じ込めることで影響を最小限にすることができる
関数スコープとvarの巻き上げ
- varは巻き上げと関数スコープという仕様があるので予測しづらく、安全性は低い。
関数スコープとvarの巻き上げ
function hello ()などの関数宣言は先頭に巻き上げられる。
hello(); // => TypeError: hello is not a function var hello = function() { return "Hello"; };
- 関数式の場合は変数名だけ巻き上げられ、値(関数)は巻き上げられないので注意が必要。
クロージャー
- クロージャーは関数が外側のスコープにある変数へ参照を保持し続ける仕組み
静的スコープ
- どの変数がどの識別子に結びつくかがコードの場所によって決まってくるスコープの仕組み。
メモリ管理の仕組み
ガーベージコレクションは参照しなくなったデータを自動で開放して削除する仕組み。
クロージャーは静的スコープと参照され続けている変数のデータが保持されているという2つの性質によって成り立っている。
- 外側のスコープにある変数の参照を内部関数が保持し続けること
関数とthis
スクリプトにおけるthis
実行コンテキストがscriptの場合,thisはグローバルオブジェクトを参照する。
- ブラウザのグローバルオブジェクトはwindowオブジェクト、Node.jsのグローバルオブジェクトはglobalオブジェクトになる。
モジュールにおけるthis
実行コンテキストがモジュールの場合はそのトップレベルに書かれたthisはundefinedとなる
globalThisは実行環境に依存することなくグローバルオブジェクトを参照することができる。
メソッドの種類
オブジェクトのプロパティが関数である場合はそれをメソッドと呼ぶ
一般的にはメソッドを含めたものを関数と呼ぶ
通常の関数式
const obj = { method1: function() {} };
- アロー関数
const obj = { method2: () => {} };
- 短縮記法(メソッド構文)
const obj = { method() {} // ← functionキーワード不要 };
- 呼び出しの方法としては
obj.method();
Arrow Function以外の関数におけるthis
functionやメソッドではthisは関数の呼び出し方によって変わってく
thisは暗黙的な引数のように呼び出された時の実行環境に渡される値
ベースオブジェクトはメソッドを呼ぶ際にそのメソッドのドット演算子、またはブラケット演算子の1つ左側に存在するオブジェクトのことを指す。
関数宣言や関数式におけるthis
関数内でthisを使用してもベースオブジェクトがない場合は意味がなく、undefinedになってしまう。
- thisを使用する場合はベースオブジェクトを持つメソッドとして使用する
メソッド呼び出しにおけるthis
メソッド(オブジェクトのプロパティとして定義した関数)におけるオブジェクトのルールとしてthisは呼び出し元(ベースオブジェクト)を参照する。
ネストされている場合はドットの直前のオブジェクトを参照する。
thisが問題となるパターン
- メソッドを変数に代入してただの関数として呼び出すとthisの参照先が失われる。
"use strict"; const person = { fullName: "Brendan Eich", sayName: function() { return this.fullName; } }; const say = person.sayName; say(); // => TypeError(thisはundefined)
対処法
関数やメソッドのthisを明示的に指定することでオブジェクトとのつながりが切れるということを防ぐことができる
- call、apply、bindメソッドを使用して明示的に指定する
callメソッドは第一引数にはthisとして指定したい値、残りの引数には呼び出したい関数の引数を記述する。
`関数.call(thisの値, ...関数の引数);`
- applyメソッドは第一引数にはthisとして指定したい値、第二引数には関数の引数を配列として指定する。
関数.apply(thisの値, [関数の引数1, 関数の引数2]);
- bindメソッドはthisの値を束縛して新しい関数を生成する。
関数.bind(thisの値, ...関数の引数); // => thisや引数がbindされた関数
Arrow Functionとthis
arrow function自身はthisを持つことをせず、定義された時点で外側のスコープのthisを参照する
- Arrow Function内のthisは呼び出し方に関係なく外側に依存する
クラス
クラスの定義
クラス宣言は
class キーワードで宣言するconstractorはインスタンス化じに一度だけ呼ばれるイニシャライズするメソッドクラス名は省略することができない。
クラス式はクラスを式として代入する
名前付きでも名前なしでもできる。
無名クラスは無名関数のように振る舞うことができる 。
constractorは特に処理がなければ省略することができる。
クラスのインスタンス化
あるインスタンスが指定されたクラスから作成されたものか判断するには
instanceofメソッドを使用することができる。コンストラクタの中では
returnは原則書かずに、thisを使用して初期化に使う
クラスのプロトタイプメソッドの定義
クラス構文には直接メソッドを定義することができる。
- メソッド内の
thisはそのメソッドを呼び出したインスタンスのことを指す。
- メソッド内の
クラス内にメソッドを定義する際には
メソッド名(){}と記述する。プロトタイプメソッドとはクラス内で定義された、メソッドは全てのインスタンスのプロトタイプを通じて共有されているメソッドのこと。
クラスのアクセッサプロパティの定義
クラスではプロパティの参照や代入のときに呼び出されるメソッドがある。これをアクセサプロパティと呼ぶ。
- アクセサプロパティはメソッドの前に参照ならばget、代入ならばsetを記述することで定義できる。
Array.prototype.lengthをアクセッサプロパティで再現する
javascriptの配列ではlengthプロパティに値を代入すると自動で要素数を調節することができる。
アクセサプロパティを使用することでプロパティにアクセスした時のような振る舞いをカスタムできる。
Publicクラスフィールド
- クラスフィールドを使用することでconstructorを使用しなくても簡単に初期化を行うことができる。
class クラス { プロパティ名 = プロパティの初期値; }
- Publicクラスフィールドは外部から自由にアクセスすることができるクラスフィールドのこと
クラスフィールドを使ってプロパティの存在を宣言する
クラスフィールドは初期値を省略可能。
- このクラスにはこのプロパティがある」と宣言的に示すため。
クラスフィールドでのthisはクラスのインスタンスを示す
クラスフィールドの初期値に this を使うと、それはクラスのインスタンスを指す。
関数(メソッド)も値として代入できる。
アロー関数と組み合わせることでthisは外側のスコープに固定されるので安全にthisが使用することができる
Privateクラスフィールド
プライベートクラスフィールドは#プロパティ名とすると外部からプロパティ名を呼び出すことができなくなる。
外から呼び出そうとするとsyntaxerrorが発生する。
最初に必ず定義が必要
静的メソッド
静的メソッドを定義するにはメソッド名にstaticを使用する。
クラスをインスタンス化せずに呼び出せるメソッド。
静的クラスフィールド
静的クラスフィールドはプライベート名の前にstaticというキーワードを記述することでクラスをインスタンス化しなくても直接プロパティを呼び出すことができる
静的プライベートクラスフィールドはクラス外からアクセスできないクラスフィールド
#の前にstaticというキーワードを記述することによってインスタンス化しなくてもクラス内からアクセスできる
プロトタイプに定義したメソッドとインスタンスに定義したメソッドの違い
プロトタイプメソッドを定義するとプロトタイプオブジェクトへ定義される。
プロトタイプオブジェクトはprototypeプロパティが生成されると自動で生成される特殊なオブジェクト。
クラスも一種の関数オブジェクトだから自動でプロトタイプオブジェクトが生成される。
プロトタイプの2つの処理
インスタンス作成とプロトタイプチェーン
Prototype内部プロパティを取得するにはObject.getPrototypeOf静的メソッドを使用する。
Object.getPrototypeOf静的メソッドをうまく活用することでをインスタンスがどこのクラスから作成されたか把握することができる。
プロトタイプチェーンとはあるオブジェクトのプロパティを参照するとき、まず自身のクラスにプロパティがないか探し、見つからない場合には
[[Prototype]]までたどって見つけ出そうとする。この上流の方まで見つけ出そうとする仕組みのことを言う
継承
継承は
extendsキーワードを使用することで新しいクラスが既存のクラスのメソッドやプロパティを引き継ぐこと。継承したクラスを定義するには
class子クラス extends 親クラス {}superはextendsによって継承された親クラスのメソッドやコンストラクタなどを呼び出すための構文
コンストラクタの処理順は親クラスから子クラスへ
親クラスと子クラスで継承されている時はコンストラクタの処理の順番は親から子の順番に処理が実行される。
継承の際にはsuperは子クラスの処理を行う前に行うことでReference errorを防ぐことができる。
プロトタイプの継承
- プロトタイプチェーンによって、子クラスのインスタンスは親クラスで定義されたプロトタイプメソッドを継承し、呼び出すことができる。
静的メソッドの継承
- クラス同士(コンストラクタ関数同士)にもプロトタイプチェーンがあり、extends によって継承関係が作られると、子クラスは親クラスの静的メソッドをプロトタイプチェーンを通じて利用できる。
superプロパティ
子クラスの
constructorから親のconstructorを呼ぶ時に使用する子クラス内でオーバーライドしたメソッドから親のメソッドを呼びたい時ときに
super.method()で行う
例外処理
try...catch構文
try{}catch{}finally{}を使用することで例外処理を行うことができる。
try節にエラーが起こりそうな文を記述。
例外が発生するとtry節は中断されてcatch節へ移動する。
catch節では発生したエラーオブジェクトを引数として受け取り、例外処理を行う。
finallyは例外関係なく処理を記述する
catch節またはfinally節のどちらか片方あればどちらかを省略することができる。
但し、例外発生時にcatch節が存在しないと例外はスルーされてしまい、finally節実行後に例外が発生する。
try { console.log("try節:この行は実行されます"); // 未定義の関数を呼び出してReferenceError例外が発生する undefinedFunction(); // 例外が発生したため、この行は実行されません } catch (error) { // 例外が発生したあとはこのブロックが実行される console.log("catch節:この行は実行されます"); console.log(error instanceof ReferenceError); // => true console.log(error.message); // => "undefinedFunction is not defined" } finally { // このブロックは例外の発生に関係なく必ず実行される console.log("finally節:この行は実行されます"); }
throw文
throw文を使用するとユーザーがエラーを投げることができる。
エラーオブジェクトにエラーを格納してそれをcatch節で捕獲することができる。
thorwできるものはエラーオブジェクトでなくても良いが、スタックトレースのためにエラーオブジェクトを投げることが推奨される。
エラーオブジェクト
- Errorオブジェクトとはnew Errorでインスタンスを生成する。第一引数にはエラーメッセージとなる文字列を挿入する。
ReferenceError
- ReferenceErrorは存在しない変数や関数などの識別子を参照しようとするとエラーになる。
SyntaxError
SyntaxErrorは構文的に不正なコードを解釈しようとしたときに生じるエラー
javascriptを実行する前のパース段階で起こるエラー。
実行前に起きるSyntaxErrorは例外処理のcatchでエラーを捕獲することができない。
console.errorとスタックトレース
Error Cause
エラーをキャッチしてさらに投げることをすることで新しいエラーメッセージを投げることができるが、これでは本来のスタックトレースが失われてしまう。
非同期処理:Promise/Async Function
同期処理は処理が上から行われ、1つの処理が終わるまで次の処理を実行することはしない。
非同期処理は基本上から順番に行われるが、待機時間のあるような処理は後回しにして、次の処理を行う。
非同期処理はメインスレッドで実行される
同期処理も非同期処理も同じメインスレッド上で処理を行なっているのでお互いに影響を受けて処理を行う。ただし非道処理の待機時間のみはメインスレッドをブロックしない。
並行処理は処理を一定の単位ごとに分割して交互に処理することであたかも同時に処理しているように見せかける仕組み
- 非同期処理の前に重たい処理があると非同期処理も実行が遅くなってしまう現象が起こる
並列処理はメインスレッドとは別のスレッドやCPUを使用して処理を物理的に同時並行に行う仕組みのこと
非同期処理と例外処理
- try..catchは同期処理においての例外をキャッチすることができる仕組みであり、非同期処理であるsetTimeoutで例外が起きても外側のtry..catchではキャッチできない。
非同期でもキャッチするには?
- 非同期処理でも例外をキャッチするには非同期で個別にtry..catchを使用する。
setTimeout(() => { try { throw new Error("非同期で発生したエラー"); } catch (e) { console.error("個別にキャッチ:", e.message); } }, 1000);
Promise
promiseインスタンスは状態や結果を表現し、その結果に応じて呼び出されるコールバック関数を登録する
new演算子でオブジェクトを作成し、コンストラクタにはresoleveとrejectの2つの引数を渡すことができるexecutor関数と呼ばれる関数を渡す。
- 非同期処理が成功したときにはresolve関数を呼び、失敗したときにはreject関数を呼ぶ
function asyncPromiseTask() { return new Promise((resolve, reject) => { // さまざまな非同期処理を行う // 非同期処理に成功した場合は、resolveを呼ぶ // 非同期処理に失敗した場合は、rejectを呼ぶ }); } // asyncPromiseTask関数の非同期処理が成功した時、失敗した時に呼ばれる処理をコールバック関数として登録する asyncPromiseTask().then(()=> { // 非同期処理が成功したときの処理 }).catch(() => { // 非同期処理が失敗したときの処理 });
thenメソッドには第一引数には成功した時のコールバック関数、第二引数には失敗した時のコールバック関数を記述する。
thenメソッドは成功した時のコールバック関数を登録することもできるし、失敗した時のコールバック関数のみも記述することができる。
thenメソッドに失敗時のコールバック関数のみを登録したい場合は第一引数にはundefineを記述する必要がある。しかしこれは非推奨。
Promiseの状態
promiseの状態には3つ存在していてFulfilled成功した状態、Rejected失敗した状態、PendingはFulfilledやRejectedでもない状態のことを言う
promiseの状態は一度FulfilledやRejectedになったら変わることはない。このことを不変(Settled)という
promiseコンストラクタ内で何度もresolve関数を呼びだしても一度しか呼び出されない。
Promise.resolve
- Promise.resolve静的メソッドは
Fulfilledの状態となったPromiseインスタンスを生成する。
Promise.reject
Promiseチェーン
Promiseチェーンとはpromiseインスタンスに対して、thenやcatchを繋げることで複数の非同期処理を順番に行うことができる構文。
ccatchはエラーの状態を処理して完了するとPromiseインスタンスの状態が成功(Fulfild)になる
- catchでエラーがキャッチされ処理が正しく終了すると、その時点でPromiseインスタンスの状態は成功(Fulfild)の状態に変化するので,次のthenで非同期処理を登録・実行できる。
Promiseチェーンで値を返す
- promiseチェーンではコールバック関数によって返した値が次のコールバック関数の引数として渡される
Promiseチェーンの最後に処理を書く
- finaryメソッドはPromiseが成功しても失敗しても一度だけコールバック関数を登録するメソッドなのでリソースの取得が確実に終わっていることを示すことができる。
Promiseチェーンで逐次処理
Promiseにおける逐次処理はthenを繋げていくことで実現できる。
- PromiseチェーンはPromiseが返す非同期処理呼び出しながら成功や失敗時のコールバック関数を登録、実行する仕組み
Promise.allで複数のPromiseをまとめる
- Promise.allは複数のpromiseを使用した非同期処理をまとめて並行処理し、その結果を配列として返す。
- 引数には配列を指定する。
- 渡したPromiseが全てfulfildの状態であればthenの処理が進むが、1つでもrejectがあれば失敗時の処理が進む。全てのpromiseがfulfildになると結果を配列として返す。
async function doAsync() { return "値"; } // doAsync関数はPromiseを返す doAsync().then(value => { console.log(value); // => "値" });
Async Function
Asybc Funtionは関数をPromise関数として扱うことができ。asyncを関数名の前に付けることでPromiseインスタンスを常に返えす。awaitは対象のpromiseの状態がFulfilledまたはRejectedに変化するまで非同期処理の完了を待機します。そしてPromiseインスタンスの状態が変わると次の処理に移る。awaitを使用することでtry...catch構文でエラーをキャッチすることができる。- 非同期処理があたかも同期処理のように上から順番に処理される 。
Promise APIとAsync Functionを組み合わせる
awaitを使用すると非同期処理が同期的な見た目で記述できるようになるが、実行順に処理されるので非同期処理の並列実行による効率的な処理が失われる。そこでpromise.all()を使用することで複数の非同期処理を同時に開始して全ての完了を待つことによって全体の処理時間を短縮できる。
Map/Set
要素の追加と取り出し
- mapメソッドの要素の追加はsetメソッドを使用し、追加した要素を取り出すにはgetメソッドを使用することができる。
- setメソッドは同じキーで値を指定した場合はそのキーの値は上書きされてしまうので注意が必要。
- getメソッドは特定のキーに紐づいた値を取り出す。
- hasメソッドは特定のキーに値が存在するかを確認するためのメソッド。
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
マップの反復処理
keysメソッドはマップの持つ全ての要素のキーを挿入順に並べたIteratorオブジェクトを返す- Itreratorオブジェクトとは順番に値を取り出せる仕組みを持ったオブジェクトのとこ
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"]
entryメソッドはmapが持つ全ての要素をエントリーとして挿入順に並べてたIteratorを返す。- エントリーは[キー,値]の組み合わせ
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"]
Map.groupBy静的メソッド
- Map.groupBy静的メソッドは配列からグループ分けしたマップを作成する
- Object.groupByと全く同じメソッドで配列からグループ分けしてマップを作成したメソッド。
- 第一引数には配列などのiteratorオブジェクト。第二引数にはグループ分けの条件を返すコールバック関数を入れる。
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
Mapはプロパティとは異なる仕組みでデータを格納する。
マップとしてのオブジェクトはキーと値の組み合わせで情報を持つオブジェクト
WeakMap
- weakMapはmapと同じくビルトインオブジェクト
Set
- Setはセット型のビルドインオブジェクト
セットの反復処理
- setが持つ値をforEachを使用してset順に反復処理を行うことができる。
const set = new Set(["a", "b"]); const results = []; set.forEach((value) => { results.push(value); }); console.log(results); // => ["a","b"] 実行
setからiteratorを作成するメソッドとしてはkey,value,entriesがある。
- keyとvalueは挿入順に列挙してitreratorを返す。
- entriesメソッドは[値, 値]といった形のエントリーを挿入順に返す
Set自身もiteratorなのでfor..fo構文を使用することができる。
const set = new Set(["a", "b"]); const results = []; for (const value of set) { results.push(value); } console.log(results); // => ["a","b"]
JSON
JSONはjavascriptのオブジェクトリテラルをベースに作成された軽量なデータフォーマット
- 文字列、数値、真偽値、null、配列、オブジェクト、この6つの基本型の組み合わせることによって表現する。
- 人間にとって理解しやすく、マシンにとっても解析しやすいのでwebAPIや多くのファイル形式で使用されている。
JSONは主に外部のアプリケーションとのやり取りで使用されるのであり、毎回必ずしも正しい形式のJSONを受信するとは限らない。そのため
JSON.parse()を使用した時に,syntaxerrorが生じる可能性がある。そこでtry...catch構文を使用することでエラーが起きた時に対処することでアプリケーションのクラッシュや予期せぬ動作を防ぐことができる。
const userInput = "not json value"; try { const json = JSON.parse(userInput); } catch (error) { console.log("パースできませんでした"); }
オブジェクトをJSON文字列に変換する
* JSONオブジェクトのままだとHTTP通信を使用してデータのやり取りができないからオブジェクトから文字列にする必要がある。そこでJSON.stirngifyメソッドを使用することでJSONオブジェクトからJSON形式の文字列に変換することができる。 * stringfifyメソッドの第一引数には文字列に変換したいJSONオブジェクト、第二引数には文字列にする際にどのようにコントロールしたいかを記述する関数や配列を記述する。
const obj = {id: 1, name: "sun", bio: null}; const replacer = (Key, value) => { if (value === null) { return undefined } return value } const json = JSON.stringify(obj, replacer) console.log(json);///{"id":1,"name":"sunny"}
const obj = {id: 1, name: "sun", bio: null}; const json = JSON.stringify(obj, ["id"]) console.log(json); ///{"id":1}
- stringifyの第3引数はspace引数とも呼ばれていて、数値を入れることでスペースを入れることができたり、\tを使用することでインテンドを追加することができる。
toJSONメソッドを使ったシリアライズ
- toJSONメソッドをプロパティとして持つときオブジェクトに対してJSONstringify()を実行すると、そのオブジェクトのtoJSON()の返り値がシリアライズした結果を返す。
Date
- Dateはビルトインオブジェクト
- Dateオブジェクトをインスタンス化することである特定の時刻を表すオブジェクトが得られる。
- Dateにnew演算子を使用することでインスタンスを作成することができる。
- 現在の時間をインスタンス化したい場合はDate.now静的メソッドを使用する。
- 任意の時間を取得するにはgettimeメソッドやtoISStringメソッドを使用するとその時刻をUTCにおける文字列に変換できる
Math
数値の大小を比較する
- Math.max()は引数に複数の数値を渡した中で最も大きい数を返す。
- Math.min()は引数に複数の数値を渡した中で最も小さい数を返す。
- min、maxも可変長の引数を取るので任意の個数の数値を比較することもできる。数値の配列を展開したい場合は...(spread構文)を使用して値の展開をすることで比較ができる。
数値を整数にする
- 少数を丸めるために使用されるメソッドとしてはfloor,ceil,roundなどがある。
- floorメソッドは引数として渡した数以下で最大の整数にするメソッド。このような関数を床関数と呼ばれる。
- ceilメソッドは引数として渡した数以上で最小の整数にするメソッド。このような関数を天井関数と呼ぶ。
- roundメソッドは一般的な四捨五入を行うメソッド。少数が0.5よりも小さい場合は切り捨てられ、それよりも大きい場合は切り上げられる。
- truncメソッドは単純に小数点以下を切り捨てる整数を返す。
ECMAScriptモジュール
- ECMAScriptモジュールはexport文によって関数や変数をエクスポートしたり、import文によって別のモジュールからエクスポートされたものをインポートすることができる。
- エクスポートやインポートには名前付きとデフォルトのという2種類の方法がある。
名前つきエクスポート/インポート
- 名前付きエクスポートはexportの次を{}を挿入してその中にすでに定義した関数名や変数名をまとめて記述してエクスポートすること
定義していない関数や変数をエクスポートする際には
export const 変数名 = 値とする。exportを定義の前に記述するエイリアスを使用すると宣言済みの変数を違う名前に変更してインポート、エクスポートができる。
- 同じ名前の関数や変数を複数インポートすることはできない。よって同じ名前の関数や変数をインポートする場合はエイリアスが必要となる。
デフォルトエクスポートはモジュールごとに1つしかエクスポートできない特殊なエクスポート
- export defaultと記述することでデフォルトエクスポートを行うことができる。
- 関数宣言すると同時にデフォルトエクスポートも行うことができる。
- 変数宣言と同時にデフォルトエクスポートを行うことはできない。
- デフォルトインポートは指定したモジュールのデフォルトエクスポートに名前をつけてインポートする
- import文に任意の名前をつけることによってデフォルトエクスポートをインポートできる。
デフォルトインポートと名前付きインポートは同時に行うことができ、カンマで区切り、名前付きインポートを{}内に記述することで組み合わせることができる。
dynamic importは動的にモジュールをインポートするための機能です。
- 通常のインポートでは静的インポートのためファイルの先頭に記述する必要があり、条件分岐の中でモジュールをインポートすることはできない。
- dynamic importを使用することでボタンをクリック時にモジュールを読み込むことができたり、大きなライブラリを必要になったタイミングで読み込むことができる。
- import演算子という関数呼び出しによく似た構文を使用する。この関数はPromiseを返すのでthenメソッドやasync/await構文を組み合わせて使用する。
インポート属性はモジュールをインポートする際に情報の追加を行うために指定する構文
- インポートするモジュールの情報をコード上に明示して、読み込み方法を制御することができる。
with { type: "json" }と追加情報を記述することで読み込むファイルがjsonであることを明示的に示すことができる。
プロポーザルの機能を試す
- ECMAscriptでまだリリースしていない提案中の機能を現在のjavascript環境であたかも使用できるようにするために
TranspilerやPolyfillを使う。
エントリーポイント
- エントリーポイントとはアプリケーションが一番初めに呼び出される部分。
- webアプリケーションにおいてhtmlドキュメントが常にエントリーポイントになる。
HTTP通信
Fetch API
- FetchAPIはHTTP通信を使用してリソースを取得するためのAPI
難しかったこと
モデルやイベントを使用して責務の分離を行う部分が特に難しいと感じました。
今回はUI操作を担当するView、ViewとModelの橋渡しをするApp、データ更新や管理を行うModelで役割分担を実施しました。
これによりAppクラスが肥大化せず、責務を分離できます。
構造はMVCモデルに近いですが、ViewはItemView(1タスク単位)とListView(複数タスク管理)、ModelはItemModel(1タスク)とListModel(全タスク)に分かれており、どこで細分化するかの基準を見極めるのが課題です。
暫定的には「1クラス=1責務」「複数の要素や状態を直接扱い始めたら分割」をルールとし、実装と読み返しを繰り返して理解を深めたいと思います。
最後に
JavaScript Primerはボリュームがあり、初学者の私には非常に難しく感じました。
特にアプリケーション作成課題は、コードを理解するだけで精一杯で、自力で書くことはまだ難しいと感じます。
今後はアプリ作成部分だけでも1週間に1度は読み直し、最終的にコードを見ずに作成できる状態を目指します。
また、JavaScript学習で躓いた時には必ず本書に立ち返り、内容を覚えてしまうくらい繰り返し読み込むことで、確実に技術力を向上させていきます。
名前空間インポートについて
初めに
学習中にうまく言語化できなかった部分について自分が納得いくまで考えて言語化したものを備忘録がてらに書いていきます。
名前空間インポートとは
- あるモジュールから名前付きエクスポートされている機能を1つにまとめてオブジェクトにしてそれをインポートすること。
// math.js export const pi = 3.14; export function add(a, b) { return a + b; } // 使う側 import * as math from "./math.js"; console.log(math.pi); console.log(math.add(1, 2));
エクスポートとは
- ECMAScriptモジュールのexportには2種類あり、名前付きエクスポートとデフォルトエクスポートが存在する。
名前付きエクスポートとは
名前付きエクスポートは指定したモジュールから変数名や関数名を指定してそれをexportすること
- すでに定義された関数や変数をエクスポートするパターンと定義と同時にエクスポートするパターンがある。
// math.js const pi = 3.14; function add(a, b) { return a + b; } // 定義後にまとめてエクスポート export { pi, add };
export const pi = 3.14; export function add(a, b) { return a + b; }
名前付きエクスポートは必ず名前を指定する必要がある。
1つのモジュールからいくつでも名前付きエクポートすることができる。
インポートするときは 同じ名前で受け取る必要がある。
デフォルトエクスポートとは
- デフォルトエクスポートとは1つだけエクスポートすることが可能で、インポート時は自由に名前を決めることができる
// myFunc.js
export default function (x) {
return x * 2;
}
// インポート側
import double from './myFunc.js';
インポートとは
他もモジュール内で定義した変数や関数をエクスポートしたものを自分のモジュール内で参照できるようにすること
インポートには大きく分けて3種類あり、名前付きインポート、デフォルトインポート、名前空間インポート
名前付きインポートとは
- モジュールが名前付きエクスポートしたものを名前指定を行い、インポートすること。
// math.js export const pi = 3.14; export function add(a, b) { return a + b; } // 使う側 import { pi, add } from "./math.js";
{}内はexport時に指定されてた名前を記述する。
- asを使用することで別の名前を指定することもできる。
複数の名前を指定して同時にインポートすることもできる。
デフォルトインポートとは
- モジュールがデフォルトエクスポートした値を1つ取り込むこと。
// logger.js export default function log(msg) { console.log(msg); } // 使う側 import log from "./logger.js"; // 名前は自由に付けられる
{}なしでOK1つのモジュールに1つだけ。
以上の用語を頭に入れた上で再度名前空間インポートについて説明してみる。
指定されたモジュール内にある名前付きエクスポートされた複数の関数や変数をまとめて1つのオブジェクト(名前空間)として束ねて受け取り、それをインポート先のモジュールにインポートする機能のこと。但しデフォルトエクスポートしたものは名前空間インポートできない。
なぜわざわざ名前空間インポートを使用するのか?名前付きインポートだけでも十分使用できるのではないか?
モジュールのエクスポートが多くて、まとめて扱いたい
モジュールのエクスポートをが多い時にまとめてインポートしたいときに便利
一括にまとめてオブジェクトにすることができる
名前の衝突を避けられる
他のモジュールからインポートするときにもし同じ名前の関数や変数をインポートする場合はリネームが必要になる。
名前空間インポートならばその必要がない。
API を「所属」と一緒に見せたい
- 大規模プロジェクトやチーム開発をする際にはどのモジュールの関数なのかを明示的に示すことは可読性や保守性の部分で有利になる。
エクスポートの数が変動する場合に安全
- 将来的にエクスポート先の変数や関数が追加される可能性がある場合は空間インポートならばインポート側で追加する必要がない。
名前付きインポートだけではダメなのか?
ほとんどの場合は名前付きインポートのみでも対応可能である。
- 但し列挙の手間、衝突回避、可読性において不利になる可能性がある。
コアモジュールや一部のライブラリではドキュメントが名前空間を使用しているので それに合わせると学習コストが下がる。