Open12

"にわかJavascripter"による学習メモ

Kohei ShimizuKohei Shimizu

ループ処理のときは、変数宣言をletにすること

paizaでの例題がvarで実装されていて、にわかJavascripterだった自分がconstに全部書き換えていたところ下記でつまづいてしまった。

問題として、Hello,world!を10回表示するプログラムが出題されていたので、以下のプログラムを最初は書いていた。

JavaScript(間違えたコード)
process.stdin.resume();
process.stdin.setEncoding('utf8');
const num = 0;
while (num < 10) {
    console.log("Hello,world!");
    num += 1;
}

↑これだと、constが原因でnumに値が代入できない!

下記の記事からの引用ですが、以下の表がわかりやすかった。
https://techplay.jp/column/1619

let var const
再宣言 不可能 可能 不可能
再代入 可能 可能 不可能
スコープ ブロックスコープ 関数スコープ ブロックスコープ
繰り返し構文 可能 可能 不可能

なので、変数宣言をletにしたところ成功した!

JavaScript(正しいコード)
process.stdin.resume();
process.stdin.setEncoding('utf8');
let num = 0;
while (num < 10) {
    console.log("Hello,world!");
    num += 1;
}

👉️ つまり、再代入をするなら、letを使うべき!

Kohei ShimizuKohei Shimizu

【JavaScriptでアルゴリズムのコレ便利①】 console.logではなくconsole.table

例えば、下記のような入力があったとします。

標準入力
1
2 3
test

これを :thinking_face: 「ちゃんと取れているかな〜」と確認したいときに、console.table(標準入力);にすると視覚的にわかりやすい!
実際に書いてみると、、、、

書いたコード
function Main(input) {
  input = input.split("\n");
  console.table(input);
}
Main(require('fs').readFileSync('/dev/stdin', 'utf8'));

そうすると、こんな出力になる!

出力
┌─────────┬────────┐
│ (index) │ Values │
├─────────┼────────┤
│    0'1'   │
│    1'2 3'  │
│    2'test' │
│    3''   │
└─────────┴────────┘
Kohei ShimizuKohei Shimizu

【JavaScriptでアルゴリズムのコレ便利②】 空白を入れて出力は区切るだけ

例えば、こんなコードがあったとします。これを、6 testという感じに「半角スペースを入れて出力したい!」って時に

書いたコード
const num = 6;
const str = "test";

console.log();のカッコ内でコンマで区切るだけで良い。

書いたコード(出力方法を追記)
const num = 6;
const str = "test";
console.log(num, str);
出力
6 test // ←半角スペースがちゃんと入っている!

何が良いかって、Rubyだと明示的に空白を入れないといけない! 😠

Rubyなら
num = 6
str = "test"
puts num.to_s + " " + str
Kohei ShimizuKohei Shimizu

【JavaScriptでアルゴリズムのコレ便利③】 Stringで出た数値をIntにしたい

例えば、こんなコードがあったとします。

const num = "6";

🤔「Stringではなく、数値(Int)で出したいんだよなあ・・・。」ってときは、冒頭に+をつけるだけ!

書いたコード
const num = 6;
console.log(+num);
出力
6 // ←ちゃんと数値!

これを 「単項プラス演算子」 というらしい。数値以外にも反映できるのが素晴らしくスマート!
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Unary_plus

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Unary_negation

Kohei ShimizuKohei Shimizu

配列の書き出し方

これがもともと定義されていたとする。
const userNames = ['Alice', 'Bob', 'Charlie'];
for文
for (let i = 0; i < userNames.length; i++){
  console.log(userNames[i]);
}
forEach文(一番スマート)
userNames.forEach(function(name) {
  console.log(name);
});
for...of文(少しスマート)
for (const name of userNames) {
  console.log(name);
}
Kohei ShimizuKohei Shimizu

オブジェクトの書き出し方

これがもともと定義されていたとする
const user = {
  id: 101,
  name: 'Bob',
  email: 'bob@example.com',
  age: 30
};
出力例(こういう出力にしたい)
ユーザー名:Bob, メールアドレス:bob@example.com
実直に書くと、こう。
console.log("ユーザー名:"+user.name+", メールアドレス:"+user.email);
「+」で連結させずにバッククォートで書ける。(直感的)
console.log(`ユーザー名:${user.name}, メールアドレス:${user.email}`);
変数として定義できる書き方もある
const { name, email } = user;
console.log(`ユーザー名: ${name}, メールアドレス: ${email}`);
Kohei ShimizuKohei Shimizu

オブジェクトの変換

これをバックエンドから受け取ったとする
const products = [
  { id: 'p001', name: '高性能コーヒーメーカー', price: 9800, category: 'kitchen' },
  { id: 'p002', name: '静音設計ケトル', price: 6500, category: 'kitchen' },
  { id: 'p003', name: 'レトロ調トースター', price: 8200, category: 'kitchen' },
  { id: 'p004', name: 'コードレス掃除機', price: 15000, category: 'home' }
];

期待するのは、あとで読みやすいように、下記の出力をdisplayProductsという配列に入れて出力してね。というもの。

期待される出力
[
  '高性能コーヒーメーカー: 9800円',
  '静音設計ケトル: 6500円',
  'レトロ調トースター: 8200円',
  'コードレス掃除機: 15000円'
]

自分で書いたコードはこう。

自分で書いたコード
const displayProducts = [];

products.forEach(product => {
  displayProducts.push(`${product.name}: ${product.price}`)
});
console.log(displayProducts);

でも、このコードだと、「一度、空配列を作る」→「別のところでpush」なので行が空いてしまって読みづらい(この間にコードを追記される懸念もある)。

なので、書き換えるとこう。

mapを使って書き換えたコード
const displayProducts = products.map(product => {
  return `${product.name}: ${product.price}`;
});
console.log(displayProducts);

mapを使うことで、displayProductsの定義がわかりやすく読める。

Kohei ShimizuKohei Shimizu

async/awaitとtry/catchの組み合わせの書き方

与えられるデータ
function fetchProduct(productId) {
  console.log(`サーバーに商品ID「${productId}」のデータをリクエストしています...`);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 実行するたびに、成功するか失敗するかがランダムに変わります
      if (Math.random() > 0.5) {
        const productData = {
          id: productId,
          name: '新世代スマートウォッチ',
          price: 35800
        };
        console.log("...データ取得に成功しました。");
        resolve(productData); // 成功した場合は、resolveでデータを渡す
      } else {
        console.log("...データ取得に失敗しました。");
        reject(new Error('商品在庫が見つかりませんでした。')); // 失敗した場合は、rejectでエラーを渡す
      }
    }, 100);
  });
}
書いたコード
async function main() {
  try {
    const productData = await fetchProduct('p-001');
    console.log(`商品名:${productData.name}, 価格:${productData.price}`);
  } catch (e) {
    console.log(`エラーが発生しました:${e.message}`);
  }

}
main();
成功したらこんな出力
サーバーに商品ID「p-001」のデータをリクエストしています...
...データ取得に成功しました。
商品名: 新世代スマートウォッチ, 価格: 35800
失敗したらこんな出力
サーバーに商品ID「p-001」のデータをリクエストしています...
...データ取得に失敗しました。
エラーが発生しました: 商品在庫が見つかりませんでした。
Kohei ShimizuKohei Shimizu

Promise.allとasync/awaitの使い方

与えられる2つのデータ
// 1. ユーザーのプロフィールを取得する(0.5〜1.5秒かかる)
function fetchUserProfile(userId) {
  console.log(`[API] ユーザー(${userId})のプロフィールを取得開始...`);
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`[API] プロフィール取得完了。`);
      resolve({
        id: userId,
        name: 'Sato',
        email: 'sato@example.com'
      });
    }, Math.random() * 100 + 50);
  });
}

// 2. ユーザーの投稿一覧を取得する(0.5〜1.5秒かかる)
function fetchUserPosts(userId) {
  console.log(`[API] ユーザー(${userId})の投稿一覧を取得開始...`);
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`[API] 投稿一覧取得完了。`);
      resolve([
        { postId: 'p001', content: '今日のランチは最高でした!' },
        { postId: 'p002', content: '新しいPCを買いました。' }
      ]);
    }, Math.random() * 100 + 50);
  });
}
期待する出力(成功した場合)
{
  user: { id: 'user-123', name: 'Sato', email: 'sato@example.com' },
  posts: [
    { postId: 'p001', content: '今日のランチは最高でした!' },
    { postId: 'p002', content: '新しいPCを買いました。' }
  ]
}
書いたコード
async function main() {
  try {
    const [user, posts] = await Promise.all([fetchUserProfile('user-123'), fetchUserPosts('user-123')]);
    const dashboardData = {
      user: user,
      posts: posts
    };
    console.log(dashboardData);
  } catch {
    console.log("APIの取得にエラーが発生しました");
  }
}
main();
Kohei ShimizuKohei Shimizu

for文の中でawaitを使う・filterでincludesで絞り込み

入力
function fetchLatestPostIds() {
  console.log("API: 最新記事のIDリストを取得しています...");
  return new Promise(resolve => {
    setTimeout(() => {
      // 'id-003'は存在しないIDだと仮定する
      resolve(['id-001', 'id-002', 'id-003', 'id-004', 'id-005']);
    }, 50);
  });
}

function fetchPostDetails(postId) {
  console.log(`API: 記事ID「${postId}」の詳細データを取得しています...`);
  return new Promise((resolve, reject) => {
    const postDatabase = {
      'id-001': { title: 'Reactの新しい状態管理法', tags: ['React', 'JavaScript'] },
      'id-002': { title: 'CSS in JSの未来', tags: ['CSS'] },
      'id-004': { title: 'TypeScriptと非同期処理', tags: ['TypeScript', 'JavaScript'] },
      'id-005': { title: 'Promise.allの便利な使い方', tags: ['JavaScript', '非同期処理'] },
    };
    
    setTimeout(() => {
      if (postDatabase[postId]) {
        resolve(postDatabase[postId]);
      } else {
        reject(new Error(`エラー: 記事ID「${postId}」は存在しません。`));
      }
    }, 70);
  });
}
期待される出力
エラーが発生しました: エラー: 記事ID「id-003」は存在しません。

[
  'Reactの新しい状態管理法',
  'TypeScriptと非同期処理',
  'Promise.allの便利な使い方'
]
書いたコード
async function main() {
  const id_lists = await fetchLatestPostIds();
  const successfulPosts = [];
  
  for (const id of id_lists) {
    try {
      const post = await fetchPostDetails(id);
      successfulPosts.push(post);
    } catch (e) {
      console.log(e.message);
    }
  }
  
  const jsPosts = successfulPosts.filter(post => post.tags.includes('JavaScript'));
  // console.log(jsPosts);
  const jsPostsTitle = jsPosts.map(post => post.title);
  console.log(jsPostsTitle);
}

main();
Kohei ShimizuKohei Shimizu

入力情報が増えた時のためにreduceで対応する

入力
const tasks = [
  { id: 1, title: 'ユーザー認証機能の実装', status: 'In Progress' },
  { id: 2, title: 'ヘッダーコンポーネントの作成', status: 'To Do' },
  { id: 3, title: 'データベースのバックアップ', status: 'Done' },
  { id: 4, title: '決済処理のAPI連携', status: 'In Progress' },
  { id: 5, title: '単体テストの追加', status: 'To Do' },
  { id: 6, title: '利用規約ページの作成', status: 'Done' },
];
期待する出力
{
  'To Do': [
    { id: 2, title: 'ヘッダーコンポーネントの作成', status: 'To Do' },
    { id: 5, title: '単体テストの追加', status: 'To Do' }
  ],
  'In Progress': [
    { id: 1, title: 'ユーザー認証機能の実装', status: 'In Progress' },
    { id: 4, title: '決済処理のAPI連携', status: 'In Progress' }
  ],
  'Done': [
    { id: 3, title: 'データベースのバックアップ', status: 'Done' },
    { id: 6, title: '利用規約ページの作成', status: 'Done' }
  ]
}

単なるループ処理で書けるじゃん!・・・と思ったので、下記のように書いた。

書いたコード
const toDoTask = []; 
const inProgressTask = []; 
const doneTask = []; 

tasks.forEach(task =>{
  if (task.status === "In Progress") {
    inProgressTask.push(task);
  } else if (task.status === "To Do") {
    toDoTask.push(task);
  } else {
    doneTask.push(task);
  }
});

const groupedTask = {
  'To Do': toDoTask,
  'In Progress': inProgressTask,
  'Done': doneTask
}
console.log(groupedTask);

↑でも、こうすると、statusに新しい要素(例:Reviewing)が出たときに対応できない。
なので、変更していく。

変更したコード(reduceを使う)
const groupedTask = tasks.reduce((grouped, task) => {
  const status = task.status;
  // console.log(status);
  
  // 'In Progress'とかの箱がなければ作る
  if (!grouped[status]) {
    grouped[status] = [];
  }
  
  // 作られている箱に追加する
  grouped[status].push(task);
  
  // 累積値を更新
  return grouped
}, {});

console.log(groupedTask);

↑こうすると、箱はもともとのtaskに準じる形になるので、対応できる!

Kohei ShimizuKohei Shimizu

クロージャー

単にカウントアップするだけのプログラムで、下記のようなコード書くとcountが他の関数にやられてしまい、プログラムが狂ってしまう。

間違っている例
let count = 0;
function createCounter() {
  return ++count;
}

console.log(createCounter()); // 1
console.log(createCounter()); // 2
count = 100; // ←ここで書き換えられてしまう。。。
console.log(createCounter()); // 101

そうではなく、「クロージャー」という関数外から干渉できなくなる仕組みを使うと良い。

クロージャーを使った例
// 「クロージャー」は、子関数を親関数で囲むことによって、変数の値を保持できる仕組み
// 親関数
function createCounter(initialValue = 0) {
  let count = initialValue;
  // 子関数
  return function() {
    count ++;
    return count;
  };
}


// 【動作確認】

// 1. 最初のカウンター(A)を作成(初期値なしなので0からスタート)
const counterA = createCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2

// 2. 別のカウンター(B)を初期値10で作成
const counterB = createCounter(10);
console.log(counterB()); // 11
console.log(counterB()); // 12

// 3. counterAを再度呼び出しても、Bの影響を受けずに自分のカウントを覚えていることを確認
console.log(counterA()); // 3 (←保持されている!)