new を不当に貶める陰謀と JavaScript におけるクラスの継承構造の話

私は陰謀論者じゃないですし JavaScriptnew 演算子が大好きなわけでも大嫌いなわけでもないです。 念のため。 本記事は Hiraku さんが書かれた下記記事への言及です。

new 演算子は使うな!?

newを封印するべき4つの理由」 でも new がいかに糞であるかが書かれていますし、その記事からも言及があるように Crockford さんが書かれた書籍 『JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス』 でも new 演算子は Bad Parts に分類されています。

new 演算子が忌避される理由はいろいろあるみたいですが、Hiraku さんの記事では

  • new を書き忘れてもコンストラクタ関数が実行されて意図しない動作になる
  • 継承を考慮する必要がある
  • 何をやっているのかわかりにくい
  • 読みづらい

という点が指摘されています。

常に new 演算子を使うことが最良の選択だとは思っていませんが、ことさら new 演算子を貶める言説も好きじゃないのでちょっと反論しておこうかな、というのがこの記事の主題です。

各継承方法のオブジェクトの関係図

はじめに、議論の土台として、純粋なプロトタイプ継承と、クラスの継承構造を持ち込んだ場合の継承方法について、オブジェクトの関係図を示します。

純粋なプロトタイプ継承

まずは JavaScript の純粋なプロトタイプ継承の形を見てみましょう。

ECMA-262 (5th edition) を満たしている JavaScript 処理系なら、Object.create メソッドで簡単にプロトタイプ継承が可能です。 Hiraku さんの記事で定義されている object 関数は、この Object.create メソッドと同じようなものです。

var baseObj = {
  name: "ベースオブジェクト", 
  printName: function(){ print( this.name ) }
};
// Object.create メソッドでプロトタイプ継承する
var subObj = Object.create( baseObj, { name: { value: "サブオブジェクト" } } );
// 使ってみる
subObj.printName() // サブオブジェクト

このような純粋なプロトタイプ継承による継承構造を図で表すと次のようになります。 非常に単純ですね。


new 演算子とクラスの継承構造

次に、プロトタイプ継承と new 演算子によるオブジェクト生成の機構を利用して、クラスの継承構造を JavaScript で実現してみましょう。 詳しい実現方法は 「JavaScript におけるクラスの作成と継承」 の記事で述べましたので、ここでは詳細には立ち入りません。 基本的な思想としては、インスタンス (に相当するオブジェクト) で全てのインスタンス変数を保持し、プロトタイプ継承の先祖のオブジェクトでインスタンスメソッドを定義する、という形式です。 コンストラクタ関数オブジェクトがクラスそのものを表しています。

少しややこしいですが、インスタンス生成には new 演算子、クラスの継承構造の生成には関数定義、という風に明確に分離しています。 また、クラスを表現するオブジェクト (コンストラクタ) とインスタンスメソッドを定義するオブジェクトが分かれているため、コンストラクタのプロパティによってクラスメソッドやクラス変数を実現できます。

Hiraku さんの記事におけるクラスの継承構造

最後に、Hiraku さんの記事に書かれている方法でクラスの継承構造を実現した場合のオブジェクトの関係を図に示します。

JavaScript における純粋なプロトタイプ継承の継承構造に、無理やりクラスとインスタンスという概念を持ち込んでいるように私は思います。 以下のような問題点があるように思います。

  • クラスとインスタンスの区別が明確でない : クラスの継承もインスタンスの生成も完全に同じ機構で実現している。 同じプロトタイプ継承によって実現しているとしても、クラスとインスタンスという概念を持ち込むのであれば、区別するようにしたほうが良い
  • クラスメソッドとインスタンスメソッドのを区別が明確でない : クラスを表すオブジェクトとインスタンスメソッドを定義するオブジェクトが同一なので、クラスメソッドとインスタンスメソッドを区別できない
  • 明示的なコンストラクタ呼び出しが必要 : new 演算子によるオブジェクト生成の場合は暗黙的にコンストラクタ関数が呼び出されましたが、この方法では明示的にコンストラクタの呼び出しが必要です

4 つの new 批判に対する反論

それでは 「newを封印するべき4つの理由」 に対して反論してみます。

new を書き忘れてもコンストラクタが実行されてしまう問題
var Person = function Person( name ) {
    this.name = name;
};
Person.prototypte.sayHello = function sayHello() {
    print( "Hello, I'm " + this.name + "." );
};

// new を忘れて実行
var tarou = Person( tarou );

確かに、new を書き忘れてもコンストラクタ関数が実行されてしまいます。 それは認めましょう。 ですが、これの何が問題なんでしょうか? new を書き忘れることってありますか? 書き忘れたとして、それが発見困難なバグに繋がりますか?

私の感覚では、new の書き忘れは引数の個数不足での関数呼び出しと同じレベルの問題であると思います。 引数の個数や型チェックが必要なら関数内でチェックをするように、チェックする必要があるならコンストラクタ関数内で this が何を指しているのかチェックすればいいだけです。

new の書き忘れで時間を浪費するようなプログラマが居るならば、その人はプログラマに向いていないので転職を勧めてあげましょう。

継承を考慮する必要があるという問題

『この書き方は、object関数をインライン展開したのと同義です…。それならいっそのこと、常にobject関数を使うようにした方が楽だと思いませんか?』 とありますが、クラスの継承を行う関数を定義すればいいだけのことです。 例えば 「JavaScript におけるクラスの作成と継承」 の記事では、クラスの継承のための extend という関数を定義しました。

何をやっているのかわかりにくいという問題

JavaScript自体はプロトタイプベースで作られているくせに、newを使うとクラスベースみたいな書き方をする必要があります。』 って、new メソッドはプロトタイプ継承を応用してクラスの継承構造を JavaScript に持ち込むためのものなので当然でしょう。 クラスの継承構造を持ち込むのであれば、インスタンス化に new 演算子を使うことは他の言語との類似性から言ってもわかりやすい と思います。 もちろんクラスの継承構造を使わないのに new 演算子を使うことは混乱の元になると思いますが。

読みづらいという問題

個人的にはオブジェクトリテラルの中に関数式を書くのは好きじゃないのでどっちかというと

var Animal = {
  name: "動物"
, breathe: function(){alert("すーはー")}
, sayName: function(){alert(this.name)}
};

よりも

var Animal = function Animal( name ) {
    this.name = name;
};
Animal.prototype.breath = function breath() {
    print( "すーはー" );
};
Animal.prototypte.sayName = function sayName() {
    print( this.name );
};

の書き方が好きなんですが。 まあでも多くの人は前者の書き方の方が好みなんだろうなー、とは思います。

ていうかこの name ってなんなんでしょう。。 クラス変数?

そもそも new 演算子のどの部分を批難しているのか?

というわけで 4 つの問題点に対する反論 (になってるかどうかは微妙ですが) を書いてきましたが、そもそも new を封印する目的がいまいちよくわかりません。

  • 本当に、ただ純粋に new 演算子を使わないようにすべき、という主張?
  • JavaScript にクラスの継承構造を持ち込むな、という主張?

本当に、ただ純粋に new 演算子を使わないようにすべき、という主張は JavaScript に限らず Java などでもあります。 new 演算子によるインスタンス化は柔軟性に欠ける、というものです。 インスタンス化のための API としてユーザーにはファクトリーメソッドを提供し、new 演算子は表から見えないようにするべきであるという主張であり、『Effective Java 第2版 (The Java Series)』 などに書かれています。

しかし、今回の場合は単純に new 演算子を表から隠すだけでなく継承構造も変えていますし、単純に new 演算子を表から隠せ、という主張ではないように思います。

それでは、JavaScript にクラスの継承構造を持ち込むな、という主張なのかというとそれも違います。 Hiraku さんの記事では 「クラス(に相当するオブジェクト)を作る」 という表現が出てきていますし、クラスの継承構造を実現したいように思えます。

クラスの継承構造が不要な場面ではわざわざクラスの継承構造なんか作らずに、JavaScript の基本的な継承であるプロトタイプ継承を使いましょう!」 という主張なら納得するのですが、Hiraku さんの記事では結局のところ何が目的なのかよくわからなくなっており、「俺俺オブジェクト指向な気がする」 と言われても仕方ないかなー、と思います。

new 演算子を使うとわかりやすい? わかりにくい? (追記)

ちょっと重要なことなので追記。

本記事の上のほうでも 「クラスの継承構造を持ち込むのであれば、インスタンス化に new 演算子を使うことは他の言語との類似性から言ってもわかりやすい」 と述べましたし、id:teramako さんも 「僕はnew演算子は好きです。書き忘れが発生してorzすることはあるけど。何故好きかというと、コードを読んでいてインスタンスを作成していることが明確に分かるから」 と仰っています。 一方で、Hiraku さんは 「JavaScript自体はプロトタイプベースで作られているくせに、newを使うとクラスベースみたいな書き方をする必要があります。中でやっていることと、外側から見えるインターフェースが違いすぎる」 という立場です。

もちろん、どちらが間違っている、ということはありません。 とはいえどちらかの立場をとらなければいけないのであれば、「誰にとってわかりやすくすべきか」 という視点を取り入れるべきでしょう。

クラスという概念を取り入れる以上、クラスの設計者とクラスのユーザーという 2 人の視点で考える必要があります。 これはしばしば同一人物でありますが、同一人物ではない可能性もあります。 そして、クラスというものは クラスのユーザーにとってわかりやすい ものであるべきだと思います。 クラスの設計・実装者は当然 JavaScript に詳しいはずですので、プロトタイプ継承がどうとか、そういう点は問題なく認識しているはずです。 一方、クラスの使用者はクラスを提供されているだけなので、「クラスに見えるけどプロトタイプ継承で実装されている」 という点は認識していないかもしれません。 また、そのように認識させる必要もありません。 なぜなら 「クラス」 を提供しているのですからクラスのように扱えればいいのです。

例えば、Person というクラスを提供するとしましょう。 このクラスは人物を表すもので、人物の名前を引数にとってインスタンス化するものとしましょう。 こう聞くと、クラスのユーザーはほぼ間違いなく以下のようなコードでインスタンス化できるものと期待するはずです。

var tarou = new Person( "田中 太郎" );

「クラスをプロトタイプ継承したオブジェクトを作って、自分で初期化関数を呼んでください」 などと言われるとどうでしょう?

var tarou = Object.create( Person );
tarou.init( "田中 太郎" );

という感じでしょうか。 お世辞にも良いインターフェイスだとは言えません。 ではオブジェクトの生成と初期化を行う関数を Person オブジェクトに追加してみますか?

// ファクトリーメソッド
Person.newInstance = function newInstance( name ) {
    Object.create( this );
    this.init( name );
};
// インスタンス化
var tarou = Person.newInstance( "田中 太郎" );

すっきりしました。 これなら良いでしょう。 しかしこうなるともはや従来の手法でいい気がします。 クラスの実装方法に違いはありますが、上でも言ったようにクラスの設計・実装者には十分な知識があるはずなのでどちらの実装方法でもあまり問題にはなりません。 それよりも、ユーザーが標準的な使用方法でクラスを使用できるかどうか、が重要なのです。

標準的な使用方法で使えるのかどうかという点において、インスタンス化についてよりももっと大きな問題があります。 instanceof 演算子を実直に使えないという問題です。 (そもそも instanceof 演算子は使うべきではない、という主張もありますが、それはさておき。) ユーザーは、Person クラスをインスタンス化した tarou は、以下の式で true になると期待するでしょう。

tarou instanceof Person; //=> true

しかし、Hiraku さんとこの方法では

tarou instanceof type( Person ); //=> true

などとしなければいけません。 大規模なフレームワークとしてクラス機構を提供するのであれば新しい書き方を導入するのもひとつの手ではありますが、単一、または少数個のクラスを提供する際にこのような書き方をクラスのユーザーに強要するのは無理というものです。 様々なクラスの提供者がそれぞれ独自に type 関数のような関数を提供したらどうでしょう? 使いにくいですよね。 これが 「俺俺オブジェクト指向」 と呼ばれてしまったひとつの理由でしょう。

プロトタイプ継承という機構を隠してしまったことの弊害

しかしながら、JavaScript が 「クラスの継承構造のような機構」 を提供し、プロトタイプ継承という機構を隠してしまったがための弊害もあります。 Hiraku さんが仰るように 「実際の挙動 (プロトタイプ継承) と見た目 (クラスの継承やインスタンス化) が異なっている」 というものです。

JavaScriptnew Date() のようにインスタンス化を行ったことがある人はたくさんいると思いますが、プロトタイプ継承という継承方法を知っている人は思っている以上に少ないのではないか、と思います。 プロトタイプ継承という機構が裏に隠れてしまっているために、(誰かが作ったクラスを使うだけなら) プロトタイプ継承について勉強する必要が無いためです。 特に JavaScriptプログラマだけでなく、web デザイナーなど本職がプログラマじゃない人にも使われていますので、ちゃんと勉強してない人も多いはずです。

そのような 「クラスを使ったことはあるけどプロトタイプ継承はよく知らない」 というような人がクラスを設計・実装しようとするとよくわからなくなって混乱してしまいます *1。 そして、「JavaScript のクラスってよくわからない」 ということになってしまうのではないかと思います。

この点はどうしようもないかなーという気がするのですが、とりあえず言えることは 「他の言語と同じようにクラスを使えるからといってろくに勉強せずクラスの設計・実装をしようとすると痛い目を見るので、まず JavaScript の勉強をしましょう」 ということですね。

私の主張

最後に JavaScript におけるオブジェクトに関して私の主張をちょこっとしておきます。

new 演算子がしばしば攻撃の対象になるのは、他の言語との類推から JavaScript においてもなんでもかんでも new 演算子を使ってオブジェクト生成するような人がいるからだと思います。 他の言語、例えば Java などではオブジェクトを生成するために基本的 *2new を使います。 一方、JavaScript では new 演算子が必要となる場面はそれほど多くありません。

JavaScript は弱い動的型付け言語であり、使い捨てのオブジェクトをオブジェクトリテラルで生成することができます。 なんらかのオブジェクトを継承したい場合、クラスの継承などしなくてもプロトタイプ継承によりオブジェクトそのものを継承することができます。 特に web サイト上で動く JavaScript は使い捨てのオブジェクトを使うことが多く、クラスの継承構造が必要になることはあまり多くないでしょう。

一方で、クラスの継承構造を利用すると便利な場面もあります。 同じメソッドを持ち、異なるデータを保持する多くのオブジェクトを使いたい場面では、クラスの継承構造が役に立ちます。 そういうところでは、クラスの継承構造をつくり、new 演算子 ((new じゃなくてファクトリーメソッドなどを使うべき、という議論もありますが、それはまた別のお話ということで。)) を使ってインスタンス化する、ということをすれば良いでしょう。

使いどころを考えて、使うべきところで使うべき方法をとるというのが重要です。

*1:Hiraku さんが仰っている 「よくわからない」 という指摘はこの点なんじゃないかと思います。

*2:ファクトリーメソッドなどもありますが。。