※スマホ対応はしてません。

タグ: es2015

JavaScript待望のクラス。(現代的JavaScriptおれおれアドベントカレンダー2017 – 04日目)

カテゴリー: JavaScript

現代的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件目の記事みたいです。思ったより少ない。

アロー関数でさくさくコールバック。(現代的JavaScriptおれおれアドベントカレンダー2017 – 03日目)

カテゴリー: JavaScript

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

便利ちょう便利。

要約

  • return するだけの場合、ブロックと return を省略できる
  • 一行の場合は自動的に return される
  • 引数がひとつだけの場合、括弧を省略できる
  • this の扱いに注意
  • arguments の代わりに ...args を使おう

使い方

例えばオブジェクトの配列↓を、

const data = [
  { id: 101, name: 'Alice' },
  { id: 102, name: 'Bob' },
  { id: 103, name: 'Charlie' },
];

IDだけの配列へ変換したい場合、やっぱり配列の map() を使うじゃないすか。

const ids = data.map(function(row) {
  return row.id;
});
console.log(ids);  // => [101, 102, 103]

これ↑をアロー関数にすると、こう↓です。

const ids = data.map((row) => {
  return row.id;
});
console.log(ids);  // => [101, 102, 103]

function を書かなくてもよくなりました。そんなに変わってないかもしれないけど、まずはここから。

省略して一行にする

いったんとりあえずそのまま一行にしてみます。

const ids = data.map((row) => { return row.id; });
console.log(ids);  // => [101, 102, 103]

今までも function 式でやってましたよね、こういうの。

さて、今回のように中身が一行だけの場合、なんと、ブロック { ~ } を省略できます。

すると、こう↓書けます。

const ids = data.map((row) => row.id);
console.log(ids);  // => [101, 102, 103]

おおっと、思ったより消えましたね。

return を省略する

実は一行のアロー関数にした場合、その => の右側が自動的に戻り値になります。そう、ブロックだけでなく return すら省略できるのです。

ちなみに戻り値が必要ない場合でも return はされますが、その値は捨てられるだけなので、特に問題はないです。逆に返しちゃいけない場合はブロック作って return undefined してください。

引数の括弧を省略する

加えて、引数の数がひとつだけの場合、括弧も外すことができます。

すると、こう↓書けます。

const ids = data.map(row => row.id);
console.log(ids);  // => [101, 102, 103]

最後に名前も短くしてしまいましょう。ほらこの場合は入る値が明白だから。

const ids = data.map(v => v.id);
console.log(ids);  // => [101, 102, 103]

だいぶ短くなりましたね。

function を単純にアロー関数へ置き換えられない場合

function 式をアロー関数で置き換えましたが、単純に置き換えできない場合もあります。

  • return が特殊な場合
  • this を使っている場合
  • arguments を使っている場合
  • new する場合

このうち return についてはもうやりました。

this が変わらない

例えば、これは駄目な例。(バック側のひとも知ってるかなと思ってるけどどう?)

el.addEventLister('click', (event) => {
  console.log(this === el);  // false
});

function だった頃は this は対象の要素を指すことが期待されました。ところがアロー関数はそうはなりません。

this は、アロー関数を実行した際の this になります。例えばコントローラーオブジェクトとか、グローバルオブジェクトとか。要は、かつて var that = this とか _this とかやってたのがいらなくなる感じです。便利っすねー。

さてじゃあ今回のこれどうするかというと、次項。

クリック他のイベントからは event.currentTarget で

対象の要素を取ってこれます。

// たまにはjQuery
$('.sugoi-button').on('click', (event) => {
  const el = event.currentTarget;
  console.log(el);
});

これはESではなくDOMの仕様。

arguments が取れない

関数実行時の引数リストを得る arguments なんですが、アロー関数の場合は普通の関数とは扱いが違うので、 arguments が用意されません。その外側の関数の引数になっちゃいます。

数がわからない場合、可変長引数 (rest parameters)で引数を全部取れます。

const f = (...args) => console.log(`${args.length}個の引数ゲットだぜ`);
f();  // => 0
f(1);  // => 1
f('ピカチュー', 'カイリュー', 'ヤドラン', 'ピジョン');  // => 4

可変長引数については後日別稿を。

new できない

コンストラクタになりません。

const Foo = () => {};
new Foo();  // TypeError: Foo is not a constructor

まあ this が固定されるという話も合わせれば、そりゃそうだよねーという感じはします。

その他

矢印 => 前は改行できない

なんか知らんけどそういう仕様になってます。

()
=> console.log('!');  // Exception: SyntaxError: no line break is allowed before '=>'

後ならよろしい。

引数がいらないなら _ とかにしたい

二文字 () 打つより一文字 _ の方が良くない? そうでもない?

アンダースコアは「受け取るけど捨てる引数」という意味です。(仕様じゃなくて風習。他言語の。)

ただ、関数に定義されている引数の数って fn.length で取れるんだけど、それを見て処理を変えてる場合はこれだと駄目です。例えばChaiのやつとか。

参考