JavaScriptからletを絶滅させ、constのみにするためのレシピ集


はじめに

本記事では、constこそが唯一神であることを証明したあと、letを使いがちな場面でいかにしてconstを使うかをまとめていきます。なお、ES2018までの基本構文(reduce, async/await, 配列とオブジェクトのスプレッド構文)を使用します。「いや、reduceとかスプレッド構文とか難しいからlet使うわ」という方のために、便利メソッド詰め合わせであるLodashを使った例もご紹介します。

追記:Lodashの使用について

「Lodashのコードにlet使われてるやん」というご指摘を多く頂いたので追記いたします。
誤解を招くタイトルにしてしまい申し訳ありません。「JavaScript で開発しているサービスのソースコード からletを絶滅させ、constのみにするためのレシピ集」という意図でした。

追記ここまで

注意事項

この記事は半分ネタで半分本気です。実際の開発でどこまでconst教を導入するかは、他のメンバーと慎重に相談してください。
その際には、以下の考察記事も参考にしてください。
JavaScriptのletは本当に悪なのか

対象者

初心者(文法覚えたて。letとconstの違いはわかる)〜中級者(Promiseを理解し、async/awaitやthen/catch, try-catchが使える)くらいを想定しています。
とくに、「とりあえず変数はletで宣言しとこう」という考えを持ってしまっている方を対象にしています。

letに対する誤解

「varは使っちゃだめ! letやconstを使いましょう!」という言い回しをよく聞きます。
varは関数全体にスコープが漏れ出してしまうのが理由です。

varはダメという主張自体は間違いないと思うのですが、「letやconstを使いましょう」というと、letとconstが同等の地位であるかのような印象を初学者の方に与えてしまいます。
違います。

constこそが唯一神であり、それと比べてしまえばletとvarの差など微々たるもの


なのです。

constが唯一神である理由

4歳娘「パパ、constしか使わないで?」
こちらの記事が非常にわかりやすくまとまっております。
この記事でご説明いただいている通り、letは「いつ再代入されるかわからない」という恐怖を読み手に与えてしまいます。constなら宣言された行だけを見ればどんな値が入っているかがわかりますが、letはコード全体を追う必要があり、読み手への負担が大きいです。
これに加えて、うっかりグローバル変数を爆誕させてしまう危険性があります。

実は危ないコード
const main = () => {
  let c = 1
  // 何らかの処理
  c = 2
  console.log(c) // 2
}

main()
console.log(c) // エラー

さて、let c = 1の行が消えたらどうなるでしょうか

グローバル変数爆誕の瞬間
const main = () => {
  // 何らかの処理
  c = 2 // var, let, const何もついてないのでグローバル変数が爆誕
  console.log(c) // 2
}

main()
console.log(c) // 2

まあ、use strictつけたり、eslintのno-implicit-globalsを設定すればグローバル変数の爆誕は防げますが。
いずれにせよ$\text{const} > \text{let}$であることが説明できました。冒頭で$\text{let} > \text{var}$は証明済みのため、まとめると$\text{const} > \text{let} > \text{var}$です。これでconstが唯一神であることが証明できました$\text{Q.E.D.}$

letをconstに変えるレシピ集

letを使いがちないくつかの場面に対して、constに変える方法を伝授していきます。

環境

  • ES2018(オブジェクトのスプレッド構文が必要なため)
  • Lodashをつかう場合は バージョンが4.17.19で
import _ from 'lodash'

している前提です。

補足:Lodashのimport方法について
バンドルサイズを削減するためには、import range from 'lodash/range'のように使う関数を個別でimportしたほうがよいです(参考)。

 また、awaitをトップレベルで使っている際は以下のようにasync関数の一部を切り出しているものとしてお考えください(そもそもNode 14.3.0からtop level await使えるから許してくれ)

async function main() {
  /** サンプルコード****************
   *                           *
   *****************************/
}
main()

これらを念頭に置いて以下のサンプルコードをお読みください。

初級

10回繰り返したいfor文

これについてはfor文の中だけのスコープになるので許しても良い気もしますが、例外を認めるとletが根絶できません。

before
for (let i = 0; i < 10; i++) {
  console.log(i)
}
after
_.range(10).forEach(i => {
  console.log(i)
})

Pythonのrangeみたいな関数をLodashは提供してくれています。今回の例では[0, 1, 2, ..., 9]という配列を生成し、それをforEachでループしています。

スプレッド構文が分かる人向け
[...Array(10)].forEach((_, i) => {
  console.log(i)
})

Array(10)は長さ10の空の配列を生成します。[...Array(10)]このようにスプレッド構文で展開すると、undefinedが10個並んだ配列が生成できます。forEachに渡したコールバック関数は2つめの引数がindexであることに気をつけましょう。(kfjt様、サンプルコードのご紹介ありがとうございます)

数値配列の合計値を算出

before
const arr = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
let sum = 0
for (let i = 0; i < arr.length; i++) {
  sum += arr[i]
}
console.log(sum)
after
const sum = _.sum(arr)
console.log(sum)

素のJSでもreduceを使えば合計は求められますが、こちらのほうがわかりやすいはずです。

reduceが分かる人向け
const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0)

reduceの例でよく紹介されるやつですね。空の配列に対して実行した際に、reduceの第2引数が指定されていないとエラーがでます。0を指定しておきましょう。

オブジェクトの配列の合計値を算出

before
const users = [{ name: 'person1', age: 10 }, { name: 'person2', age: 20 }, { name: 'person3', age: 30 }]
let sumOfAge = 0
for (const user of users) {
  sumOfAge += user.age
}
console.log(sumOfAge)
after
const sumOfAge = _.sumBy(users, 'age')
console.log(sumOfAge)

LodashのsumByの使い所です。

reduceが分かる人向け
const sumOfAge = users.reduce((accumulator, currentUser) => accumulator + currentUser.age, 0)

第2引数に0を指定しているので、空の配列のときにもエラーがでません。また、第2引数を指定しないと、acuumulatorに最初オブジェクトが入ってしまうので気をつけましょう。

if文

before
let tax
if (isTakeout) {
  tax = 0.08
} else {
  tax = 0.1
}
after
const tax = isTakeout ? 0.08 : 0.1

三項演算子を使ってはいけないと誰かに言われたら、先ほどのconstが唯一神である証明でも見せて黙らせましょう。三項演算子はネストさせない限りは使っても問題ありません。

じゃあswitch文どうするのよ

before
let message
switch (response.status) {
  case 200:
    message = 'OK'
    break
  case 204:
    message = 'No Content'
    break
  // ...省略
}
console.log(message)
after
const getMessageByStatus = (status) => {
  switch (status) {
  case 200:
    return 'OK'
  case 204:
    return 'No Content'
  // ...省略
  }
}

const message = getMessageByStatus(response.status)

関数に切り分けましょう。breakする必要もなくなって行数が減りました。
(ちなみにこの例だとオブジェクトで宣言しといたほうが良い)

オブジェクトで宣言しとく
const statusToMessage = {
  200: 'OK',
  204: 'No content'
}
const message = statusToMessage[response.status]

中~上級

try-catchとの兼ね合い

before
let response
try {
  response = await requestWeatherForecast() // 天気予報APIを叩く
} catch (err) {
  console.error(err)
  response = '曇り' // APIから取得できなかった場合は適当に曇りとか言ってごまかす
}
console.log(response)
after
const response = await requestWeatherForecast().catch(err => {
  console.log(err)
  return '曇り'
})

Promiseのcatchメソッド内でreturnした値はawaitを通せば外界の変数に代入することができます

例外catchしたら早期returnしたいんだが

before
let response
try {
  response = await requestWeatherForecast() // 天気予報APIを叩く
} catch (err) {
  // なにかエラーが起きたときの処理
  console.error(err)
  return
}
console.log(response)
after
const response = await requestWeatherForecast().catch(err => {
  // なにかエラーが起きたときの処理
  console.error(err)
})
if (response === undefined) return
console.log(response)

先ほどのちょっとした応用です。例外が発生したときはcatchに渡したコールバック関数が実行されます。そのコールバック関数の中で何もreturnしないと、responseにはundefinedが入ります。(なおrequestWeatherForecastが正常の範囲内でPromise<undefined>を返す場合は「エラーが起きていないのにreturnしてしまう」のですが、そもそもresponseがundefinedなら後続の処理ができないのでreturnしてしまっても良いでしょう)。

リトライ処理

before
// 天気予報APIを叩く。エラーが出たら10回までリトライする
const MAX_RETRY_COUNT = 10
let retryCount = 0
let response
while(retryCount <= MAX_RETRY_COUNT) {
  try {
    response = await requestWeatherForecast() // 天気予報APIを叩く
    break
  } catch (err) {
    console.error(err)
    retryCount++
  }
}
console.log(response)
after
// 与えられた関数をmaxRetryCount回までリトライする関数。
const retry = (maxRetryCount, fn, retryCount = 0) => {
  if (retryCount >= maxRetryCount) return undefined

  return fn().catch(() => retry(maxRetryCount, fn, retryCount + 1)) // retryCountを1増やして再帰呼び出し
}

const response = await retry(MAX_RETRY_COUNT, requestWeatherForecast)

retryのようなラップ関数をつくりましょう。

番外編(不変じゃないconst)

有名な話ではありますが、constは再代入できないだけで、constで宣言した配列に要素を追加したり、constで宣言したオブジェクトにプロパティを追加することはできてしまいます。

constでも変更を加えられる例
const arr = []
arr.push(1) // arr: [1]
const obj = {}
obj.a = 1 // obj: { a: 1 }

これらの行為はconstという唯一神をletと同じ地位まで貶める愚行です。以下で、配列やオブジェクトを変更しがちな例を紹介し、その代替案を紹介します。

配列から条件に合うものだけ抜き出す

before
// 偶数だけを抜き出す
const arr = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
const result = []
for (const n of arr) {
  if (n % 2 === 0) result.push(n) // 愚行
}
console.log(result)
after
const result = arr.filter(n => n % 2 === 0)

filterを使いましょう。ちなみにfilterは宣言しておいた関数を渡すと可読性が高まります。

可読性が高い
const isEven = num => num % 2 === 0 // 関数を用意
const result = arr.filter(isEven)

変数がundefinedじゃないときだけオブジェクトに追加

before
// トークンがundefinedじゃなかったら、Authorizationヘッダーを追加
const header = {
  'Content-Type': 'application/json'
}
if (token !== undefined) header.Authorization = `Bearer ${token}` // 愚行
after
const header = {
  'Content-Type': 'application/json',
  ...(token === undefined ? {} : { Authorization: `Bearer ${token}` })
}

三項演算子と...のスプレッド構文の組み合わせです。空のオブジェクトをスプレッド構文で展開すると消えるというテクニックは便利です。

オブジェクトの値部分に処理を加える

before
// 値部分をNumberに変換し、値が偶数のところだけ抜き出す。
const obj = { a: '1', b: '2', c: '3', d: '4', /* ... */ }
cosnt result = {}
for (const [key, value] of Object.entries(obj)) {
  const number = Number(value)
  if (number % 2 === 0) {
    result[key] = number // 愚行
  }
}
console.log(result) // { b: 2, d: 4, ... }
after
const isEven = num => num % 2 === 0 // 偶数かを判定する関数を用意
const result = _.pickBy(_.mapValues(obj, Number), isEven)

配列にmapメソッドがあると思いますが、LodashのmapValuesはそれのオブジェクト版だと考えると話が早いです。pickByは先ほどでてきた配列のfilterのオブジェクト版です。
ちなみに、Lodashは以下のように使うとメソッドチェーンがはかどります(ただし、Lodashの関数すべてをインポートすることになり、バンドルサイズが増大するので、フロント開発ではやらないほうが良いです)。

メソッドチェーン
_(obj).mapValues(Number).pickBy(isEven) // { b: 2, d: 4, ... }

reduceとスプレッド構文が分かる人向け
const result = Object.entries(obj).reduce((accumulator, [currentKey, currentValue]) => {
  const number = Number(currentValue)
  if (isEven(number)) return { ...accumulator, [currentKey]: number }

  return accumulator
}, {})

結論

普段コードを書いているときにletを使いたくなったら、ふとこの記事を思い出してください。
そのletはconstに替えられないのかと。
それでも、あなたが悩んだ末にconstではなくletを使うことを決意したら、私たちconst教徒は止めません。
なぜなら、 あなたが見つけ出したそのletは唯一神constよりも尊いものに違いないのですから…。

あとがき

他にもletを使いたくなる場面があったらご指摘いただけますと幸いです。
また、記事に不備がございましたらご指摘いただけますと幸いです。