プラグインをきれいに書くために

fspace

ユーザー
プログラミングというのは、その記法が「言語」と呼ばれているように、文章を記述する行為です。正しい内容が記述されているかどうかのみならず、また時にはそれ以上に、わかりやすくきれいに記述されていることが重視されます。

ツクールMZでは公式でプラグイン講座が公開され、以前よりも初心者がプラグイン開発を始めやすくなりました。しかし、こういったところで説明されるのはプラグインの動く仕組みだけで、『どう書くべきか』という話はしてくれません。

そこで、このスレではプログラミングの作法や整理術、パラダイムなど、きれいなコードを書くための技術について知識を共有していこうと思います。

-- 前置き 終わり

基本的に自分が思いつくままに雑多な内容を書くスレになると予想してますが、もちろん寄稿は歓迎です。

また、特にまとめとして整理しているつもりもないので、流れとか考えずに質問とか議論とかしてもらって大丈夫です。
 

fspace

ユーザー
品質とリファクタリング

とりあえず最初なので、そもそも「いいプログラム」って何よ、という話から。

直接役に立つ知識じゃないのであんまり面白くはないかもしれませんが、本題に入る前の導入くらいに思ってください。

なるべくわかりやすく書いたつもりですが、題材自体が小難しいのである程度はご容赦を。


ソフトウェア品質

プログラムは誰が作っても同じものになるわけではなく、そこには良し悪し、つまり『品質』があります。

プログラムの品質と言われてもピンとこないかもしれませんが、例えば、わかりやすいところでは機能(適合)性があります。これは大雑把に言うと「やりたかったことがちゃんとできるかどうか」です。それ以外にも「使い勝手がいいかどうか(使用性・ユーザビリティ)」、「安全に使えるかどうか(安全性・セキュリティ)」、「負荷が軽いかどうか(効率性)」あたりはわかりやすいと思います。

一方で、わかりづらいけれど重要な品質として『保守性』があります。これはプログラムを作った後、「そのメンテナンスがどれだけ簡単か」を示します。プログラムでは一度作ったらそれで終わり、ということはまずありません。バグが報告されれば修正が必要になりますし、新しい機能の要求があれば変更が必要になります。保守性のないプログラムを作ってしまうと、こういうときに「仕様です」「できません」と言うしかなくなってしまいます(あるいは泣く泣く最初から作り直します)。

保守性は機能性や使用性と違って外側に現れないので、よく利用者からは軽視されてしまうのですが、プログラマにとっては最もクリティカルに影響を受ける品質なのでとても重視されています。そのため、コードにバグがあっても何も言われませんが、コードが汚い(保守性が低い)とクソコードと非難されます。

このスレの趣旨はクソコードと呼ばれないための保守性の高いコードを書く知識を得ることです。


保守性

じゃあ保守性が高いって具体的にどういうことなのさ、という話。

一般に「保守性」という言葉はだいたいの雰囲気で使われている感はあるのですが、一応規格があるようなのでそれに従うと、保守性とは次の性質を含みます。

* モジュール性 「機能ごとに小さくまとまっているかどうか」
* 再利用性   「機能を別のプログラムに移しやすいかどうか」
* 解析性    「動作や構造が理解しやすいかどうか」
* 修正性    「変更やバグの修正が簡単かどうか」
* 試験性    「動作テストが簡単かどうか」

言葉ごと覚える必要はないので、だいたいの雰囲気を掴みましょう。

まず、モジュール性は「小さな部品をつくってそれを組み合わせることで全体をつくりましょう」ということです。大きなものを管理するというのは大変で、ある部分を直したら別の部分が壊れた、ということが頻発しがちです。小さな部品を組み立てるようにつくることで、部品ごとに動作をチェックでき、一度に考えるべき内容が少なくてすみます。プラグインに関していうと、ひとつのプラグインに機能を詰め込み過ぎない、などもモジュール性を高めることになります。

再利用性は「可能な限り汎用的な部品をつくりましょう」ということです。ある部品が正しく動作するか確認するのはそれなりに大変な作業です。すでに正しく動作する部品があり、それを再利用できるのであれば手間もバグの発生する可能性も減らせます。ただし、汎用的につくろうとすることで別の問題を引き起こすこともあるのでケースバイケースではあります。

解析性はざっくりと「シンプルにつくりましょう」ということです。プログラミングに慣れてくるとついトリッキーなことをしたくなりますが、わかりづらいコードは人のミスを誘発し、バグを引き起こします。同じ機能を提供できるのであれば、シンプルな動作や構造の方がいいのです。

修正性はそのまま「修正(変更)しやすいようにつくりましょう」です。これ自体を意識するというよりは、他の性質を満たしていると自然と達成されることが多いです。例えば、モジュール性や解析性が高いと、変更に伴って修正が必要な範囲を特定しやすいので、変更時に新たなバグをつくりこんでしまう可能性を減らせます。

試験性もそのまま「テストしやすいようにつくりましょう」です。コードを書き終えた直後に「よし、リリースしよう」なんて人はいないはずで、リリース前には必ずコードが正しく動作するかをテストします。このとき、「このコードをテストするにはこれとそれとあれとどれが必要で……」となってしまうとテストするのも一苦労ですし、バグが出たときに結局どのコードに起因するバグなのかわかりにくくなってしまいます。部品ごとにテストができるようになっていると、問題箇所の特定がとてもスムーズにできます。

規格にはありませんが、保守性に関連してよく語られる品質(?)として『可読性』があります。これは「コードが読みやすいかどうか」で、解析性に近い、またはその一部と捉えてもいいかもしれません。ソースコードはコンピュータが理解できるものであると同時に、人間にとっても理解しやすいものでなければなりません。
結局バグというのは人間のミスでしかないので、人間が理解しやすいコードを書くことは、バグを減らすことにつながります。


開発工程とリファクタリング

さて、保守性が高いとはどういう状態かはわかりました。「これで保守性の高いコードが書けるぜ」となるなら大したもんですが、残念ながら普通そうはなりません。というわけで、どうすれば保守性の高いコードを書けるか、という話です。

「こういう機能のプログラムを作ろう」と決めてからリリースするまでには、基本的に次の工程(下流工程といいます)を踏みます。

1. 設計     「どういう仕組みでつくろうか?」
2. コーディング 「……カキカキ」
3. テスト    「ちゃんと動く?」

意識的にやっているかどうかは別として、まあ異論はないんじゃないかと思います。

で、保守性を高めるには各工程における技術がそれぞれ必要になってきます。

設計では各機能間の関係性を整理してシンプルな構造にまとめ上げる技術が必要とされます。これはとても高度な技術で、こうしておけば大丈夫と言えるような絶対の正解はありません。様々な関係性とそれによって保証できる性質、よいと考えられているパターンなどを学んで、それを実際の問題に適用して整理できることが要求されます。主観ですが、保守性のだいたい八割くらいはこの設計で決まります。

コーディングでは実装したい処理を簡潔に記述する技術が必要とされます。処理の意味を理解し、その意味をコードとして表現できる文章力が要求されます。また、言語や文化における美的センスを解してそれに適応することや、言語の基本的な機能や慣用表現(イディオム)を知って使いこなせることも必要です。

テストでは動作を確認するために必要なテストパターンを構成する技術が必要とされます。どういう状況でバグが発生しうるかを推測し、それらを確認できる可能な限り少ないテストパターンの構成によって、バグのない状態を保証できることが要求されます。

……正直何を言っているのかよくわからないと思いますが、なんかいろいろと必要なんだな、くらいに考えてもらえれば十分です。それぞれの具体的な内容こそが、以降のこのスレで扱う話題になります。

さて、プログラムの開発工程としては上記の通りなのですが、実際問題としてこの工程を一回通すだけで完璧なものをつくるというのは無理があります。完成形を明確にイメージして細部まで完全に考慮した設計を一発で行うというのは人間業じゃないですし、プログラムは拡張されていくもので、最初の設計が常に素晴らしい設計のままであるとも限りません。プログラムの最初の完成形を維持するだけでは高い保守性を確保することはできないのです。

そこで、プログラムには「その挙動を変えずに構造や記述をきれいにする作業」を継続的に行う必要があります。この作業のことを『リファクタリング』といいます。メンテナンスをしやすくするためのメンテナンスですね、ややこしいですが……。

初心者のうちはあまりリファクタリングの意義がわからないかもしれませんが、経験のある人ほどこの大切さが身に染みているのでためらいなくこの作業をします。こればかりは経験しないとわからないような気もするので、とりあえずリファクタリングという作業の存在だけ覚えておいてください。


まとめ

* 保守性は大事
* リファクタリングも大事
* プログラミングは動くものを書けるようになってからが本番
 

fspace

ユーザー
コードの臭い

せっかくリファクタリングの話をしたので、技術編の一発目は「コードの臭い」の話でもしようかと。


コードの臭い Code Smell

「コードの臭い」と呼ばれるものがあります。

これはリファクタリングを必要とするコードの存在を示す兆候のことで、 つまり、「こんな兆候があると保守性が低下している可能性が高いよ」というパターンのことです。 いわゆる危険信号ですね。

有名なMartin Fowler氏のリファクタリング本で紹介されているもので、 去年の暮れに第2版の翻訳が出てるみたいなので気になる人は読んでみるといいかもしれません。

ここではわかりやすいものに絞っていくつか紹介します。


不可思議な名前 Mysterious Name

:confused:
JavaScript:
Game_Actor.prototype.addNewData = function (data, flag1, flag2) {
    // ...
};

プログラミングはよく名前付けの作業だと言われます。プログラミングで最も難しいことのひとつも名前付けだそうです。

誰だってコードの内容を理解するのに逐一処理を追いたくはありません。関数や変数に適切な名前がついていれば、具体的な処理手順を読まずとも、そこで何をやろうとしているのか概要を把握することができます。

関数において、(内部のコードを見ることなく)関数名や引数名だけを見て、処理の概要が把握できないのであれば、それは名前が不適切な可能性が高いです。変数についても同様で、初期化処理を読むことなく、その表す内容を推測できる必要があります。この臭いがするときには時間をかけてでも名前の変更を検討しましょう。

名付けの際にはその名前が使われる文脈と範囲に注意するといい名前になります。名前は詳細であればあるほどいいというものでもありません。長すぎる名前が何度も出てくると野暮ったい印象になります。文脈から十分に読み取れる情報は省略し、極めて短い範囲でしか使われない名前は簡潔に表現します。粒度と詳細度のバランスを考えましょう。

また、名前を考えることは処理の意味を検討することでもあります。もし適切な名前が思い浮かばないのであれば、例え自分の書いたコードだったとしても、その処理の意味をうまく理解できていない可能性があります。処理の意味をよく考え、それでもダメなら処理の切り分け方が間違っているのかもしれません。関数化する範囲を変えながら、いい名前が付けられる分割を探すことで、処理を整理することができます。


重複したコード Duplicated Code

:confused:
JavaScript:
Game_Actor.prototype.compareRanks = function (other) {
    const actorA = this.actor();
    const metaA = actorA.meta["rank"];
    const rankA = typeof metaA === 'string' ? Number.parseInt(metaA, 10) : 0;
    const actorB = other.actor();
    const metaB = actorB.meta["rank"];
    const rankB = typeof metaB === 'string' ? Number.parseInt(metaB, 10) : 0;
    return rankA - rankB;
};

プログラム中にまったく同じ意味でまったく同じコードを書いている箇所があったら、それらはひとつにまとめるべきです。

あるコードに修正や変更が必要になったとき、もし同じコードが複数箇所にあると、それらをすべて書き換えて回らないといけません。ひとつでも書き換え忘れるとバグが発生してしまいます。ひとつのコードを複数箇所で使っている場合には、書き換えは一箇所で済み、書き換え漏れの心配もありません。

:biggrin:
JavaScript:
Game_Actor.prototype.compareRanks = function (other) {
    return this.rank() - other.rank();
};

Game_Actor.prototype.rank = function () {
    const actor = this.actor();
    const meta = actor.meta["rank"];
    const rank = typeof meta === 'string' ? Number.parseInt(meta, 10) : 0;
    return rank;
};

ただし、コードの意味と粒度には注意してください。同じコードであっても意味が違うのであれば共通化すべきではありません。また、あまりに小さい単位で共通化してしまうと意味がわかりづらくなってしまうかもしれません。


長い関数 Long Function

:confused:
JavaScript:
Game_Actor.prototype.doSomething = function () {
    // 何十行にもおよぶコード
};

コアスクリプトを見ればわかるように、多くの関数というのは十行以内の処理で収まります。二十行もあれば大抵の処理はきれいに書けるでしょう。もしそれ以上の長さになってしまっているのであれば、それは関数内の処理をうまく整理できていないのかもしれません。

よく処理を観察し、それらがどんなステップで構成されているか整理し、各ステップに名前を付けて関数化しましょう。小さな関数の組み合わせで表現することで、処理はぐっとわかりやすくなります。なぜなら、関数名を見るだけで何をしているのかだいたい理解できるからです。

一点だけ、この臭いをルール化して扱わないように注意してください。例えば、何行以上であってはならない、何行以内なら問題ない、と厳密に決めてしまうと、本来の読みやすくするという目的に反してしまう可能性があります。処理によって適切な記述量は様々です。あくまでも指標として捉えてください。


長いパラメータリスト Long Parameter List

:confused:
JavaScript:
Window_Base.prototype.drawHpAndMp = function (
    x, y,
    currentHp, maxHp, currentMp, maxMp,
    hpColor, hpFontSize, mpColor, mpFontSize,
) {
    // ...
};

長すぎるパラメータリストは関数の概要を瞬時に把握しづらくします。また、パラメータの内容と順番の対応を記憶しづらくするため、呼び出し時にパラメータを取り違える原因にもなります。

状況によって様々なリファクタリング方法が考えられますが、最も出番が多いのはオブジェクトにまとめてしまう方法です。

:biggrin:
JavaScript:
Window_Base.prototype.drawHpAndMp = function (x, y, hpInfo, mpInfo) {
    const {
        current: currentHp,
        max: maxHp,
        style: { color: hpColor, fontSize: hpFontSize },
    } = hpInfo;
    const {
        current: currentMp,
        max: maxMp,
        style: { color: mpColor, fontSize: mpFontSize },
    } = mpInfo;
    // ...
};

グローバルなデータ Global Data

:confused:
JavaScript:
window.lastActor = null;

Game_Actor.prototype.doSomething = function () {
    if (window.lastActor !== this) {
        // ...
    }

    window.lastActor = this;
};
:confused:
JavaScript:
function MessageLogManager() {
    throw new Error("This is a static class");
};

MessageLogManager.setup = function () {
    this._logs = [];
    this._scenes = {};
};

globalThis.MessageLogManager = MessageLogManager;

あらゆる場所からアクセスできるデータというのはとても便利です。しかし、これらは便利すぎるがゆえにあらゆる可能性を許してしまいます。例えば、後で使おうと記憶しておいたデータがいつの間にか書き換えられてしまうかもしれません。誰が書き換えたかはわかりません。データを書き換えることのできる容疑者は山のようにいます。

プログラミングでは「何でもできる」よりも「必要なことだけができる」が好まれます。制限を与えて状況を簡単化すると、考慮すべきパターンが少なくて済むからです。何でも起こりうる世界を正確に制御するなんてのは人類には早すぎます。

windowglobalThisに対するデータの設定は、どこからでもアクセスできてしまうため極力避けるべきです。これは、XXXManagerのような静的クラスであっても同じです。コアスクリプトに元から存在するものは仕方ありませんが、プラグインから追加する場合にはその判断が妥当かどうかよく検討する必要があります。

また、グローバル直下じゃないから大丈夫というわけでもありません。グローバル変数のプロパティとして定義するのもグローバルに直接定義するのも、誰でもアクセスできるという点ではあまり変わりありません。重要なのは必要最小限の範囲からのみアクセスできることです。

一時的なデータであれば、可能な限り関数の引数として直接渡せないかどうかを検討します。数フレームに渡って必要なデータであれば、適切なオブジェクトをよく考えて、そのプロパティとして記憶しましょう。


データの群れ Data Clumps

:confused:
JavaScript:
TouchInput.detectDoubleClick = function () {
    const prevX = this._prevClickX;
    const prevY = this._prevClickY;
    const prevTime = this._prevClickTime;
    if (prevX !== undefined && prevY !== undefined && prevTime !== undefined) {
        // ...
    }
};

変更されるタイミングや参照されるタイミングがほぼ一致しているデータは、常に群れになって移動します。同じ関数内で一緒に変更され、同じ関数内で一緒に参照され、引数として渡されるときも一緒です。

このようなデータは個別に管理するのではなく、オブジェクトにまとめて群れ全体として管理してやるとすっきりします。

:biggrin:
JavaScript:
TouchInput.detectDoubleClick = function () {
    const prevClick = this._prevClick;
    if (prevClick !== undefined) {
        const { x: prevX, y: prevY, time: prevTime } = prevClick;
        // ...
    }
};

ループ Loops

:confused:
JavaScript:
Game_Party.prototype.gems = function () {
    const result = [];
    const items = this.items();
    for (let i = 0; i < items.length; i++) {
        const item = items[i];
        if (item.meta["gem"] === true) result.push(item);
    }
    return result;
};

ループはプログラミングにおけるとても基本的な構文です。このループがダメと言われるとぎょっとするかもしれません。しかし、最近の言語では多くのケースでループよりもいい代替が用意されています。

JavaScriptでは配列に対して、mapfilterreduceなど様々な関数が定義されています。ループ構文では繰り返しによって何かをしたい、ということしか伝えられませんが、これらの関数を使えば何がしたいのかをより明確に伝えることができます。

:biggrin:
JavaScript:
Game_Party.prototype.gems = function () {
    return this.items().filter(item => item.meta["gem"] === true);
};

ちなみに、極力こっちを使いましょうというだけであって、ループ構文を使ってはいけないわけではありません。念のため。


コメント Comments

:confused:
JavaScript:
Game_Party.prototype.avgLevel = function () {
    const array = this.members();

    // パーティメンバーのレベルの合計を計算する
    let value = 0;
    for (let i = 0; i < array.length; i++) {
        value += array[i].level;
    }

    // 合計値をメンバー数で割って平均値を計算する
    const result = value / array.length;

    return result;
};

コメントはわかりづらい処理を補足説明する基本的にはよいものです。しかし、しばしば根本的な問題を隠すのに使われてしまいます。つまり、『わかりづらい処理』は本当にわかりづらい処理なのか、単にコードが汚いだけではないのか、ということです。

プログラム上で最も信頼できるのはコード記述で、例えコメントがあったとしても、やはりコードを読むことは避けられません。そして、読みづらいコードはコメントがあっても、依然として読みづらいままです。

コメントはコードからは読み取りづらい情報を補足説明するためだけに使い、整理されていないコードを説明するために使ってはいけません。

:biggrin:
JavaScript:
Game_Party.prototype.avgLevel = function () {
    const actors = this.members();
    const totalLevel = actors.reduce((sum, actor) => sum + actor.level, 0);
    const averageLevel = totalLevel / actors.length;
    return averageLevel;
};

おわりに

とりあえず他の知識を必要としない簡単な臭いだけを説明しました。全部で24あるので、そのうちまた紹介するかもしれません。

「変更可能なデータ(Mutable Data)」や「疑わしき一般化(Speculative Generality)」あたりは最近のトレンドにも関連してくるので触れたいところですね。「インサイダー取引(Insider Trading)」、「巨大なクラス(Large Class)」、「相続拒否(Refused Bequest)」あたりは初心者に多いので重要ですが、果たしてツクールプラグインでやったものかどうか……。
 

fspace

ユーザー
とりあえず書き溜めた分はここまでです。

続きはまた気が向いたときに。
 

DarkPlasma

ユーザー
Martin FowlerのRefactoring、気になったのでポチってしまいました。表現も比較的平易で、内容も頷けるものばかりで名著ですね。(初学者が読むべきものかというと微妙なところではありますが)

不吉な臭い辺りの知識というか経験則は、ある程度の量のコードを書かないとピンと来ないもののような気はしています。
この記事のターゲットは書き始めたばかりの初学者というよりは、プラグインをいくつか公開している(あるいはしようとしている)くらいの人でしょうか。
 

fspace

ユーザー
Martin FowlerのRefactoring、気になったのでポチってしまいました。表現も比較的平易で、内容も頷けるものばかりで名著ですね。(初学者が読むべきものかというと微妙なところではありますが)
初めてこの本を読んだときに「リファクタリングってこうやるのか」と感動した記憶があったので紹介したかったんですよね。今回は不吉な臭いだけ取り上げましたが、リファクタリング操作についてもかなり詳細に書かれていて、リファクタリングを学ぶには最適な本だと思います。

不吉な臭い辺りの知識というか経験則は、ある程度の量のコードを書かないとピンと来ないもののような気はしています。
この記事のターゲットは書き始めたばかりの初学者というよりは、プラグインをいくつか公開している(あるいはしようとしている)くらいの人でしょうか。
スレ全体としてはターゲットを特に決めていなくて、自分が書きたくなった内容を自由に書くつもりでいます。

ただやっぱり「きれいに書く」という発想自体、いくらかコードを書いて経験を積まないとなかなか難しいので、それくらいの人が対象になることは今回も含めて多いですね。なるべく知識のない人でも理解できるよう工夫はしますが、プラグイン講座を読んだばかりの人にはちょっと難しいかもしれません。
 

fspace

ユーザー
保守性 基礎の基礎

とりあえず書きたいことから書いた結果、初心者を置いてけぼりにした感があるので、今回は保守性における基礎中の基礎の話です。

普段からJavaScriptのプログラムを書いている人からすれば「当たり前でしょ」というレベルの話ですが、知らなくてもプログラムは書けてしまうため、入門書に詳しく書いていなかったり、重要じゃないと思って見逃しがちな内容のようです。

ちなみに、このあたりが守れていないとコードの内容を読むまでもなく一発で初心者だとバレます。

命名規則

プログラミング言語ではそれぞれ、変数や関数にどんな風に名前をつけるかという規則がある程度決まっています。

JavaScriptでは明確に規定されているわけではないのですが、慣習的に使われているものがあり、皆それに従って書きます。

すべてが ひらがなで かかれている ぶんしょうに突然沢山ノ漢字記述ガ連続スルと読みづらいように、一貫性のない記法は可読性を著しく下げるため、例え自分の好む記法があったとしても、ある程度はルールに従ってコードを記述しなければなりません。

JavaScriptで使われる名前の構成法は次の三種類です。
  • ローワーキャメルケース(または単にキャメルケース)
    • "lowerCamelCase"
    • 最初の単語はすべて小文字、以降の単語は先頭のみ大文字
    • 通常の変数や関数、プロパティなどはすべてコレ
  • アッパーキャメルケース(またはパスカルケース)
    • "UpperCamelCase"
    • 各単語の先頭のみ大文字
    • コンストラクタなど型の一種とみなせるものはコレ
  • スクリーミングスネークケース
    • "SCREAMING_SNAKE_CASE"
    • すべて大文字でアンダースコアで単語間を連結
    • 定数など特殊な意味付けがされたものはコレ
ちなみに、キャメルはラクダのことで大文字の部分がコブのように見えることに由来します。同様に(スクリーミングでない)スネークケース("snake_case")は細長いヘビのように見えるからで、アンダースコアの代わりにハイフンで連結したものは串焼きの意味でケバブケース("kebab-case")と呼ばれます。

基本的には上記の三つのみなのですが、コードを読みやすくするために一貫性をもって書かれているのであれば、ある程度崩して使うことは許容されます。例えば、コアスクリプトではGame_Actorのように分類のためのラベルGame_を付けたり、グローバル変数であることを示すために先頭に$を付けたりしています。

一方で、無意味にスクリーミングでないスネークケースや独自の記法を使ったりすると、単にルールを知らない人あるいはルールを守れない人だと思われてしまいます。十分な理由付けができないのであれば、ルール通りに書くのが無難です。

ちなみに、命名規則にはこれ以外にも「関数は動詞から始める」のような詳細事項がたくさんありますが、例外やら統一されていないルールやらが多いのでここでは触れません。気になる人は「命名規則」や「スタイルガイド」でググってください。

インデント

インデントはコードの構造をわかりやすくするための字下げのことです。基本的には左中括弧の次の行で一段下げて、右中括弧の行で一段上げます。

これがあるとないとでは可読性に雲泥の差があるので、まともにプログラムを書く人でインデントを付けない人は皆無です。よほど始めたばかりでもない限りは初心者でもやっていると思います。

じゃあ、なぜこんな話を取り上げたかというと、インデントにはいくつかのスタイルがあり、一貫性をもってそれを適用できていない人がちらほらといるからです。

インデントのスタイルとして現在よく使われているのは次の三つです。
  • 半角スペース4つで一段(4スペース派)
  • 半角スペース2つで一段(2スペース派)
  • タブ文字1つで一段(タブ派)
最近は2スペース派が増えてきていますが、コアスクリプトは4スペース派ですね(自分はタブ派)。ちなみに、全角スペースを使ってるとネタにされます。

どのスタイルがいいかという話はよく論争になるほど難しい(?)議題ですが、それぞれ一長一短あることは皆が認めるところなので、どれを使っても問題はありません。ただし、混ぜると怒られます。

「わざわざ混ぜないよ」と思うかもしれませんが、例えば、コードをコピペしたりするとよく混ざります。あとは、エディタがタブインデントの設定になっているにも関わらず、スペースキーでインデントしたりすると、エディタが自動挿入したものと手動入力したもので混ざります。

異なるインデントが混ざることを防ぐには、コードフォーマッタと呼ばれるツールを利用するのが最も手っ取り早いと思います。コードフォーマッタはソースコードをきれいに整形するツールで、その機能の一部としてインデントのスタイルを統一してくれます。最近のテキストエディタには標準で搭載されていることも多く、例えば、WindowsのVSCodeでは、"Alt+Shift+F"で実行できます。

ちなみ、コアスクリプト中によく現れるコメント// prettier-ignoreの、Prettierもよく使われる有名なコードフォーマッタです。このコメントはPrettierに「この部分は整形しないでね」と指示するためのものです。

余談ですが、タブとスペースが混在しているソースコードはGitHub上で見るとすぐにわかります。というのも、多くのテキストエディタがデフォルトでタブ文字を半角スペース4つで表示するのに対し、GitHubではこれを半角スペース8つで表示するからです。インデントがガタガタに崩れていてカッコ悪いので気を付けましょう。

Strictモード

JavaScriptには言語特有の事情としてStrictモードというものがあります。

JavaScriptは1995年にできた言語だそうで、すでに25年ほどの歴史があります。その間様々な機能が加えられてきたのですが、そのすべてがいい結果をもたらしたわけではありません。つまりは、「あの時はいいと思った機能」というのがいくつかあります。

本当ならこんな機能は消し去ってしまいたいのですが、これらを削除してしまうと過去に書いたコードが動かなくなってしまうため、苦肉の策としてStrictモードというものが考え出されました。これはスクリプトまたは関数の最初の文(コメントは除く)に"use strict";と記述することで、そのスクリプトまたは関数内において、邪魔な機能をすべてなかった(無効化する)ことにしてしまおうというものです。これのおかげでJavaScriptはいくつかの黒歴史を封印することに成功しました。

このStrictモードが導入されたのもすでに10年近く前の話で、現在ではこれを有効にしないケースというのはまずありません。プラグインを書く時のテンプレートに大抵入っているので、あまり書かれていないコードを見ることはないですが、"use strict";よりも前に別の文を書いているために機能していないケースをたまに見かけます。必ず『スクリプト』または『関数』の【最初の文】として書きましょう。

旧い記述

前項でも触れた通り、JavaScriptには長い歴史があり、Strictモードが導入されたのもすでに10年前です。Strictモードで禁止するほどではなかった機能や、Strictモード導入時はまだ普通に使っていた機能の中にも、現代のJavaScriptではまず使われないものがいくつかあります。

var

MVとMZで最も大きく変化した点といえば、varがいなくなったことだと思います。

varはその仕様に問題が指摘されていたため、ES2015(2015年のJavaScript仕様)でletconstという二つのキーワードによって置き換えられました。現在でも互換性のためにvarは動作しますが、使うべきではないというのが一般の見解です。

varlet/constの大きな違いは変数の有効範囲(スコープ)です。

varには、プログラム上のどこからでもアクセスできるグローバルスコープと、宣言した関数内からのみアクセスできる関数スコープしかありませんでした。そのため、少し複雑な関数を書くと意図せず名前が被って、よくバグの原因となりました。

letconstでは、グローバルスコープと関数スコープに加えて、中括弧で囲まれた範囲内からのみアクセスできるブロックスコープが追加されました。これによってif文やfor文の内部で宣言された変数を外部で参照することができなくなり、また、外部で宣言した変数を誤ってif文やfor文の内部で書き換えてしまう事故も発生しづらくなりました。

JavaScript:
// var の場合
function foo(array, value) {
    var sum = 0;
    for (var i = 0; i < array.length; i++) {
        // fooの関数スコープの変数としてvalueを宣言
        // 別の変数のようでも実は引数のvalueと同一の変数
        var value = array[i];  // 誤って引数のvalueを上書き
        sum += value;
    }
    return sum * value;  // 引数のvalueを使うつもりが別の値に
}

// let の場合
function foo(array, value) {
    let sum = 0;
    for (let i = 0; i < array.length; i++) {
        // for文のブロックスコープの変数としてvalueを宣言
        // 引数のvalueとは別の変数
        let value = array[i];
        sum += value;
    }
    return sum * value;  // 引数のvalueを意図通り参照
}

// おまけ:現代的な記述
const foo = (array, value) => array.reduce((sum, x) => sum + x, 0) * value;

ちなみに、letconstの違いは再代入ができるか否かです。letはできて、constはできません。letconstについてはまたそのうち扱う予定です。

varlet/constの違いは他にもいくつかあるので、気になる人はググってください。

for-in

コアスクリプトで未だに使われていたりはするのですが、通常のプログラミングで滅多に使われなくなった機能としてfor-inループがあります。

for-inループはオブジェクトのプロパティのキーを列挙する構文ですが、オブジェクトのプロトタイプを辿ってしまうという仕様のために、想定外のキーが列挙されてしまうことが多く、とにかくバグを引き起こしがちでした。で、実際プロトタイプを辿って何かしたいケースがあったのかというと、そんなケースは滅多になく、そのオブジェクト自身のキーさえ列挙できれば大抵は事足りました。

そのため、for-inループはES2015で導入されたfor-ofループを使って、次のように書き換えるのが今は一般的です。

JavaScript:
// オブジェクトのキーのみ必要な場合
for (const key of Object.keys(obj)) { /* ... */ }

// オブジェクトの値のみ必要な場合
for (const value of Object.values(obj)) { /* ... */ }

// オブジェクトのキーと値が必要な場合
for (const [key, value] of Object.entries(obj)) { /* ... */ }

// 配列のインデックスのみ必要な場合
for (const index of array.keys()) { /* ... */ }

// 配列の値のみ必要な場合
for (const value of array) { /* ... */ }
for (const value of array.values()) { /* ... */ }

// 配列のインデックスと値が必要な場合
for (const [index, value] of array.entries()) { /* ... */ }

プロトタイプを辿りたいという稀有なケースでもない限りはこっちを使いましょう。

==

JavaScriptは元々スクリプト、つまりはちょっとした挙動のカスタマイズのための言語でした。しかし、そこから大きく成長し、今は大規模プログラムの構築に使われることも多くなりました。これに伴い、JavaScriptでは手早くささっと書けることよりも、間違いのないよう厳密に書けることが重視されるようになってきました。そんな中で問題視されるようになったのが==という演算子です。

==は等価演算子と呼ばれ、よく似たものに===という厳密等価演算子があります。何が違うかというと、==は左右の値の型が違っても頑張って型を合わせようとします。一方、===は左右の値の型が違うならば違う値だと判断します。

で、何が問題かと言うと、==の型を合わせようとする処理の仕様がとんでもなく複雑だということです。もしこれをそらんじることができる人がいるなら、その人はなかなかのJavaScriptギークだと言えます。

プログラマにとって「よくわからないけど動いた」はネタにされるレベルでご法度です。手早く書くことよりも厳密に書くことが重視されるようになった今、理解できない==で型変換を省くよりも、手動の型変換と===を組み合わせる方が好まれるようになり、==を使うケースというのは唯一の例外を除いてなくなりました。

JavaScript:
// 同じ挙動でも型変換と厳密等価演算子を使った方が意図がわかりやすい
if (stringValue == booleanValue) { /* ... */ }
if (Number(stringValue) === Number(booleanValue)) { /* ... */ }

ちなみに、唯一の例外というのはnullundefinedを同時に判定したい場合で、次の二つの記述は同じ意味になります。

JavaScript:
// x == null の形でのみ == を使う人もいる
if (value == null) { /* ... */ }
if (value === undefined || value === null) { /* ... */ }

誤解のないように言っておくと、このケースでは==を使うべきというわけではなく、このケースに限り好みの問題として許容されるというだけです。個人的にはすべて===を使うべきだと思います。

非標準・非推奨のAPI

API(Application Programming Interface)というのはざっくり言うと、プログラムの部品の使い方です。例えば、どんな関数があって、どんな引数を与えると、どんな値を返して、といった正式な仕様のことを言います。

JavaScriptで標準で使える変数や関数もAPIとして仕様が定められています。また、APIとして仕様が定められている変数や関数自体のことをAPIと呼ぶこともよくあります(むしろこっちの意味で使う方が多いかもしれません)。

JavaScriptはその長い歴史もさることながら、最近までWeb上で動作する唯一の言語として、様々なブラウザベンダによって魔改造(独自拡張)されてきた特異な過去をもちます。そんな経緯もあり、JavaScriptには、実装されているけど標準化されていないAPIや非推奨とされているAPIがたくさんあります。基本的にこれらを使ってはいけません。

例えば、ツクールプラグインでよく見かけるものだと、RegExp.$1なんかは非標準のAPIです。また、実はコアスクリプトが使っているKeyboardEvent.keyCodeも現在は非推奨のAPIです(本当はMZで直して欲しかったんですがMVとの互換性を優先したみたいです)。

JavaScriptはその長い歴史と手の出しやすさから、旧い記事や初心者の書いた間違った記事が検索でヒットすることもよくあります。何か使ったことのない機能を利用する場合には、個人のブログだけではなく、信頼性の高いソースも確認することが必要です。

もちろん最も信頼できるソースは言語仕様書なのですが、これは読み慣れている人でもない限りは相当読むのがつらいので、多くの人は比較的信頼性の高いソースとしてMDNというサイトを参照しています。これはFirefoxを開発するMozillaが運営し、Google、Microsoft、Samsungも協力しているサイトで、Webに関するあらゆるAPIが網羅的に解説されています。何か新しいものを知ったら、とりあえずAPIの名前と一緒に「MDN」とつけて検索してみるといいと思います。

おわりに

今回の内容は、他人の書いたプラグインを読んでいて初心者っぽいなと思うポイントのうち、知れば簡単に直せそうなものをピックアップしたものです。

ただ『簡単に直せる』とはいっても、「書いた、動いた、ヤッタ」という自己満足の世界から、「他人に評価される良さとは」という言わば芸術のような世界へと切り替えていくわけなので、人によってはなかなか大変かもしれません。特にこういった内容に対して、その魅力よりも「しなくちゃいけない」という強制力を感じてしまう人はつらいみたいです。

自分があまりそう感じたことがないので正直適切なアドバイスはできませんが、まあ当たり前のことを当たり前に捉えることかなと思います。誰も強制していないこと、評価されない書き方は評価されないこと、評価される必要がなければ努力する必要もないこと、努力していないことは評価されないこと、良いと説明できなければ良いと思ってもらえないこと、人それぞれの考えがあること、自分自身の考えがあること。起こるべきことが起こります。どうするかは自由です。

余談

今回の内容を書くにあたって調べたことで衝撃を受けたことが二つ。
ひとつはRegExp.$1などがレガシーな非推奨機能として標準APIになろうとしていること。
もうひとつはMDNがMozilla Developer Networkじゃなくなっていたこと。
 

げれげれ

ユーザー
いつも参考になる情報をありがとうございます。
毎回かならず熟読させていただいております。
今回も少なからず学びがありました。

---

ちょうど今回の内容とも関連し、以前から少し気になっていたことがありますので、
質問させていただいてもよろしいでしょうか。

無意味にスクリーミングでないスネークケースや独自の記法を使ったりすると
冒頭の命名規則の件なのですが、ツクールのプラグインでは
メソッドをオーバーライドする際のエイリアスにスクリーミングでないスネークケースを
使用している例が多く見受けられます。
私もその流れに倣って採用しているのですが、これはJavaScript本来の記法としては
あまり好ましくない、ツクールプラグイン界隈独自のガラパゴスな慣習だと解釈しておいた方が良いのでしょうか。
(自分なりに調べてみたところ、主にデータベース方面で使われる記法だとか)

もしそうであれば、ツクールのプラグインはともかくとして、
それ以外の場所ではなるべく避けようと思います。
 
上書きの際にスクリーミングでないスネークケースを使うのは、まさに「一般的でない」から使ってるんだと思います。
要するに、他で使わないからまず被らないだろうという安心感があるとゆー。
あとで追加された変数やクラスなどがたまたま同じ識別子になることは絶対ない、といいますか。
 

fspace

ユーザー
冒頭の命名規則の件なのですが、ツクールのプラグインでは
メソッドをオーバーライドする際のエイリアスにスクリーミングでないスネークケースを
使用している例が多く見受けられます。
次のような変数のことでしょうか。

JavaScript:
const Game_Actor_traitObjects = Game_Actor.prototype.traitObjects;

まず、これはスネークケースではありません。スネークケースはすべての単語が小文字かつそれらをアンダースコアで連結した記法のことを言います。もし、この変数をスネークケースで書くのであればgame_actor_trait_objectsになります。

じゃあこの記法は何なのかというと、読みやすさのために導入された、特に名前のない独自記法になります。ツクールプラグインでは書き換え前の関数を記憶するパターンが頻出するため、こういった変数名に一定のパターンがあった方が、名前を考える手間もなくわかりやすく書くことができます。本文中にも書いた通り、読みやすさのために一貫性をもって書かれるのであれば、こういった記法は許容される傾向にあります。

ちなみに、なぜ既存の記法ではなく独自記法を導入するかというと、アッパーキャメルケース(っぽいもの)が使われるクラス名(e.g. Game_Actor)とローワーキャメルケースが使われるメソッド名(e.g. traitObjects)をそのままの形で変数名に残したいからだと思います。あとは、JavaScriptの変数名に利用できる記号というと_$くらいなので、これらを連結に使います($は少し特殊な印象が強すぎるので_を使う人が多いと思います)。

また、とんびさんの言うとおり、この形で変数名を付けておけば、意図せず名前が被ってしまうことを防ぐことができます。逆に、同じメソッドを複数回書き換えようとした場合には名前被りによって検出することもできます。

私もその流れに倣って採用しているのですが、これはJavaScript本来の記法としては
あまり好ましくない、ツクールプラグイン界隈独自のガラパゴスな慣習だと解釈しておいた方が良いのでしょうか。
ツクール以外で使われない記法なのは確かですが、こういった独自記法を導入すること自体はツクール以外でもよくあります。結局のところ、統一されていることが重要なのではなくて、読みやすいことが重要なので。

(自分なりに調べてみたところ、主にデータベース方面で使われる記法だとか)
スネークケースはRuby、Python、Rustのような言語だと、JavaScriptのローワーキャメルケースの代わりに使われます。なので、ツクールプラグインでスネークケースを使っている人をみると、MV以前からプラグインを書いていた人なんだろうなと思います(MV以前はRubyだったので)。
 

げれげれ

ユーザー
>とんびさん、fspaceさん
ご回答ありがとうございます。
とても参考になります。

あまり一般的ではない独自記法ではあるものの、意図と一貫性がきちんと明確になっているので
これはこれとして問題ない、ということですね。
また、なぜこのような記法になっているのかのイメージも掴めたと思います。

ノドにつっかえてた小骨が取れた気分です!
ありがとうございました!
 

fspace

ユーザー
constのススメ

前回、varはES2015でletconstに置き換えられたという話をしました。「じゃあ、letconstはどう使い分けるべきなの?」というのが今回の話です。

結論から言うと、可能な限りすべてconstを使います。

letconst

まずはletconstについての復習。

letconstの違いは再代入が可能かどうかでした。

JavaScript:
// let は再代入可能
let foo = 0;
foo = 42;

// const は再代入不可能
const bar = 0;
bar = 42;  // Error

constで再代入ができないのは変数に対してのみで、プロパティの書き換えは問題なくできます。

JavaScript:
// プロパティの書き換えは問題なし
const baz = { qux: 0 };
baz.qux = 42;

// 同じようでもこっちはエラー
baz = { qux: 42 };  // Error

ちなみに、constはconstant(定数)の略なので、感覚的にプロパティの書き換えができるのはちょっと変なのですが、もとから予約語(変数名などに使えない予約された単語)だったらしく、過去のコードを壊さないために採用されたそうです。

constが好まれる理由

さて、最初に結論としてconstを使うべきといいました。なぜなのか、という話。

プログラミングを始めたばかりだと、おそらく自由に何でもできるというのは素晴らしいことのように感じられると思います。わざわざエラーに怒られたくはないし、能力を制限された窮屈な環境でコードは書きたくないかもしれません。できないよりはできる方がよく、constよりはletの方が魅力的に映るのはそうおかしなことではありません。

しかし、保守の段階に移ると、letは一転、その牙を剥きます。

例えば、次のコードを見てください。

JavaScript:
// 独自の特殊ダメージ量を計算する (let版)
function specialDamage(battler, target) {
    let value = battler.specialDamageBase();
    value *= battler.specialDamageRate();
    value += battler.specialDamagePlus();
    let element = battler.specialElement();
    if (element === 0) element = battler.specialWeaponElement();
    let elementRate;
    if (target.isSpecialWeakElement(element)) {
        elementRate = target.specialWeakElementRate();
    } else if (target.isSpecialSubWeakElement(element)) {
        elementRate = target.specialSubWeakElementRate();
    }
    value *= elementRate;
    return value;
}

このコードを見て、ダメージの計算式がわかるでしょうか。また、コードにバグがあることに気付けるでしょうか。

これをconstで書き直してみます。

JavaScript:
// 独自の特殊ダメージ量を計算する (const版)
function specialDamage(battler, target) {
    const damageBase = battler.specialDamageBase();
    const damageRate = battler.specialDamageRate();
    const damagePlus = battler.specialDamagePlus();
    const battlerElement = battler.specialElement();
    const element = battlerElement === 0 ? battler.specialWeaponElement() : battlerElement;
    const elementRate =
        target.isSpecialWeakElement(element) ? target.specialWeakElementRate() :
        target.isSpecialSubWeakElement(element) ? target.specialSubWeakElementRate() : undefined;
    return (damageBase * damageRate + damagePlus) * elementRate;
}

どうでしょうか。

条件演算子(?:)に対する慣れの問題はあるかもしれませんが、どんな計算式だったか、どんなバグが含まれているのか、どちらもわかりやすくなったと思います(letの方がわかりやすかったと言われたらそれまでですが……)。

ただ、二つのコードを見比べて、これが本当にconstを利用した結果なのか、別の要因によるものじゃないのか、というのは少し疑問かもしれません。

ということで、constの効能について詳しく考えてみます。

再代入できないことの価値

letconstの違いは再代入できないことでしかないのでした。これにはどんな意味があるでしょうか。

プログラミングにおいて制約というのはとても重要な意味があります。なぜなら、制約と保証は表裏一体で、制約を付ければ何らかの保証がされるからです。

ここで、再代入できない制約の裏には次の二つの保証があります。

  • 必ず初期化されている
  • 初期化時の値から絶対に変化していない

ひとつめ、再代入されないということは、初期化を遅延させることができないということです。そのため、宣言時に初期化しない変数というのは無意味で、これらは構文エラーとして検出されます。結果として、constで宣言した変数は必ず明示的に初期化されることになります。初期化忘れが発生することはありません。

ふたつめ、再代入されないということは、初期化時の値のままであるということです。これはつまり、その変数が宣言されている行をみれば、その値が何であるかすぐにわかるということです。宣言箇所と参照箇所の間に何行あろうと関係ありません。

強制される記法

constによる直接の制約は再代入できないことのみですが、これによって間接的にできなくなることがいくつかあります。

例えば、ひとつは変数名の使いまわしです。

JavaScript:
let value = 12;
value = value + 34;  // OK
value *= 56;         // OK

const value = 12;
value = value + 34;  // Error
value *= 56;         // Error

変数名はコメントと同じで、自由な記述が許された、意図を伝えるための重要なツールです。同じ名前を何度も使うことは、書く側にとっては楽ですが、読む側にとっては苦痛になります。

constでは計算の度に別の名前を使うことが強制されます。

JavaScript:
const baseValue = 12;
const adjustedValue = baseValue + 34;
const scaledValue = adjustedValue * 56;

その他、条件分岐やループを使った操作的な値の設定も不可能になります。

JavaScript:
// if文と代入による初期化(const不可)
let value;
if (condition) {
    value = 42;
} else {
    value = 24;
}

操作的な値の設定は、変数の宣言箇所と初期値の設定箇所との距離を離し、それらの関係性や変数の表す内容をわかりづらくしてしまいます。

constでは宣言と初期値を並べて記述することが強制されます。
 

fspace

ユーザー
constへの書き換え

さて、constの方がいいということは理解できたとして、いざ書き換えようとすると、これが意外と難しかったりします。constへの書き換えにはいくつかのパターンを知ることと慣れが必要です。

慣れはどうしようもないので、ここではとりあえずパターンについて紹介することにします。

シンプルなケース

まずは条件分岐やループが存在しないシンプルなケースについて。

そもそも再代入を行っていないletであれば、単にletというキーワードをconstに置き換えるだけでOKです。

JavaScript:
// let
let foo = 42;

// const
const foo = 42;

再代入を行っている場合には、同様にletconstに置き換えた上で、再代入している部分を新たなconst変数の宣言へと置き換えます。

JavaScript:
// let
let foo = 42;
foo *= 256;

// const
const foo = 42;
const bar = foo * 256;

もちろん計算後の値となっているのは新たに導入した変数の方なので、再代入以降の位置で元の変数を参照していた箇所はすべて、新たに導入した変数を参照するように書き換える必要があります。

条件分岐を含むケース

条件分岐を含むケースは少しだけやっかいです。

なぜなら、constは初期化を遅延させることができないからです。また、if文内で宣言した変数をif文外で参照することもできません。

JavaScript:
// let
let foo;
if (condition) {
    foo = 42;
} else {
    foo = 24;
}
let bar = foo * 256;

// const?? #1
const foo;  // Error
if (condition) {
    foo = 42;
} else {
    foo = 24;
}
const bar = foo * 256;

// const?? #2
if (condition) {
    const foo = 42;
} else {
    const foo = 24;
}
const bar = foo * 256;  // Error

じゃあどうするかというと、条件演算子(?:)を使います。

条件演算子はA ? B : Cの形で記述し、Aが真ならBを、Aが偽ならCを計算結果とする、という演算子です。A、B、Cと三つの項をとる演算子がこれしか存在しないことから三項演算子と呼ばれることもあります。

これを利用すると、条件分岐を利用した初期化を次のように記述できます。

JavaScript:
// const
const foo = condition ? 42 : 24;
const bar = foo * 256;

さて、これで条件分岐が書けるようになりました……とするのは少し早計です。

なぜなら、条件分岐内の処理が変数の初期化だけとは限らないからです。

JavaScript:
// let
let foo;
if (condition) {
    doSomethingA();
    foo = 42;
} else {
    doSomethingB();
    foo = 24;
}

条件演算子のBやCの部分には計算結果を返す式しか書けないため、これをそのまま条件演算子で書き換えることはできません。

こういう場合の奥義として、条件分岐ごと関数化してしまうという方法があります。

JavaScript:
// const #1
const doSomethingPreferred = condition => {
    if (condition) {
        doSomethingA();
        return 42;
    } else {
        doSomethingB();
        return 24;
    }
};

const foo = doSomethingPreferred(condition);

変数に対する代入文だった部分は、return文に書き換えます。要は初期値を返す関数にしてしまうわけです。

また、条件分岐の内側の処理の方を関数化してしまうという方法もあります。

JavaScript:
// const #2
const onTrue = () => {
    doSomethingA();
    return 42;
};
const onFalse = () => {
    doSomethingB();
    return 24;
};

const foo = condition ? onTrue() : onFalse();

状況に応じて使い分けましょう。

ループを含むケース

ループを含むケースは少しやっかいです。なぜなら、(以下略)。

ループにも条件分岐と似たような問題がありますが、条件演算子のような便利なものはないので関数化が基本戦略になります。

JavaScript:
// let
let index = -1;
for (let i = 0; i < array.length; i++) {
    if (array[i] === 42) {
        index = i;
        break;
    }
}

// const #1
const find42 = array => {
    for (let i = 0; i < array.length; i++) {
        if (array[i] === 42) return i;
    }
    return -1;
};
const index = find42(array);

さて、これでループが書けるようになりました……というのはやっぱり早計です。

目ざとい人は気付いたかもしれませんが、関数化したコードにはまだletが残っています。そう、ループ変数です。

ループというのはループ変数が繰り返しの度に変化することによって終了します。もしループ変数(あるいはそれに類するもの)が変化しなければ無限ループとなり、プログラムが停止しなくなってしまいます。また、ループ変数以外でも、例えば総和を計算する際には、前のループ処理までの部分的な結果を蓄積するための変数が必要になってきます。これらは繰り返しの度に変化しなければなりません。

じゃあ、ループではletを使うしかないのかというと、実はそうでもありません。次のコードを見てください。

JavaScript:
// const #2
const find42 = array => {
    const rec = (array, i) => {
        if (i < array.length) {
            return array[i] === 42 ? i : rec(array, i + 1);
        } else {
            return -1;
        }
    };
    return rec(array, 0);
};
const index = find42(array);

一見ループ処理に見えないかもしれませんが、同じ処理をletなしで実現しています。

何をしたかというと、『再帰関数』と呼ばれるものを定義しました。

再帰関数とは内部で自分自身を呼び出す関数のことを言います。関数内で呼び出した自分自身が、またその内部で自分自身を呼び出し、さらにその内部で自分自身を呼び出し、……と続くことで再帰関数はループ処理を表すことができます。当然、永遠に呼び出し続けると無限ループになってしまうので、呼び出すかどうかを決める条件分岐が再帰関数内には必ずあります。

さて、これで今度こそループからletを撲滅できました。これにて一件落着としたいところですが、少し疑問に思う人もいるかもしれません。我々の目的はきれいなコードを書くことであって、letを淘汰することではありません。すなわち、「再帰関数は本当に読みやすいのか」ということです。

実のところ、慣れや文化の問題もあって、これに結論を下すことは容易ではありません。JavaScriptのように手続き型プログラミングという考えの流れを汲む言語では、ループ構文とletによる記述の方が一般的ですが、最近注目されている関数型プログラミングという考えの流れを汲む言語では、再帰関数による記述の方が一般的です。

結局のところ、どちらを使っても構いませんし、状況によって使い分けても構いません。自分がわかりやすいと思う方を使ってください。

標準APIの利用

ここまでは、JavaScriptの構文に限定した話をしてきました。

しかし、JavaScriptで提供される標準APIの中には、内部で条件分岐やループを処理してくれるものが多数あります。これらを使えば、わざわざ自分で処理を関数化しなくても、一発でconstを初期化できる場合があります。

たくさんあるのでここでは紹介しませんが、調べて少しずつ覚えていってください。

JavaScript:
// 内部に条件分岐を含むAPI
const minValue = Math.min(a, b);
const maxValue = Math.max(a, b);

// 内部にループを含むAPI
const sum = array.reduce((sum, x) => sum + x, 0);
const index1 = array.indexOf(42);
const index2 = array.findIndex(x => x === 42);

無意味なズルはしないこと

さて、ここまでパターン別にletconstに書き換える方法を紹介してきたのですが、実はletconstに書き換える万能の方法があります。それが次のコードです。

JavaScript:
// let
let value = 12;
value = value + 34;
value *= 56;

// const??
const obj = { value: 12 };
obj.value = obj.value + 34;
obj.value *= 56;

確かにletはなくなりました。

しかし、これが何の意味もない書き換えであることはわかると思います。これならletで書かれていた方がいくらかマシです。

実は別解もあります。

JavaScript:
// no let??
(value => {
    value = 12;
    value = value + 34;
    value *= 56
})();

関数の引数は仕様的にはletと同じで再代入が可能です。これは引数が省略された際、デフォルト値を設定するのによく使われていたのですが、デフォルト引数が指定可能になった現在、引数に値を再代入すべきケースは思い当たりません。

関数の引数は本質的にconstだと思い込んでコードを記述した方がミスは少ないでしょう。

おまけ:constによる関数定義

もしかしたら今回のコードを読んでいて、次のような関数定義に違和感を覚えた人もいるかもしれません。

JavaScript:
const doSomething = () => { /* ... */ };
  /* or */
const doSomething = function () { /* ... */ };

「関数定義といえばこうじゃないの?」と。

JavaScript:
function doSomething() { /* ... */ }

結論から言うと、どちらを使っても構わないのですが、どちらも若干の欠点があります。

前者はconstの行が実行されて初めて変数に関数が代入されます(もしかしたら関数を変数に代入できること自体に驚く人もいるかもしれません)。そのため、constの行よりも前でその関数を実行しようとすると、未初期化変数の参照でエラーとなってしまいます。

一方、後者はホイスティングと呼ばれる仕様によって、定義行より前の位置からでも呼び出すことができます。その代わり、この定義方法は仕様がvarと同じなため、スコープの問題を抱えます。つまり、誤って同名の関数を定義してもエラーとならず、意図せず別の関数を実行してしまう可能性があります(変数に比べると発生率が格段に低いのでそれを問題と捉えるかどうかは人によります)。

あとはアロー関数を使いたいなら必然的に前者になります。アロー関数はthisを利用しないことを明示する意味でも有効です。

おわりに

というわけで、『constを使おう!』という話でした。

単に再代入できないというだけなのですが、「制約って何で重要なの?」とか、「ソースコードの書き換えって具体的にどうやるの?」とか、基礎として大事な発想がいろいろと詰まっているので、そういうところの雰囲気を感じてもらえればな、と思います。
 
トップ