JavaScript
es6
codingstyle

ES6時代のコーディングスタイルを考える

More than 1 year has passed since last update.

ここではコーディングスタイルを考える上で、ES6(ECMAScript2015)の以下の特徴に注目して考察するものです。

  • 変数宣言は、constを基本とする
  • クラス宣言文や関数宣言文はブロックスコープ
  • 文字列では、テンプレートリテラルを活用する
  • ジェネレータのために、複数の構文が拡張された

また、以下の基準を採用していることを特に触れておきます。

  • ES5コードの混在、ES5時代のスタイルとの互換性は取らない
  • ES6 modulesが存在しない状況で適用可能なスタイルを取る
  • 視認性を犠牲にコーディング時のミスの影響を隠蔽するスタイル(ヨーダ記法など)は取らない
  • 局所視認性より、全体視認性での高さを重視する
    • 微妙に違うものを区別しやすいスタイルにする
    • 同じものは同じスタイルを取る
    • 一覧しやすくコンパクトに書ける方を採用する

変数宣言は、constを基本とする

ES6では再代入を認めないconst変数が導入されました。そしてビルトインの機能も合わせて、コード中の変数の大半はconst変数で済むようにできる状況になりました。

変数は、その利用範囲がなるべく小さくなるように用いる

  • 変数は、使う場所のなるべく近くで宣言する
  • 可能な限りconst変数を基本とし、そうできない場合に限り再代入可能なlet変数にする
    • var変数はグローバルコンテキストに影響させる目的でのみ用いる(関数内では用いない)
    • for-of ループでは、const変数を用いる
  • 変数1宣言文での複数変数宣言は、論理関係で同等になるもの(xywidthheightなど)だけでまとめるとよい

ES5以前のvar変数には、関数スコープおよび宣言部のhoisting(巻き上げ)があって、その存在が有名なコーディングスタイルに大きく影響を与えていました。とりわけ、変数宣言を関数の頭にまとめるスタイルは、モダンなプログラミング言語で行う「変数の影響範囲をなるべく小さくする」コードデザインを阻害し、しかも複数箇所で同名変数を再利用する可読性の低いコードが量産しました。const変数では、このような頭での一括宣言自体が不可能になります。

const及びlet変数は、その範囲をなるべく小さくなるよう宣言します。なるべく後ろ側に、変数を使う式に近いところに配置します。このため変数宣言は、その論理的な意味の違いによって、ブロック内の別々の位置に来ることが基本です。

(for ofループでのconst変数の利用は、firefoxで長らく使用できない状態でした。firefox-51にてやっと利用可能になったようです。)

インデントは2スペース

  • 2スペースのインデントを用いる
  • const宣言文の折り返しは6スペース、let宣言文の折り返しは4スペースを用いる

constが5文字であるため、宣言文で複数の変数で折り返して記述する場合、6スペースで折り返すと変数の頭が揃います。4スペースだとconst宣言文の折り返しだけが行頭が違う状況になります。

クラス宣言文や関数宣言文はブロックスコープ

  • 関数オブジェクト式を与える変数宣言文ではなく、関数宣言文を用いることを考える

クラス定義や関数定義分も変数同様ブロックスコープになりました。

ちなみに、引数のデフォルト値の式は、関数内(の頭)で順に評価されるコードです(Pythonのような宣言時で1回の評価ではない)。値でキャプチャしてるわけではないので、副作用のある変数を使う場合には注意すること。

// tail call recursion
function uniq(A, i = 0, r = [], h = A[i], l = r.length) {
  if (i === A.length) return r;
  return uniq(A, i + 1, l === 0 ? [h] : h === r[l - 1] ? r : [...r, h]);
}

console.log(uniq([1, 2, 2, 3, 3, 3, 4, 4, 4, 4])); //=> [1, 2, 3, 4]

文字列では、テンプレートリテラルを活用する

  • 文字列リテラルは、ダブルクォート"を基本で用いる
  • 文字連結は、+を使わず、すべてテンプレートリテラルで行う
  • シンボル用途は、なるべく文字列を使わず、Symbol値を用いる

ES6では、バッククォートでくくった文字列を作るテンプレートリテラル構文が、追加されました。テンプレートリテラル内には${式}を記述でき、その式の評価値から文字列が展開されます。

文字列の結合では、+Arrayに入れてjoin('\n')を用いたスタイルが使われていたが、テンプレートリテラルはその用途を広くカバーすることになります。

const line = `${name}: ${value}`;

またES5のコーディングスタイルでは、HTML属性との兼ね合いから、シングルクォート'を基本にする文字列を採択するものが多いが、シングルクォートと(シンタックスハイライトを行っていても)バッククォートとの識別性が、とくにターミナル上で、悪いという欠点があります。

また、シングルクオート文字列をシンボルの代わりに用いるスタイルも存在します。この場合は、クォート文字で区別するのではなく、ES6で追加されたSymbolを用いるのが望ましいです。

const localProps = Symbol`props`;

のように、テンプレートリテラルでシンボルを書くことも可能でしょう(式展開は使えないが、シンボルなら文字列埋め込みのほうがよい)。

ジェネレータのために、複数の構文が拡張された

  • function *()yield *iterではなく、function* ()yield* iterを用いる
  • オブジェクトやクラスのメソッド構文でのジェネレータは、* method()* [method]()ではなく、*method()*[method]()を用いる

functionのあとの*のあるなしではなく、function*の時点で、ジェネレータであると識別できることを重視した。

ちなみにyield*式の評価値は、(pythonのyield from同様)ジェネレータのreturnの値が得られる。Arrayなど普通のイテレータではundefinedになる。

その他

  • 一括代入文は、カンマ式ではなく、Arrayのdestructuringを使う
// euclidean algorithm
while (b > 0) [a, b] = [b, a % b];
  • 1引数アロー関数式の括弧の省略
const len = (str) => str.length; //代入式の右辺の場合、括弧を入れておく
["a", "ab", "abc"].map(s => len(s)); //引数の場合、括弧は省く
  • method.bind(this)は使わず、アロー関数() => this.method()を使う

  • 使わない引数名には__1 (ブロック内でlodash等は使わない前提)

  • オブジェクトの情報隠蔽は、classではなく、Proxyを使う

追記: 実行速度に関して

シンプルなコードは、最速なコードではないでしょう。極端な話をすると、JavaScriptの実行環境では、Arrayにアクセスするコードは、ループを変数で展開すると実行時間が大幅に短くなるが、コードの量とその複雑度は上がります。

しかし普通は、小さくシンプルであるほうが読みやすく、なによりバグが入る隙が小さいです。そして実行速度第一で書いたつもりの未成熟なコードとくらべても、小さなコードは十分速いでしょう。たとえば上述のuniqはたった4行だけど末尾再帰です。速いつもりではじめから書かれたコードで実際どれだけ速いでしょうか。

プログラムは、「任意の入力データと結果の間の全体関係」の記述という側面があります。それは処理中の状態でも常に成立する関係(不変条件invariant)であり、それこそが処理の正しさの源です。シンプルなコードというのは正しさがその記述から自明であることです。このuniqでいえば、「要素が一つ増えた時(i + 1h = A[i])に、最後の値と同じ値だった場合(h === r[l - 1])はそのまま(r)が、違う場合では最後にその値を追加したもの([...r, h])が結果である」が任意の入力全体に対して成り立つのがuniqの意味の本質であって、(そこに「最後の値」がない最初の状況での関係を加えたうえで)再帰呼び出し部分にそのまま記述しています。

これを表現するのに何行もあるとどうでしょう。その構成要素である変数が長い範囲に散らばってたら、正しいこと(もしくはバグが入った時どこが間違ってるか)を判別するのにどれだけかかるでしょうか。