はじめに
本記事では、constこそが唯一神であることを証明したあと、letを使いがちな場面でいかにしてconstを使うかをまとめていきます。なお、ES2018までの基本構文(reduce, async/await, 配列とオブジェクトのスプレッド構文)を使用します。「いや、reduce
とかスプレッド構文とか難しいからlet
使うわ」という方のために、便利メソッド詰め合わせであるLodashを使った例もご紹介します。もちろん、Lodashは機能に対してサイズが大きいライブラリであるため、フロントエンド開発でバンドルサイズを軽減したいという方などはLodashの例は無視し、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は関数全体にスコープが漏れ出してしまうのが理由です。
【JavaScriptのvarが嫌われる理由】
— きよしろー@Web系エンジニアになりたい人 (@kiyoshiro944) May 24, 2020
宣言した関数内ならどこでも使えてしまうから
つまり、if文の中で宣言したのに外でも使えてしまう
これは例えると、鍵アカでつぶやいたツイートが日本中に公開されるくらいやばいpic.twitter.com/lUQ3jMwiG9
varはダメという主張自体は間違いないと思うのですが、「letやconstを使いましょう」というと、letとconstが同等の地位であるかのような印象を初学者の方に与えてしまいます。
違います。
constこそが唯一神であり、それと比べてしまえばletとvarの差など微々たるもの
なのです。 # constが唯一神である理由 [4歳娘「パパ、constしか使わないで?」](https://qiita.com/Yametaro/items/17f5a0434afa9b88c3b1) こちらの記事が非常にわかりやすくまとまっております。 この記事でご説明いただいている通り、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'
が省略されているとお考えください。(本当はバンドルサイズを削減するためには、import range from 'lodash/range'
のように使う関数を個別でimportしたほうがよいです(参考)。
また、awaitをトップレベルで使っている際は以下のようにasync関数の一部を切り出しているものとしてお考えください(そもそもNode 14.3.0からtop level await使えるから許してくれ)
async function main() {
/** サンプルコード****************
* *
*****************************/
}
main()
これらを念頭に置いて以下のサンプルコードをお読みください。
初級
数値配列の合計値を算出
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)
const sum = _.sum(arr)
console.log(sum)
reduceが分かる人向け
const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0)
reduceの例でよく紹介されるやつですね。空の配列に対して実行した際に、reduceの第2引数が指定されていないとエラーがでます。0を指定しておきましょう。
オブジェクトの配列の合計値を算出
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)
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文
let tax
if (isTakeout) {
tax = 0.08
} else {
tax = 0.1
}
const tax = isTakeout ? 0.08 : 0.1
三項演算子を使ってはいけないと誰かに言われたら、先ほどのconstが唯一神である証明でも見せて黙らせましょう。三項演算子はネストさせない限りは使っても問題ありません。
じゃあswitch文どうするのよ
let message
switch (response.status) {
case 200:
message = 'OK'
break
case 204:
message = 'No Content'
break
// ...省略
}
console.log(message)
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との兼ね合い
let response
try {
response = await requestWeatherForecast() // 天気予報APIを叩く
} catch (err) {
console.error(err)
response = '曇り' // APIから取得できなかった場合は適当に曇りとか言ってごまかす
}
console.log(response)
const response = await requestWeatherForecast().catch(err => {
console.log(err)
return '曇り'
})
Promiseのcatchメソッド内でreturnした値はawaitを通せば外界の変数に代入することができます
例外catchしたら早期returnしたいんだが
let response
try {
response = await requestWeatherForecast() // 天気予報APIを叩く
} catch (err) {
// なにかエラーが起きたときの処理
console.error(err)
return
}
console.log(response)
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してしまっても良いでしょう)。
リトライ処理
// 天気予報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)
// 与えられた関数を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 arr = []
arr.push(1) // arr: [1]
const obj = {}
obj.a = 1 // obj: { a: 1 }
これらの行為はconstという唯一神をletと同じ地位まで貶める愚行です。以下で、配列やオブジェクトを変更しがちな例を紹介し、その代替案を紹介します。
配列から条件に合うものだけ抜き出す
// 偶数だけを抜き出す
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)
const result = arr.filter(n => n % 2 === 0)
filterを使いましょう。ちなみにfilterは宣言しておいた関数を渡すと可読性が高まります。
const isEven = num => num % 2 === 0 // 関数を用意
const result = arr.filter(isEven)
変数がundefinedじゃないときだけオブジェクトに追加
// トークンがundefinedじゃなかったら、Authorizationヘッダーを追加
const header = {
'Content-Type': 'application/json'
}
if (token !== undefined) header.Authorization = `Bearer ${token}` // 愚行
const header = {
'Content-Type': 'application/json',
...(token === undefined ? {} : { Authorization: `Bearer ${token}` })
}
三項演算子と...
のスプレッド構文の組み合わせです。空のオブジェクトをスプレッド構文で展開すると消えるというテクニックは便利です。
オブジェクトの値部分に処理を加える
// 値部分を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, ... }
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を使いたくなる場面があったらご指摘いただけますと幸いです。
また、記事に不備がございましたらご指摘いただけますと幸いです。