この記事は僕がちゃんとプログラミングを勉強し始めるきっかけとなったJSの問題に関する内容の記事になります。
はじめに
最近ではNext.jsを使った長期インターンに参加できるほどに少しはマシになったとは思いますが、半年前まではJavaScriptを少しかじっただけで、何も理解していませんでした。
しかし、ある時期に今回ご紹介する問題を 否が応でも 解かないといけない状況になり、JS-Primer(JSの教科書的なやつ)と睨めっこしながら奮闘しなんとか解き、先輩方にそれをレビューしてもらいました。
そこで 、今回の記事では、
前よりは成長した僕の知見と、先輩エンジニアの方々のアドバイスを融合させつつ
- 昔の僕のコードの何がダメなのか
- より良い解答(1を踏まえて、どこを改善すると良くなるのか)
- 補足説明
この3点に着目しつつ解説していこうと思います!
前置き
この問題は(ほぼJSの知識だけで解けますが)Typescripで出来ています。
もし、ローカル環境で解いてみたいという方は
Typescriptチュートリアル-環境構築編-という記事を参考にして頂き、
ブラウザで手軽にいきたい方は
Typescript Playgroundというブラウザ実行環境などを使ってみてください!
(別に、環境構築など色々用意しなくても読めるようにしていますので、ご安心を。)
また、恐らく至らぬポイントがあると思います。その場合はコメントしていただいて、より良いコードにするにはどうすればいいかご教授して頂きたいです!
よろしくお願いします。
お待たせ致しました!それではやって行きましょう!
問題
お時間に余裕のある方、自分の解いたものと後で解説するものを見比べたい方などは、是非一度解いてみてください!
(ちなみに、この問題を見た時、当時の僕は「ど、どうすりゃええの?」と固まりました笑)
/**
* 犬の情報が格納された配列 dogs があります。
* dogs に対して操作を行う下の4問に回答してください。
*/
type Dog = {
// ID
id: number;
// 犬の種類
type: string;
// 体重
weight: number;
};
const dogs: Dog[] = [
{
id: 1,
type: "toyPoodle",
weight: 4,
},
{
id: 2,
type: "husky",
weight: 20,
},
{
id: 3,
type: "pug",
weight: 7.5,
},
];
// 例題: 全ての犬の type を順番にconsoleに出力してください。
// Answer:
dogs.forEach((dog) => {
console.log(dog.type);
});
// 問題1: dogsからweightが8未満の対象を取り除いた配列をconsoleに出力してください。
// Answer:
// 問題2: dogsからidが3のDogオブジェクトを取得してconsoleに出力してください。
// Answer:
// 問題3: dogs内の全Dogオブジェクトに isCat: false を追加した配列をconsoleに出力してください。
// Answer:
// 問題4: dogs 内の全ての weight を合計した値を返却する関数を作成してください。
// Answer:
解けましたか??
ちなみに当時の僕は(レポート期間ではあったけど)3日ほどかかりました笑
解説
ここからは
- 昔の僕のコードとそれのダメなポイント
- より良い解答(1を踏まえて、どこを改善すると良くなるのか)
- JS-Primerで補足説明
の3つを軸に解説して行きます!
例題
準備運動です。
全ての犬の type を順番にconsoleに出力してください。
問題の再掲
type Dog = {
// ID
id: number;
// 犬の種類
type: string;
// 体重
weight: number;
// 真偽値
isCat?: boolean;
};
const dogs: Dog[] = [
{
id: 1,
type: "toyPoodle",
weight: 4,
},
{
id: 2,
type: "husky",
weight: 20,
},
{
id: 3,
type: "pug",
weight: 7.5,
},
];
そもそも例題なのに解説する必要ないのでは…と思ったそこのあなた!
実はあるんですよ。改善点が。
(ただ、これは改善してもしなくてもOKですが)
dogs.forEach((dog) => {
console.log(dog.type);
});
出力結果
// "toyPoodle"
// "husky"
// "pug"
改善案
1行程度の処理であれば、まとめてあげると ()やreturnを無くせて見やすくなる。
dogs.forEach(dog => console.log(dog.type));
補足説明
アロー関数の仮引数が1つのときは()を省略できる
-
JS-Primer Arrow Function
こんな感じでやっていきます。
問題1
ここからが本番です。
(ダメな部分はコードの中に❶ ❷
のような形で書き、それがダメポイントの解説番号と連動するようにしてます)
問題の再掲
type Dog = {
// ID
id: number;
// 犬の種類
type: string;
// 体重
weight: number;
// 真偽値
isCat?: boolean;
};
const dogs: Dog[] = [
{
id: 1,
type: "toyPoodle",
weight: 4,
},
{
id: 2,
type: "husky",
weight: 20,
},
{
id: 3,
type: "pug",
weight: 7.5,
},
];
dogsからweightが8未満の対象を取り除いた配列をconsoleに出力してください。
昔の僕のダメな解答()
const filterdWeightArray❶ = ❸[dogs.filter((weight❷) => {
return (
weight.weight > 8
)
})];
console.log(filterdHardnessArray);
出力結果
// [[{
// "id": 2,
// "type": "husky",
// "weight": 20
// }]]
いや〜やばいですね笑。
ダメなポイント
1. 関数の名前のタイポ
細かいかもだけど結構重要
- const filterdWeightArray
+ const filteredWeightArray
eが抜けている
2. 出力結果が、配列の中に配列が入っている
本来は↓のようにしないといけない。
[{
"id": 2,
"type": "husky",
"weight": 20
}]
3. dogsをfilterしたcallbackの引数がweightなのは、意味が通っていない
const filteredWeightArray = [dogs.filter((weight ❌ → dog 🙆 ) => { // このweightが良くない!
return (
weight.weight >= 8
)
})];
これを先輩に指摘された僕のコメント ↓
そう! 確かに、weight
自体はfilteredWeightArrayの中の対象の要素でしかないので、なんでも良いんですよ。
でも、weight.weigh
は明らかにダメです
少なくとも、filterで回す対象の配列がどういう役割なのか、ということは名前を見たときにわかるようにしましょう。
4. 解答例(コメントを頂き、より良い別解あり)
- 例題でも書いたように省略して書くなら
const filteredWeightsArray = dogs.filter(dog => dog.weight >= 8);
console.log(filteredWeightsArray ←❌ //これは実は微妙で、filterという命名の方がいい);
// [{
// "id": 2,
// "type": "husky",
// "weight": 20
// }]
- もし、配列ではなくてオブジェクトで出力する問題だったら
dogs.filter(dog => dog.weight >= 8)
.forEach(filtered => console.log(filtered));
// filterでweight8以上の対象を配列に入れて、それをforEachで回してlogに出している
// {
// "id": 2,
// "type": "husky",
// "weight": 20
// }
コメントを頂き、その方の考えとその解答を以下に書きます
引数に関して補足
@miyabisunさんのコメントを参考に、追記します。
filter
の中とかの限定された空間ならば、dogの頭文字を取ってd
みたいなのにする事が多い- そこでしか使わない超長い変数名(僕の今回のコードの
filteredWeightsArray
)を名付けるのは、読みづらい
という理由から、
console.log(dogs.filter(d => d.weight >= 8));
// or 改行で開く
console.log(
dogs.filter(d => d.weight >= 8)
);
という方がいいと思います。(めっちゃ分かりやすい)
問題2
dogsからidが3のDogオブジェクトを取得してconsoleに出力してください。
問題の再掲
type Dog = {
// ID
id: number;
// 犬の種類
type: string;
// 体重
weight: number;
// 真偽値
isCat?: boolean;
};
const dogs: Dog[] = [
{
id: 1,
type: "toyPoodle",
weight: 4,
},
{
id: 2,
type: "husky",
weight: 20,
},
{
id: 3,
type: "pug",
weight: 7.5,
},
];
昔の僕のダメな解答()
❷for (let ❶index = 0; index < dogs.length; index++) {
if (dogs[index].id === 3) {
console.log(dogs[index]);
}
};
出力結果 ⭕️(出力としては正解)
// {
// "id": 3,
// "type": "pug",
// "weight": 7.5
// }
ダメなポイント
1. indexはi
にしていい
2. forループは可読性が悪いから他のメソッド(map,forEach,findとか)で第二引数にindexを渡した方がreadable(読みやすい)
const findIdThreeObj = dogs.find((_, index) => dogs[index].id === 3
);
console.log(findIdThreeObj);
// {
// "id": 3,
// "type": "pug",
// "weight": 7.5
// }
ちなみに、第一引数の
_
は第一引数の値を使わないけど第二引数のindexを使いたい時に、使わんことを明示的に示すために_
を使っています。
左画像 ←:forループについて突っ込まれた時にした僕の質問
右画像 →:それに対する先輩の解答
(ただ、可読性は良くなるけど、パフォーマンスを考えないといけない場合はforの方がいいらしい。(大概はmap,forEachでOK!))
3. id=3があることが保証されていない時やidが一意でない時を考慮出来ていない
(これはどこまで、考慮するかにもよります)
console.log(dogs.find(dog => dog.id === 3));
よし、3で指摘されたことを生かして↑のように書いたらOKなのかというと微妙で、findメソッドは条件に最初に一致したものを返すメソッドなので、idが一意でない場合(実は2個ある時など)に、他にもそのidがあるのに、それを無視してしまうという怖さがあります。
(詳しくはfindメソッドの危険性についての考察などを参照してください)
なので、もしfindでidがない場合の処理は
console.log(dogs.find(dog => dog.id === 3) ?? "配列Dogには、id3は存在しません");
とか
null合体演算子を使ってundefined(findは一致しない時にはundefinedを返す)の時にエラーを返す的な処理を書けば良いのではないかと思います。
4. 解答例
console.log(dogs.find(dog => dog.id === 3) ?? "配列Dogには、id3は存在しません");
//または
const findIdThreeObj = dogs.find((_, index) => dogs[index].id === 3
);
console.log(findIdThreeObj);
問題3
dogs内の全Dogオブジェクトに isCat: false を追加した配列をconsoleに出力してください。
問題の再掲
type Dog = {
// ID
id: number;
// 犬の種類
type: string;
// 体重
weight: number;
// 真偽値
isCat?: boolean;
};
const dogs: Dog[] = [
{
id: 1,
type: "toyPoodle",
weight: 4,
},
{
id: 2,
type: "husky",
weight: 20,
},
{
id: 3,
type: "pug",
weight: 7.5,
},
];
ダメな解答
type Dog = {
// ID
id: number;
// 犬の種類
type: string;
// 体重
weight: number;
+ // 真偽値
+ isCat: boolean;
};
for (let index = 0; index < dogs.length; index++) {
dogs[index].isCat = false;
}
console.log(dogs);
❶元の配列・型を破壊しないで、新しく配列を生成した方がいい
-
型に関しては、Dogに
isCat=boolean
を入れるよりも明示的に型を拡張してあげる方がわかりやすいし、元の型(Dog)を使い回す場合は下のようにした方がいい。
type Dog = {
id: number;
type: string;
weight: number;
};
type = JudgeDog {
isCat? = boolean;
};
type NewDog = Dog & JudgeDog;
もし、元の型(Dog)を使わず、isCat=boolean
を作るだけでいいなら、
type = JudgeDog = Dog & {
isCat?: boolean;
};
-
配列に関しては、
consoleに出力する流れで、元の配列に新しいプロパティisCat=boolean
を追加しています。
なので、もし破壊的なメソッドを使った場合に元の配列を参照できなくなったりするので、新しい配列を生成してそれを出力する方がわかりやすい!
例はこんな感じ↓
const newArr = dogs.map((dog) => {
return {...dog, isCat: false};
})
console.log(newArr);
// 出力結果⭕️
// [{
// "id": 1,
// "type": "toyPoodle",
// "weight": 4,
// "isCat": false
// }, {
// "id": 2,
// "type": "husky",
// "weight": 20,
// "isCat": false
// }, {
// "id": 3,
// "type": "pug",
// "weight": 7.5,
// "isCat": false
// }]
問題4
dogs 内の全ての weight を合計した値を返却する関数を作成してください。
問題の再掲
type Dog = {
// ID
id: number;
// 犬の種類
type: string;
// 体重
weight: number;
// 真偽値
isCat?: boolean;
};
const dogs: Dog[] = [
{
id: 1,
type: "toyPoodle",
weight: 4,
},
{
id: 2,
type: "husky",
weight: 20,
},
{
id: 3,
type: "pug",
weight: 7.5,
},
];
僕のダメな解答
// ❶そもそもダメなところ
let weightSum = 0;
for (let index = 0; index < dogs.length; index++) {
weightSum += dogs[index].weight;
}
console.log(weightSum);
//31.5
ダメなポイント
❶関数を作れという問題意図に沿っていない
今回はconsoleに出力する問題ではありません!!
ただ、合計値をlogに出すという問題なら、上のコードのようにfor文を回して足すのでもいいとは思います。
でも、配列の累積値を求める時にはreduceメソッド(JS-Primer)を使って関数化するという手も良いでしょう!
const SumDogsWeight = (dogs: Dog[]):number => {
return dogs.reduce((acc, current) => acc + current.weight, 0)
};
console.log(SumDogsWeight(dogs));
上のように関数化しておくと、引数を変えるだけで配列の値の合計値を出すSumDogsWeightを使いまわせることができるようになります。
まとめ
お疲れ様でした!!
ただ「for文を多用して、出力結果があっていれば万事OK」というのではなくて、
- コードの読みやすさ
- 使い回しのしやすさ(関数化)
- 命名規則
という部分にも気を配りながらコードを書けるとより良いよ!!」
また、「この問題を頑張って解いたことが、JavaScriptを理解する一歩目になるよ〜」と昔の自分に言ってあげたいです。
最後まで読んでいただきありがとうございました!
もし、コーヒーを買っていただけると、すごく励みになります
https://www.buymeacoffee.com/shinyamamoto
後書き
この記事を書くにあたって、僕のクソコードに対して丁寧にレビューをしてくださった先輩方、当時の自分には目から鱗な情報ばかりで成長するきっかけになりました。ありがとうございました。
次回は、【🎉【1000草🌱】【メンバーサイトをリリース】した記念🎉 〜これまでやってきたこと・今動いてること・将来やることを述べる〜】で会いましょう!