Tech Racho エンジニアの「?」を「!」に。
  • 開発

JavaScriptスタイルガイド 9〜14: クラス、モジュール、イテレータ、プロパティ、変数、巻き上げ(翻訳)

概要

AirbnbによるJavaScriptスタイルガイドです。
MITライセンスに基いて翻訳・公開いたします。


github.com/airbnbより

凡例

原文にはありませんが、項目ごとに目安となる分類を【】で示しました。

  • 【必須】【禁止】:従わないと技術的な悪影響が生じる
  • 【推奨】【非推奨】:技術上の理由から強く推奨される、または推奨されない
  • 【選択】:採用してもしなくてもよいスタイル
  • 【スタイル】:読みやすさのためのスタイル統一指示
  • 【知識】:指示に該当しない基礎知識

JavaScriptスタイルガイド 9〜14: クラス、モジュール、イテレータ、プロパティ、変数、巻き上げ (翻訳)

メモ: 本ガイドではBabelの利用を前提とします。また、babel-preset-airbnbあるいは同等のライブラリが必要です。他に、airbnb-browser-shimsまたは同等のライブラリでshims/polyfillsをアプリにインストールしていることも前提とします。

9. クラス(class)とコンストラクタ(constructor)

9.1 【推奨】常にclassを使うこと。prototypeの直接操作は避けること。

理由: class文法は簡潔かつわかりやすい。

// Bad
function Queue(contents = []) {
  this.queue = [...contents];
}
Queue.prototype.pop = function () {
  const value = this.queue[0];
  this.queue.splice(0, 1);
  return value;
};

// Good
class Queue {
  constructor(contents = []) {
    this.queue = [...contents];
  }
  pop() {
    const value = this.queue[0];
    this.queue.splice(0, 1);
    return value;
  }
}

9.2 【推奨】継承にはextendsを使うこと。

extendsはビルトイン機能であり、instanceofを損なわずにプロトタイプ機能を継承できる。

// Bad
const inherits = require('inherits');
function PeekableQueue(contents) {
  Queue.apply(this, contents);
}
inherits(PeekableQueue, Queue);
PeekableQueue.prototype.peek = function () {
  return this.queue[0];
};

// Good
class PeekableQueue extends Queue {
  peek() {
    return this.queue[0];
  }
}

9.3 【選択】メソッドは、メソッドチェーンの目的でthisを返してもよい。

// Bad
Jedi.prototype.jump = function () {
  this.jumping = true;
  return true;
};

Jedi.prototype.setHeight = function (height) {
  this.height = height;
};

const luke = new Jedi();
luke.jump(); // => true
luke.setHeight(20); // => undefined

// Good
class Jedi {
  jump() {
    this.jumping = true;
    return this;
  }

  setHeight(height) {
    this.height = height;
    return this;
  }
}

const luke = new Jedi();

luke.jump()
  .setHeight(20);

9.4 【選択】独自のtoString()メソッドを作成してもよい。ただし正しく動作させ、副作用が生じないようにすること。

class Jedi {
  constructor(options = {}) {
    this.name = options.name || 'no name';
  }

  getName() {
    return this.name;
  }

  toString() {
    return `Jedi - ${this.getName()}`;
  }
}

9.5 【知識】クラスには、指定のない場合にデフォルトのコンストラクタが1つ与えられる。空のコンストラクタ関数や、親クラスに委譲するだけのためのコンストラクタ関数は不要。

// Bad
class Jedi {
  constructor() {}

  getName() {
    return this.name;
  }
}

// Bad
class Rey extends Jedi {
  constructor(...args) {
    super(...args);
  }
}

// Good
class Rey extends Jedi {
  constructor(...args) {
    super(...args);
    this.name = 'Rey';
  }
}

9.6 【禁止】クラスのメンバーの重複は避ける。

理由: クラスのメンバー宣言が重複すると、最後の宣言が暗黙で優先される。重複はほとんどの場合バグの原因になる。

// Bad
class Foo {
  bar() { return 1; }
  bar() { return 2; }
}

// Good
class Foo {
  bar() { return 1; }
}

// Good
class Foo {
  bar() { return 2; }
}

10. モジュール(module)

10.1 【推奨】モジュールは常にimportexportで使い、標準でないモジュールシステムの利用は避けること。好みのモジュールシステムへのトランスパイルはいつでも行える。

理由: モジュールは未来の機能だ。今のうちから未来の機能を使っておこう。

// Bad
const AirbnbStyleGuide = require('./AirbnbStyleGuide');
module.exports = AirbnbStyleGuide.es6;

// ok
import AirbnbStyleGuide from './AirbnbStyleGuide';
export default AirbnbStyleGuide.es6;

// best
import { es6 } from './AirbnbStyleGuide';
export default es6;

10.2 【非推奨】importでワイルドカードを使わないこと。

理由: ワイルドカードを避けることで、デフォルトのexportが1つに確定する。

// Bad
import * as AirbnbStyleGuide from './AirbnbStyleGuide';

// Good
import AirbnbStyleGuide from './AirbnbStyleGuide';

10.3 【スタイル】importから直接exportしないこと。

理由: 一行に収まる方が簡潔ではあるが、importexportの方法を1つに定めて統一する方がよい。

// Bad
// filename es6.js
export { es6 as default } from './AirbnbStyleGuide';

// Good
// filename es6.js
import { es6 } from './AirbnbStyleGuide';
export default es6;

10.4 【推奨】1つのパスからのimportは1つにまとめること。

理由: 同じパスからのimportが複数あるとコードのメンテナンスが困難になることがある。

// Bad
import foo from 'foo';
// … some other imports … //
import { named1, named2 } from 'foo';

// Good
import foo, { named1, named2 } from 'foo';

// Good
import foo, {
  named1,
  named2,
} from 'foo';

10.5 【必須】ミュータブルなバインドをexportしないこと。

理由: ミューテーションは一般的に避けるべきであり、特にミュータブルなバインドのexportは避けるべき。こうした特殊な手法が必要になることはなくもないが、一般には定数の参照だけをexportすべき。

// Bad
let foo = 3;
export { foo };

// Good
const foo = 3;
export { foo };

10.6 【推奨】複数のモジュールを1つのexportでエクスポートする場合、名前付きexportよりもdefaultexportが望ましい。

// Bad
export function foo() {}

// Good
export default function foo() {}

10.7 【必須】import文は他の文よりも上に置くこと。

理由: importは巻き上げられる(ホイスティング)ので、常にトップに配置することで予想外の挙動を防げる。

// Bad
import foo from 'foo';
foo.init();

import bar from 'bar';

// Good
import foo from 'foo';
import bar from 'bar';

foo.init();

10.8 【スタイル】importが複数行に渡る場合はインデントすること。インデント方法は配列やオブジェクトリテラルと同様にする。

理由: 波かっこ{ }のスタイルを本スタイルガイドの他の波かっこブロックのルールに合わせるため。末尾のカンマ,も省略しないこと。

// Bad
import {longNameA, longNameB, longNameC, longNameD, longNameE} from 'path';

// Good
import {
  longNameA,
  longNameB,
  longNameC,
  longNameD,
  longNameE,
} from 'path';

10.9 【禁止】モジュールのimport文ではWebpackローダーの文法を使わないこと。

理由: importでWebpack文法を使うとモジュールバンドラー(module bundler)と結合される。Webpackのローダー文法はwebpack.config.jsで使うのが望ましい。

// Bad
import fooSass from 'css!sass!foo.scss';
import barCss from 'style!css!bar.css';

// Good
import fooSass from 'foo.scss';
import barCss from 'bar.css';

11. イテレータ(iterator)とジェネレータ(generator)

11.1 【非推奨】イテレータを使ってはならない。for-infor-ofなどのループではなく、JavaScriptの高階関数の利用が望ましい。

本スタイルガイドでのイミュータブル重視ルールを守るため。値を返す純粋な関数を扱う方が、副作用を伴う方法よりも理解しやすい。

配列の列挙にはmap()every()filter()find()findIndex()reduce()some()などを使うこと。
配列の生成にObject.keys()Object.values()Object.entries()を使うことでオブジェクトを列挙できるようになる。

const numbers = [1, 2, 3, 4, 5];

// Bad
let sum = 0;
for (let num of numbers) {
  sum += num;
}
sum === 15;

// Good
let sum = 0;
numbers.forEach((num) => {
  sum += num;
});
sum === 15;

// ベスト(【TBD】use the functional force)
const sum = numbers.reduce((total, num) => total + num, 0);
sum === 15;

// Bad
const increasedByOne = [];
for (let i = 0; i < numbers.length; i++) {
  increasedByOne.push(numbers[i] + 1);
}

// Good
const increasedByOne = [];
numbers.forEach((num) => {
  increasedByOne.push(num + 1);
);

// ベスト(関数らしさを保っている)
const increasedByOne = numbers.map(num => num + 1);

11.2 【非推奨】ジェネレータは当面使わないこと。

理由: ES5へのトランスパイルがあまりうまくいっていない。

11.3 【必須】ジェネレータの利用が避けられない場合や、上述のアドバイスに従うつもりがない場合は、ジェネレータの関数のシグネチャでのスペーシングを正しく行うこと。

理由: function*が合わさることで1つの概念キーワードが構成される。*functionの修飾子ではない。function*は全体で1つであり、単なるfunctionとは異なる点に注意。

// Bad
function * foo() {
  // ...
}

// Bad
const bar = function * () {
  // ...
};

// Bad
const baz = function *() {
  // ...
};

// Bad
const quux = function*() {
  // ...
};

// Bad
function*foo() {
  // ...
}

// Bad
function *foo() {
  // ...
}

// 非常に悪い
function
*
foo() {
  // ...
}

// 非常に悪い
const wat = function
*
() {
  // ...
};

// Good
function* foo() {
  // ...
}

// Good
const foo = function* () {
  // ...
};

12. プロパティ(property)

12.1 【推奨】プロパティへのアクセスにはドット記法.を使うこと。

const luke = {
  jedi: true,
  age: 28,
};

// Bad
const isJedi = luke['jedi'];

// Good
const isJedi = luke.jedi;

12.2 【推奨】プロパティに変数でアクセスする場合は角かっこ[ ]記法を使うこと。

const luke = {
  jedi: true,
  age: 28,
};

function getProp(prop) {
  return luke[prop];
}

const isJedi = getProp('jedi');

13. 変数(variable)

13.1 【必須】変数宣言は常にconstまたはletで行うこと。このとおりにしない変数はグローバルになってしまう。キャプテン・プラネットで警告されているように、私たちはグローバル名前空間の汚染を避けたいと考えている。

// Bad
superPower = new SuperPower();

// Good
const superPower = new SuperPower();

13.2 【推奨】constletは変数宣言ごとに付けること(まとめて書かない)。

理由: 変数宣言の書式が揃うので追加が楽になるうえ、末尾の;,の取り違えを心配しないで済む。そうしないと約物レベルのわかりにくい違いが生じてしまう。デバッガでも宣言ごとにステップ実行されるのもよい。まとめて宣言するとデバッガでも1ステップで終わってしまい、宣言ごとにチェックしづらくなる。

// Bad
const items = getItems(),
    goSportsTeam = true,
    dragonball = 'z';

// Bad
// (上との違いに注意: こういう間違いが起きやすくなる)
const items = getItems(),
    goSportsTeam = true;
    dragonball = 'z';

// Good
const items = getItems();
const goSportsTeam = true;
const dragonball = 'z';

13.3 【スタイル】const行を先に書き、次にlet行を書くこと(両者を混ぜて書かない)。

理由: 先に行った代入に依存する代入を追加するときに便利。

// Bad
let i, len, dragonball,
    items = getItems(),
    goSportsTeam = true;

// Bad
let i;
const items = getItems();
let dragonball;
const goSportsTeam = true;
let len;

// Good
const goSportsTeam = true;
const items = getItems();
let dragonball;
let i;
let length;

13.4 【必須】変数の代入は、意味のある場所で行うこと。

constletはブロックスコープであり、関数スコープではない。

// Bad - 不要な関数呼び出し
function checkName(hasName) {
  const name = getName();

  if (hasName === 'test') {
    return false;
  }

  if (name === 'test') {
    this.setName('');
    return false;
  }

  return name;
}

// Good
function checkName(hasName) {
  if (hasName === 'test') {
    return false;
  }

  const name = getName();

  if (name === 'test') {
    this.setName('');
    return false;
  }

  return name;
}

13.5 【禁止】変数代入をチェイン(連鎖代入)しないこと。

理由: 変数代入をチェインすると、暗黙でグローバル変数が作成される。

// Bad
(function example() {
  // JavaScriptでは以下のように解釈される:
  // let a = ( b = ( c = 1 ) );
  // letキーワードは変数aにしか効かない
  // 変数bと変数cはグローバル変数になる
  let a = b = c = 1;
}());

console.log(a); // ReferenceErrorをスロー
console.log(b); // 1
console.log(c); // 1

// Good
(function example() {
  let a = 1;
  let b = a;
  let c = a;
}());

console.log(a); // ReferenceErrorをスロー
console.log(b); // ReferenceErrorをスロー
console.log(c); // ReferenceErrorをスロー

// `const`でも同様の点に注意すること

13.6 【非推奨】単項の++--は避けること。

理由: eslintのドキュメントにあるとおり、単項の++--にはセミコロン;が自動挿入されやすいため、これで値を増減させるとアプリでひそかにエラーが発生することがある。
また、num++num ++よりもnum += 1などの文の方が値を改変していることが明確になる。単項の++--による増減を排除することで、プログラムで思わぬ挙動の原因になる「値の事前インクリメント/デクリメント問題」も防止できる。

// Bad

const array = [1, 2, 3];
let num = 1;
num++;
  --num;

let sum = 0;
let truthyCount = 0;
for (let i = 0; i < array.length; i++) {
  let value = array[i];
  sum += value;
  if (value) {
    truthyCount++;
  }
}

// Good

const array = [1, 2, 3];
let num = 1;
num += 1;
num -= 1;

const sum = array.reduce((a, b) => a + b, 0);
const truthyCount = array.filter(Boolean).length;

14. 巻き上げ(ホイスティング: hoisting)

14.1 【知識】var宣言はスコープの先頭へ巻き上げられるが、宣言した変数の代入は巻き上げられない点に注意。一方、const宣言とlet宣言はTDZ(Temporal Dead Zones)という新しい概念の恩恵を受ける。「typeofはもはや安全ではない」を理解することが重要である。

// notDefinedというグローバル変数は定義されていないことが前提
// 当然これは動かない
function example() {
  console.log(notDefined); // => ReferenceErrorをスロー
}

// 変数参照の後で変数宣言を作成すると
// 変数が巻き上げられるため、これは動作する
// 注意: 代入の値`true`は巻き上げられない
function example() {
  console.log(declaredButNotAssigned); // => undefined
  var declaredButNotAssigned = true;
}

// インタプリタは変数宣言をスコープの冒頭に巻き上げられる
// つまりコード例は次のように書き換えられる
function example() {
  let declaredButNotAssigned;
  console.log(declaredButNotAssigned); // => undefined
  declaredButNotAssigned = true;
}

// constやletを使う場合
function example() {
  console.log(declaredButNotAssigned); // => ReferenceErrorをスロー
  console.log(typeof declaredButNotAssigned); // => ReferenceErrorをスロー
  const declaredButNotAssigned = true;
}

14.2 【知識】無名関数式では変数名が巻き上げられるが、関数の代入は巻き上げられない点に注意。

function example() {
  console.log(anonymous); // => undefined

  anonymous(); // => TypeError anonymous is not a function

  var anonymous = function () {
    console.log('無名関数式');
  };
}

14.3 【知識】名前付き関数式では変数名が巻き上げられるが、関数名と関数本体は巻き上げられない点に注意。

function example() {
  console.log(named); // => undefined

  named(); // => TypeError named is not a function

  superPower(); // => ReferenceError superPower is not defined

  var named = function superPower() {
    console.log('とびます');
  };
}

// 関数名と変数名が同じ場合も同様
function example() {
  console.log(named); // => undefined

  named(); // => TypeError named is not a function

  var named = function named() {
    console.log('named');
  };
}

14.4 【知識】関数宣言では、関数名と関数本体が巻き上げられる点に注意。

function example() {
  superPower(); // => とびます

  function superPower() {
    console.log('とびます');
  }
}

関連記事(JavaScript)

Vue.jsサンプルコード(01〜03)Hello World・簡単な導入方法・デバッグ・結果の表示とメモ化

JavaScript、jQuery入門ーフォーム作成で実際に使った例を振り返りながら

JavaScriptでElement.styleがnullになって焦った

HTML + CSS + JavaScript で簡単に導入できるdatetimepicker の比較

Rails アプリケーション開発で役に立ったJavaScript デバッグの小技

JSの非同期処理を初めてES6のPromiseを使ったものに書き換えてみた

JavaScriptでモーダルウィンドウを出すなら


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。