現代的JavaScriptおれおれアドベントカレンダー2017 – 04日目

JavaScriptはオブジェクト指向プログラミング言語の一種ですが、「クラスベース」の他言語と異なり「プロトタイプベース」だと言われています。クラスの代わりにプロトタイプ prototype を使ってあれやこれやしてきました。

そんな我が道を行くJS君でしたが、やっぱり寂しかったのか、クラスの仕組みが導入されました。継承もできる。使っていきましょう。

使い方

ざっくりこんな感じ。

class Sushi {
  constructor(neta) {
    this.neta = neta;
  }

  get name() {
    return this.neta;
  }

  make() {
    console.log(`Hey ${this.name} o-match!`);
  }
}

const tuna = new Sushi('tuna');
tuna.make();  // => "Hey tuna o-match!"

なんとなくオブジェクトにメソッド書いてきたやりかたに近い感じですね。カンマとかいらないけど。

constructor() はインスタンス生成時に自動的に呼ばれるやつです。プロパティの初期化とかしましょう。

プロパティ

メソッドは前項のように書けるんですが、プロパティはどうするかというと、全部 constructor() の中で、動的に this へ追加していきます。

constructor(neta) {
  this.neta = neta;
}

JavaScriptのクラスはメソッド(か静的メソッド)しか定義できません。無理やり書くと “Exception: SyntaxError: bad method definition” とかになります。(プロパティも書けるようになるとかいう話も聞いた気がするけど、ES2017でもそうなってないすね。)

他の言語やってると「ンン!」という感じあるかもしれないですけど、まあほらJavaScriptは動的にプロパティ持てるから……。むしろ静的に持とうとすると失敗する (FAQ) から……。どうせ初期化時に値を設定するんだからこっちの方がわかりやすくない? だめ? だめならいいです。

あとは代替手段じゃないですけど、今回の name のように、getterを用意するという手もあります。場合によってはこちらもご検討ください。(別稿参照)

継承

extends でやります。あと constructor() からは super() で継承元クラスのコンストラクタを実行しましょう。

class SushiRoll extends Sushi {
  constructor(neta) {
    super(neta);
  }

  get name() {
    return `${this.neta} roll`;
  }
}

const californiaRoll = new SushiRoll('california');
californiaRoll.make();  // => "Hey california roll o-match!"

extends の右辺、上記の Sushi の部分には割と何でも書ける感じです。例えばここに class 式を丸ごとぶっこむことも可能です。(実践はご遠慮ください。)

静的メソッド(クラスメソッド)

static を付けます。

class Sushi {
  static prepareSumeshi() {
    console.log('Cooking rice...');
  }

  // ...
}

Sushi.prepareSumeshi();

class 宣言と class 式

function と同様に、クラスも宣言 (declaration) と式 (expression) の両方が用意されています。前項の例は宣言の方ですね。(セミコロンもいらない。)

いちおう、 class 式で書いて関数の引数にクラスを与えたりできます。

// 引数に
console.log(class {});

// 変数に
const Tako = class extends SushiNeta {
  // ...
};

それより module.exports に与えるのがありそう。

module.exports = class MyModule {
  // ...
};

ちなみに export に与える場合は宣言扱いです。セミコロン不要。

export default class MyModule {
  // ...
}

その他

巻き上げがない

var に対する let のように(?)、 class も function と異なり「巻き上げ」が起こりません。

なのでこれ↓だと未定義エラー。

const tuna = new Sushi('tuna');  // Uncaught ReferenceError: Sushi is not defined
                                 // Exception: ReferenceError: can't access lexical declaration `Sushi' before initialization

class Sushi {
}

常にStrictモード

クラス記法の中身は常に厳格な記述をすることになってます。

変数宣言なしで foo = 123 とか書いたらグローバル変数が作られる代わりにエラーになります。うれしい。

“Exception: ReferenceError: |this| used uninitialized in Bar class constructor”

コンストラクタの最初に super() を呼んでください。

Uncaught ReferenceError: Must call super constructor in derived class before accessing ‘this’ or returning from derived constructor

実際は最初じゃなくてもいいんですが、スーパーコンストラクタに与える値をこねくり回す以外の処理はやめましょう。

どうしても長くて関数にしたい場合は、クラスメソッドにするかなあ。

class Bar extends Foo {
  constructor(v) {
    const v2 = Bar.makeFoo(v);
    super(v2);
  }
  
  static makeFoo(v) {
    return { foo: v.toUpperCase() };
  }
}

“Exception: SyntaxError: missing { before class body”

ついうっかりクラス名の方に括弧書いちゃうとこのエラーが出ます。

class Foo() {  // Uncaught SyntaxError: Unexpected token (
}

もちろん正しくはこうです。

class Foo {
}

prototype は健在

いちおうクラスという仕組みはできたんですが、その実態は今まで通りの prototype ベースのオブジェクトだったりします。なのでクラスを作ったら Foo.prototype は健在です。

クラスが紹介されたときも「これはただの糖衣構文 (syntax sugar) だ」と言われてました。(なのでBabelで旧環境向けに変換できる。)

互換性を保持したということっすかね。

参考

  • ECMAScript 2015 Language Specification – ECMA-262 6th Edition
    • 14.5 Class Definitions
    • 14.5.11 Static Semantics: PrototypePropertyNameList … プロトタイプオブジェクトを用意する話
    • 14.5.14 Runtime Semantics: ClassDefinitionEvaluation … プロトタイプオブジェクトを作る話
    • 10.2.1 Strict Mode Code … クラスはStrictモードだよ
    • 13.1.4 Static Semantics: DeclarationPart … 巻き上げについて
    • 15.2.3 Exports … export の右辺はクラス宣言
  • class – JavaScript | MDN

ついでに

この記事で、このブログのちょうど256件目の記事みたいです。思ったより少ない。